「気づいたらポイントが消えていた」を防ぐには、頭で覚えるより、仕組みに見張らせるほうが確実です。結論を先に言うと、残高と失効日を1枚のスプレッドシートに書くだけで、動き出す日が自動で出て、カレンダーが毎朝チェックして知らせてくれます。
この記事では、Googleスプレッドシートと、コピペするコード(Google Apps Script、略してGAS)で失効管理シートを作ります。コードを1回貼って「setup」を実行すると、表・計算式・色分け・毎朝の自動チェックまで整います。プログラミングの知識は要りません。
どのポイントがいつ失効するかの一覧はポイントの有効期限まとめに書きました。この記事は、その期限を見張る仕組みを作る実践編です。コードは2026年10月時点のGoogle Apps Scriptの仕様に沿って書いています。Googleの画面や仕様は変わることがあります。
結論:管理するのは「失効日」と「動き出す日」の2つ
失効の取りこぼしは、期限を知らなかったからより、知っていたのに動くのが遅れたから起きます。ポイントの交換や反映には日数がかかるものがあるので、失効日だけをメモしても、ぎりぎりで気づくと間に合いません。そこでこの表は、失効日から逆算した「動き出す日」を自動で出して、その日にカレンダーで知らせます。
残高と失効日の入力は手作業です。行数が10行ほどなら、月に1回5分前後が目安です。サービスにログインして残高を自動で取ってくる仕組みは、あえて作りません。理由は「このシートにできないこと」の章に書きました。
表の列は14個|入力するのは9つだけ、あとは自動
表(「残高」シート)の列は次のとおりです。入力が必要なのは9列だけで、残りの5列は自動で出ます。
| 列 | 入力/自動 | 中身 |
|---|---|---|
| サービス名 | 入力 | 「dポイント(通常)」「楽天ポイント(期間限定)」のように、通常と期間限定は行を分ける |
| 種類 | 入力 | 延長型・年度型・付与ごと型・期間限定・無期限から選ぶ |
| 残高(pt) | 入力 | いまのポイント数 |
| 1ptあたり(円) | 入力 | 1ポイント=1円なら1、10ポイント=1円なら0.1 |
| 円換算 | 自動 | 残高 × 1ptあたり(円) |
| 失効日 | 入力 | 最後に使える日。サービスの画面に出ている失効予定日を写す。無期限は空欄 |
| 残り日数 | 自動 | 今日から失効日までの日数 |
| 出口(使い道) | 入力 | 店で使う、銀行に交換する、など |
| 動かす日数 | 入力 | 使う・交換するのに必要な日数(決め方は後の章) |
| 動き出す日 | 自動 | 失効日 − 動かす日数 − 余裕日数(初期値7日) |
| 状態 | 自動 | 今すぐ/注意/OK/失効済のどれか |
| 最終確認日 | 入力(任意) | 残高と失効日を最後に確かめた日 |
| メモ | 入力(任意) | 自由欄。先頭が「サンプル」の行は、カレンダーに登録されない |
| 登録ID | 自動 | カレンダーの予定と結びつける欄。隠れていて、触らなくてよい |
「状態」は4つで、色でも分かります。
失効日の調べ方|種類ごとに「何を表に写すか」
失効日は、自分で計算せず、サービスの画面に出ている失効予定日を写すのが基本です。数え方がサービスごとに違うからです。たとえば楽天ポイント(通常)は「最後にポイントを獲得した月」から数えるので、使うだけでは延びません。dポイントは「最後にためた・使った日」から12か月後ですが、ドコモ料金で進呈されたポイントでは延びません。
| 種類 | 失効日の入れ方 | 確認する場所の例 |
|---|---|---|
| 延長型 | 画面に出ている失効予定日。ためたり使ったりして延びたら更新する | dポイントクラブのアプリ・サイトの「ポイント詳細」→「1年以内に失効するポイント」 |
| 年度型 | 決まった日にまとめて失効する。1回入れれば、その年度は変わらない | nanacoポイントは毎年3月末(2025年4月〜2026年3月分は2027年3月31日)。WAON POINTは人ごとに違うので、iAEONアプリなどで確認 |
| 付与ごと型 | 期限が近い分だけを1行にする(残高も近い分だけ) | ポイントサイトの通帳、ドットマネーの失効予定など |
| 期間限定 | 期限が違う分は、期限ごとに行を分ける | 楽天PointClubの「ポイント実績」、PayPayアプリ、Amazonの「マイポイント」 |
| 無期限 | 失効日は空欄にする。状態も空欄になり、通知は作られない | PayPayポイント(通常)など。通常は期限がない |
ポイントを別のサービスに交換したら、交換先のポイントを新しい行に入れてください。たとえばPayPayポイントをVポイントに交換すると、交換したVポイントには交換日から1年の期限が付きます。無期限のポイントが、期限付きに変わることがあります。
確認画面の名前は、当サイトのポイントの有効期限まとめを確認した2026年9月28日時点のものです。画面の名前は変わることがあります。
作り方は3ステップ|空のスプレッドシートにコードを貼って、1回実行
作業は、パソコンのブラウザで行います。所要時間は5分ほどです。
空のスプレッドシートを作る
Googleスプレッドシートで新しい空白のファイルを作り、「ポイ活残高管理」などの名前を付ける。データが入っているファイルでは実行しない(コードが中止する)。
コードを貼って保存する
メニューの「拡張機能」→「Apps Script」を開く。最初から入っているコードを全部消して、次の章のコードを貼り付け、保存する。
「setup」を1回だけ実行する
画面上部の関数の選択欄で「setup」を選んで「実行」を押す。権限の確認画面が出たら許可する。「準備できました」と出れば完了。スプレッドシートに戻ると、表とサンプル5行ができている。
できあがるのは「残高」シートと「設定」シートです。設定シートには、余裕日数(7日)と注意の範囲(30日)が入っていて、数字を変えれば表の判定も変わります。毎朝8時台の自動実行も、setupの中で一緒に入ります。
メールの閲覧・送信、ドライブ全体、外部サービスとの通信を求める項目が出たら、いったん止めて、貼ったコードを見直してください。この記事のコードは、これらを使っていません。カレンダーの権限は、Googleの仕様で、アカウントのカレンダー全体を表示・編集する権限として出ます。コードは「ポイ活期限」という専用カレンダーだけを作って使います。スプレッドシートの権限は、このファイルだけに絞っています。
コード全文|Apps Scriptに丸ごとコピペ
次のコードを、Apps Scriptの画面に丸ごと貼ってください。コードの中で変えてよいのは、先頭の設定値だけです。
/** @OnlyCurrentDoc */
// ポイ活 残高・失効管理シート(ポイ活マエストロへの道!)
// 使い方:setup を1回だけ実行 → 表に入力 → 毎朝8時台に dailyCheck が自動で動く
const SHEET = '残高', CONF = '設定', CAL = 'ポイ活期限';
const ROWS = 200; // 入力できる行数
const HOUR = 10; // カレンダーに予定を入れる時刻(朝のチェックの後)
const REMIND = 10; // 予定の何分前に通知するか
const MAIL = false; // true にするとカレンダーのメール通知も付く
const TYPES = ['延長型', '年度型', '付与ごと型', '期間限定', '無期限'];
const HEAD = ['サービス名', '種類', '残高(pt)', '1ptあたり(円)', '円換算', '失効日', '残り日数', '出口(使い道)', '動かす日数', '動き出す日', '状態', '最終確認日', 'メモ', '登録ID'];
// ---- 初期設定(1回だけ) ----
function setup() {
const ss = SpreadsheetApp.getActive();
let sh = ss.getSheetByName(SHEET);
if (!sh) {
sh = ss.getSheets()[0];
if (sh.getLastRow() > 0) return SpreadsheetApp.getUi().alert('空のスプレッドシートで実行してください');
sh.setName(SHEET);
} else if (sh.getLastRow() > 1) {
return SpreadsheetApp.getUi().alert('入力済みのデータがあるため中止しました');
}
ss.setSpreadsheetTimeZone(Session.getScriptTimeZone());
const n = ROWS - 1;
sh.getRange(1, 1, 1, HEAD.length).setValues([HEAD]).setFontWeight('bold').setBackground('#3F51B5').setFontColor('#ffffff');
sh.setFrozenRows(1);
sh.setFrozenColumns(1);
const f = [[], [], [], []];
for (let r = 2; r <= ROWS; r++) {
f[0].push([`=IF(OR(C${r}="",D${r}=""),"",C${r}*D${r})`]);
f[1].push([`=IF(F${r}="","",F${r}-TODAY())`]);
f[2].push([`=IF(F${r}="","",F${r}-N(I${r})-'設定'!$B$2)`]);
f[3].push([`=IF(OR(F${r}="",N(C${r})<=0),"",IF(F${r}<TODAY(),"失効済",IF(TODAY()>=J${r},"今すぐ",IF(TODAY()>=J${r}-'設定'!$B$3,"注意","OK"))))`]);
}
[[5, f[0]], [7, f[1]], [10, f[2]], [11, f[3]]].forEach(p => sh.getRange(2, p[0], n, 1).setFormulas(p[1]));
[[3, '#,##0'], [4, '0.0#'], [5, '#,##0'], [6, 'yyyy/mm/dd'], [7, '0'], [9, '0'], [10, 'yyyy/mm/dd'], [12, 'yyyy/mm/dd']].forEach(p => sh.getRange(2, p[0], n, 1).setNumberFormat(p[1]));
sh.getRange(2, 2, n, 1).setDataValidation(SpreadsheetApp.newDataValidation().requireValueInList(TYPES, true).setAllowInvalid(false).build());
const dv = SpreadsheetApp.newDataValidation().requireDate().setAllowInvalid(false).setHelpText('日付で入力(例 2026/12/1)').build();
sh.getRange(2, 6, n, 1).setDataValidation(dv);
sh.getRange(2, 12, n, 1).setDataValidation(dv);
const rg = sh.getRange(2, 11, n, 1);
sh.setConditionalFormatRules([['今すぐ', '#F4C7C3'], ['注意', '#FCE8B2'], ['OK', '#CDEBD3'], ['失効済', '#D9D9D9']].map(p =>
SpreadsheetApp.newConditionalFormatRule().whenTextEqualTo(p[0]).setBackground(p[1]).setRanges([rg]).build()));
[[1, 200], [2, 90], [3, 80], [4, 100], [5, 80], [6, 100], [7, 80], [8, 200], [9, 90], [10, 100], [11, 70], [12, 100], [13, 220]].forEach(p => sh.setColumnWidth(p[0], p[1]));
sh.hideColumns(14);
// ---- サンプル5行(メモが「サンプル」で始まる行は、カレンダーに登録されません) ----
sh.getRange(2, 1, 5, 4).setValues([
['dポイント(通常)', '延長型', 3200, 1],
['楽天ポイント(期間限定)', '期間限定', 800, 1],
['ポイントサイト(10pt=1円の例)', '延長型', 54000, 0.1],
['nanacoポイント', '年度型', 1200, 1],
['PayPayポイント(通常)', '無期限', 600, 1]]);
sh.getRange(2, 6, 5, 1).setValues([[new Date(2026, 11, 1)], [new Date(2026, 10, 30)], [new Date(2027, 0, 20)], [new Date(2027, 2, 31)], ['']]);
sh.getRange(2, 8, 5, 2).setValues([['店頭で使う', 3], ['楽天市場で使う', 0], ['銀行に交換', 7], ['電子マネーに交換', 3], ['', '']]);
sh.getRange(2, 12, 5, 2).setValues([1, 2, 3, 4, 5].map(() => [new Date(), 'サンプル:消して使う']));
const cf = ss.getSheetByName(CONF) || ss.insertSheet(CONF);
cf.getRange(1, 1, 3, 2).setValues([['項目', '値'], ['余裕日数', 7], ['注意の範囲(日)', 30]]);
cf.getRange(5, 1).setValue('動き出す日 = 失効日 − 動かす日数 − 余裕日数。動き出す日の「注意の範囲」日前から「注意」、当日以降は「今すぐ」。');
ss.setActiveSheet(sh);
installTrigger();
SpreadsheetApp.getUi().alert('準備できました。サンプルを消して、自分の残高を入力してください。毎朝8時台に自動で確認します。');
}
// ---- 毎朝8時台に動く本体 ----
function dailyCheck() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName(SHEET), cf = ss.getSheetByName(CONF);
if (!sh || !cf) return;
const buf = Number(cf.getRange(2, 2).getValue()), warn = Number(cf.getRange(3, 2).getValue());
const cal = getCal_();
const today = day_(new Date());
const rows = sh.getRange(2, 1, ROWS - 1, 14).getValues();
const ids = rows.map(r => [r[13]]);
rows.forEach((r, i) => {
const [name, , bal, , , exp, , exit, move, , , , memo, id] = r;
const st = calc_(exp, bal, move, buf, warn, today);
let ev = null;
if (id) { try { ev = cal.getEventById(id); } catch (e) { ev = null; } }
if (!name || !st || st.status === '失効済' || String(memo).indexOf('サンプル') === 0) {
if (ev) ev.deleteEvent();
ids[i] = [''];
return;
}
const when = st.start < today ? today : st.start;
const title = `【ポイ活】${name} ${Number(bal).toLocaleString()}pt ${st.status === '今すぐ' ? '今すぐ動かす' : '動き出す日'}(失効 ${exp.getMonth() + 1}/${exp.getDate()}・あと${st.left}日)`;
const desc = `出口:${exit || '未入力'}\n失効日:${Utilities.formatDate(exp, Session.getScriptTimeZone(), 'yyyy/MM/dd')}\nメモ:${memo || ''}\n※この予定はスクリプトが自動で更新します。`;
if (ev && day_(ev.getStartTime()).getTime() === when.getTime()) {
ev.setTitle(title);
ev.setDescription(desc);
} else {
if (ev) ev.deleteEvent();
const at = new Date(when.getFullYear(), when.getMonth(), when.getDate(), HOUR, 0);
ev = cal.createEvent(title, at, new Date(at.getTime() + 15 * 60000), { description: desc });
ev.removeAllReminders();
ev.addPopupReminder(REMIND);
if (MAIL) ev.addEmailReminder(REMIND);
}
ids[i] = [ev.getId()];
});
sh.getRange(2, 14, ROWS - 1, 1).setValues(ids);
}
// ---- 部品 ----
function calc_(exp, bal, move, buf, warn, today) {
if (!(exp instanceof Date) || !(Number(bal) > 0)) return null;
const e = day_(exp);
const start = new Date(e.getFullYear(), e.getMonth(), e.getDate() - (Number(move) || 0) - buf);
const left = Math.round((e - today) / 864e5);
const status = e < today ? '失効済' : today >= start ? '今すぐ' : today >= new Date(start.getFullYear(), start.getMonth(), start.getDate() - warn) ? '注意' : 'OK';
return { start, left, status };
}
function day_(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()); }
function getCal_() {
const found = CalendarApp.getCalendarsByName(CAL);
return found.length ? found[0] : CalendarApp.createCalendar(CAL);
}
function installTrigger() {
removeTrigger();
ScriptApp.newTrigger('dailyCheck').timeBased().everyDays(1).atHour(8).create();
}
function removeTrigger() {
ScriptApp.getProjectTriggers().filter(t => t.getHandlerFunction() === 'dailyCheck').forEach(t => ScriptApp.deleteTrigger(t));
}| 設定値 | 初期値 | 意味 |
|---|---|---|
| ROWS | 200 | 入力できる行数(式を入れる行数)。通常は変えない |
| HOUR | 10 | カレンダーに予定を入れる時刻。朝8時台のチェックのあと |
| REMIND | 10 | 予定の何分前に通知するか |
| false | trueにすると、カレンダーのメール通知も付く | |
| 設定シート B2 | 7 | 余裕日数。動き出す日を、失効日のさらに何日前にするか |
| 設定シート B3 | 30 | 注意の範囲。動き出す日の何日前から「注意」にするか |
コードがすることは4つです。setupは、表と設定シートを作ります(初回に1回だけ)。dailyCheckは、毎朝、表を見て予定を作る・更新する・消すところまでをします。installTriggerとremoveTriggerは、毎朝の自動実行を入れる・外す係です。
予定の入り方は、次のとおりです。残高が1以上で、失効日を過ぎていない行について、動き出す日の10時に15分の予定を入れます。状態が「今すぐ」の行は、毎朝その日の予定に作り直すので、動かすまで毎朝通知が来ます。残高を0にする、失効日を過ぎる、サービス名を消すと、予定は消えます。
このコードは、Google以外のサーバーと通信しません。表のデータは、あなたのGoogleアカウントの中だけで処理されます。
月1回5分の更新ルーティンと、動かす日数の決め方
表は、入れて終わりではありません。月に1回、残高と失効日を見直すのが、いちばん取りこぼしにくい進め方です。
「動かす日数」は、出口ごとに次のように入れます。
| 出口 | 入れる日数の考え方 |
|---|---|
| 店頭・ネットで使う | 0〜3日。dポイントは、店で使った日ではなく履歴の反映日で判定される。反映に数日かかることがあるので、共通ポイントは3日を目安にする |
| ポイントサイトから銀行・電子マネー・他のポイントへ交換 | 交換ページに書かれた「反映までの日数」のうち、長いほうに合わせる。書かれていなければ14日にしておく(これは当サイトの目安) |
| 別のサービスを経由して交換 | それぞれの日数を足す |
| 交換先が止まっているとき | 余裕日数の7日を使って、別の出口を探す。出口の考え方はポイントの出口設計にまとめた |
💡 日数は「長めに見る」が安全
余裕日数の7日は、使い道を選ぶ時間と、交換先が止まっていたときに別の出口を探す時間のために足しています。端数のポイントの使い道はポイントの端数を使い切る方法も参考にしてください。
入れてはいけない情報と、権限の守り方
ポイ活のアカウントは、不正アクセスの被害が実際に出ています。表に書くのは、サービス名・残高・失効日・出口だけで足ります。ログインに使える情報は、1つも書かないでください。
① ID・パスワード・認証コード
書かない。表に必要なのは、サービス名と残高と失効日だけ。
② 会員番号・カード番号・口座番号
書かない。サービスを見分けるだけなら、サービス名で足りる。
③ 共有設定
スプレッドシートは自分だけにする。「リンクを知っている全員」にしない。
④ 出所の分からないコード
他人のコードを貼る前に、全文を読む。メール・ドライブ全体・外部への通信を求めるコードは使わない。
💡 安全に使うための3つの設定
① スプレッドシートの共有は「制限付き」のままにする。② Googleアカウントの2段階認証をオンにする。③ やめるときは、removeTriggerを実行して、カレンダー「ポイ活期限」とスプレッドシートを削除する。ポイントサイト側の自衛はポイントサイトの不正アクセス対策にまとめました。
このシートにできないこと4つ
万能ではありません。できないことを先に知っておくと、安心して使えます。
よくある質問
まとめ
- 残高・失効日・出口・動かす日数を書けば、動き出す日と状態は自動で出る
- 失効日は計算せず、サービスの画面に出ている失効予定日を写す
- コードを貼ってsetupを1回実行すると、毎朝8時台に確認し、専用カレンダーに予定を入れる
- ID・パスワード・カード番号は表に書かない。共有も「制限付き」のまま
- 月に1回、残高と失効日を更新する。行は削除せず、残高を0にする
関連記事:ポイントの有効期限まとめ、ポイントの出口設計、ポイントの端数を使い切る方法、ポイントサイトの不正アクセス対策
参考にした公式情報(2026年10月27日確認):Google Apps Script「ClockTriggerBuilder」、Google Apps Script「CalendarEvent」、Google Apps Script「OAuth Client Verification」、Google Cloud ヘルプ「Unverified apps」
本記事のコードと表は、公式ドキュメントに沿って作成した例です。Googleの仕様や画面は変更される場合があります。ご自身のアカウントで使う前に、サンプルで試運転し、権限の内容をご自身で確認してください。ポイントの有効期限や条件は、各サービスの公式案内が優先されます。

コメント