乐天Pay是高返点消费路线里很重要的一环,但它一直没有被Zaim官方支持自动导入——每次消费都要手动记账,时间一长很容易漏记。在参照了这篇Zenn上的文章之后,我用Google Apps Script(GAS)把这件事实现了自动化。这篇文章记录完整的设置步骤,以及2025年11月更新修复的一个问题。
前提条件
- 有Google账户(用于运行GAS脚本)
- 已设置将乐天Pay的使用记录发送至该Gmail(类似下图的通知邮件)
- 已在Zaim中手动添加乐天Pay账户
-15.png)
设置步骤
1在 Google Apps Script 新建项目
2给项目起个名字
3将代码粘贴到编辑区域右侧
-13.png)
代码如下(注释为日文,看不懂的地方请使用翻译工具)。2025年11月22日更新:修复了第117行的问题。
const CONSUMER_KEY = PropertiesService.getScriptProperties().getProperty("ZAIM_CONSUMER_ID");
const CONSUMER_SECRET = PropertiesService.getScriptProperties().getProperty("ZAIM_CONSUMER_SECRET")
const RAKUTEN_PAYMENT_ID = parseFloat(PropertiesService.getScriptProperties().getProperty("RAKUTEN_PAYMENT_ID"));
const DEFAULT_CATEGORY_ID = parseFloat(PropertiesService.getScriptProperties().getProperty("DEFAULT_CATEGORY_ID"));
const DEFAULT_GENRE_ID = parseFloat(PropertiesService.getScriptProperties().getProperty("DEFAULT_GENRE_ID"));
// 今日の日付を取得
var date = new Date(); // 現在の日付と時刻を取得
var today = Utilities.formatDate(date, Session.getScriptTimeZone(), "yyyy-MM-dd");
// 1日前の日付を取得
var yesterday = new Date(date.getFullYear(), date.getMonth(), date.getDate() - 1);
yesterday = Utilities.formatDate(yesterday, Session.getScriptTimeZone(), "yyyy-MM-dd");
// 認証のリセット
function reset() {
var service = getService();
service.reset();
}
// 認証サービスの設定
function getService() {
return OAuth1.createService("Zaim")
.setAccessTokenUrl("https://api.zaim.net/v2/auth/access")
.setRequestTokenUrl("https://api.zaim.net/v2/auth/request")
.setAuthorizationUrl("https://auth.zaim.net/users/auth")
.setConsumerKey(CONSUMER_KEY)
.setConsumerSecret(CONSUMER_SECRET)
.setCallbackFunction("authCallback")
.setPropertyStore(PropertiesService.getUserProperties());
}
// OAuth Callbackの設定
function authCallback(request) {
var service = getService();
var authorized = service.handleCallback(request);
if (authorized) {
return HtmlService.createHtmlOutput("認証できました!このページを閉じて再びスクリプトを実行してください。");
} else {
return HtmlService.createHtmlOutput("認証に失敗");
}
}
// GETパラメーターを作成
function encodeParams(params) {
var encodedParams = [];
for (var key in params) {
encodedParams.push(encodeURIComponent(key) + "=" + encodeURIComponent(params[key]));
}
return encodedParams.join("&");
}
// 過去の支払いデータを取得
function getPastData(service) {
var url = "https://api.zaim.net/v2/home/money";
var params = {
"start_date": yesterday,
"end_date": today,
}
var response = service.fetch(url + "?" + encodeParams(params));
var result = JSON.parse(response.getContentText());
var rakutenPayData = result.money.filter(function(item) {
return item.from_account_id === RAKUTEN_PAYMENT_ID;
});
return rakutenPayData
}
// 楽天ペイ情報をZaimに登録(メイン関数)
function rakutenPayToZaim() {
var start = 0;
var max = 2;
var query = 'subject: ("楽天ペイアプリご利用内容確認メール" OR "楽天ペイ 注文受付") ';
var threads = GmailApp.search(query, start, max);
var service = getService();
if (service.hasAccess()) {
var existingData = getPastData(service)
} else {
var authorizationUrl = service.authorize();
Logger.log("次のURLを開いてZaimで認証したあと、再度スクリプトを実行してください。: %s",
authorizationUrl);
}
for (var i = 0; i < threads.length; i++) {
var messages = threads[i].getMessages();
for (var j = 0; j < messages.length; j++) {
var message = messages[j];
var subject = message.getSubject();
var body = message.getPlainBody();
if (subject == "楽天ペイアプリご利用内容確認メール") {
var usageDate = body.match(/ご利用日時\s*([\s\S]*?)伝票/)[1].trim();
var cash = body.match(/楽天キャッシュ\s*([\s\S]*?)\s*円/)[1].trim().replace(/,/g, "");
var card = body.match(/カード\s*([\s\S]*?)\s*円/)[1].trim().replace(/,/g, "");
var shop = body.match(/ご利用店舗\s*([\s\S]*?)電話/)[1].trim();
cash = parseInt(cash, 10)
card = parseInt(card, 10)
cash += card
usageDate = usageDate.replace(/^(\d{4})\/(\d{2})\/(\d{2}).*/, "$1-$2-$3")
} else if (subject == "楽天ペイ 注文受付(自動配信メール)") {
var usageDate = body.match(/\d{4}-\d{2}-\d{2}/g)[0]
var cash = body.match(/[\d,]+円/g).at(-1).replace(/,/g, "").replace(/円/g, "");
var shop = body.match(/提携サイト「(.*?)>(.*?)>(.*?)<(.*?)」/)[3];
} else {
continue;
}
var isExisting = false;
Logger.log("日付:" + usageDate + ", 支払金額:" + cash + ", お店:" + shop)
if (cash == 0) {
Logger.log("ポイント払いのためスキップ");
continue;
}
if (usageDate != yesterday & usageDate != today) continue;
for (var k = 0; k < existingData.length; k++) {
if (existingData[k]["date"] == usageDate && existingData[k]["amount"] == cash && existingData[k]["place"] == shop) {
isExisting = true;
Logger.log("既に入力済み")
break;
}
}
if (!isExisting) {
var files = DriveApp.getFilesByName("ZAIM_DB");
var originalData = []
if (files.hasNext()) {
spreadsheet = SpreadsheetApp.open(files.next());
var originalSheet = spreadsheet.getSheetByName("登録カテゴリー");
originalData = originalSheet.getRange(2, 1, originalSheet.getLastRow() - 1, 4).getValues();
}
var category_id = DEFAULT_CATEGORY_ID;
var genre_id = DEFAULT_GENRE_ID;
for (var k = 0; k < originalData.length; k++) {
var [storeName, exactMatch, categoryId, genreId] = originalData[k];
if ((exactMatch && storeName === shop) || (!exactMatch && shop.includes(storeName))) {
category_id = categoryId;
genre_id = genreId;
break;
}
}
var url = "https://api.zaim.net/v2/home/money/payment";
var payload = {
"category_id": category_id,
"genre_id": genre_id,
"amount": cash,
"date": usageDate,
"place": shop,
"from_account_id": RAKUTEN_PAYMENT_ID,
"comment": "システムから登録"
};
var options = {
"method": "post",
"payload": payload
};
service.fetch(url, options);
Logger.log("支払い入力完了")
}
}
}
}
// Spreadsheetに書き込み
function writeCategoriesToSpreadsheet(categories, genres, accounts) {
const SPREADSHEET_NAME = "ZAIM_DB";
const CATEGORY_SHEET_NAME = "カテゴリと内訳";
const ACCOUNT_SHEET_NAME = "支払方法";
const ORIGINAL_SHEET_NAME = "登録カテゴリー";
let spreadsheet;
var files = DriveApp.getFilesByName(SPREADSHEET_NAME);
if (files.hasNext()) {
spreadsheet = SpreadsheetApp.open(files.next());
} else {
spreadsheet = SpreadsheetApp.create(SPREADSHEET_NAME);
}
let categorySheet = spreadsheet.getSheetByName(CATEGORY_SHEET_NAME);
if (categorySheet) {
categorySheet.clear();
} else {
categorySheet = spreadsheet.insertSheet(CATEGORY_SHEET_NAME);
}
let accountSheet = spreadsheet.getSheetByName(ACCOUNT_SHEET_NAME);
if (accountSheet) {
accountSheet.clear();
} else {
accountSheet = spreadsheet.insertSheet(ACCOUNT_SHEET_NAME);
}
let originalSheet = spreadsheet.getSheetByName(ORIGINAL_SHEET_NAME);
if (!originalSheet) {
originalSheet = spreadsheet.insertSheet(ORIGINAL_SHEET_NAME);
var originalHeaders = ["店舗名", "完全一致", "カテゴリーID", "内訳ID"];
originalSheet.getRange(1, 1, 1, originalHeaders.length).setValues([originalHeaders]);
}
let defaultSheet = spreadsheet.getSheetByName("シート1");
if (defaultSheet) {
spreadsheet.deleteSheet(defaultSheet);
}
var categoryHeaders = ["カテゴリーID", "カテゴリー名", "内訳ID", "内訳名"];
categorySheet.appendRow(categoryHeaders);
var accountHeaders = ["支払ID", "支払方法"];
accountSheet.appendRow(accountHeaders);
var categoryRows = [];
categories.forEach(category => {
var categoryGenres = genres.filter(genre => genre.category_id === category.id);
if (categoryGenres.length > 0) {
categoryGenres.forEach(genre => {
categoryRows.push([category.id, category.name, genre.id, genre.name]);
});
} else {
categoryRows.push([category.id, category.name, "", ""]);
}
});
var accountRows = accounts.map(account => [account.id, account.name]);
categorySheet.getRange(2, 1, categoryRows.length, categoryHeaders.length).setValues(categoryRows);
accountSheet.getRange(2, 1, accountRows.length, accountHeaders.length).setValues(accountRows);
Logger.log("カテゴリ情報、内訳、支払い方法一覧を取得しました: " + spreadsheet.getUrl());
}
// カテゴリと内訳の取得(スプレッドシート作成のメイン関数)
function getInfo() {
var service = getService();
if (!service.hasAccess()) {
var authorizationUrl = service.authorize();
Logger.log("次のURLを開いてZaimで認証したあと、再度スクリプトを実行してください。: %s",
authorizationUrl);
}
var categoryUrl = "https://api.zaim.net/v2/home/category";
var response = service.fetch(categoryUrl)
var category = JSON.parse(response.getContentText()).categories;
var filterCategory = category.filter(item => item.mode === "payment" && item.active === 1.0)
.sort((a, b) => a.sort - b.sort);
var genreUrl = "https://api.zaim.net/v2/home/genre";
response = service.fetch(genreUrl)
var genre = JSON.parse(response.getContentText()).genres;
var filterGenre = genre.filter(item => item.active === 1.0)
.sort((a, b) => a.sort - b.sort);
var accountUrl = "https://api.zaim.net/v2/home/account";
var response = service.fetch(accountUrl);
var account = JSON.parse(response.getContentText()).accounts;
account = account.filter(item => item.active === 1.0)
.sort((a, b) => a.sort - b.sort);
writeCategoriesToSpreadsheet(filterCategory, filterGenre, account);
}4设置 Zaim API 应用
访问 Zaim Developers,用Zaim账号登录后,点击 Add a new application,按以下内容填写:
- Name:随意
- Service Type:选 Browser
- Description:随意
- Organization:填自己的ID即可
- ServiceURL:
https://script.google.com/ - 三个勾选项全部勾上
-12.png)
提交后会得到Consumer Key和Consumer Secret,记下来备用。
-11.png)
5设置 Script Property
点击左下方齿轮菜单,找到 Script Property。
-10.png)
添加以下两行:
ZAIM_CONSUMER_ID: [Consumer Key]
ZAIM_CONSUMER_SECRET: [Consumer Secret]-9.png)
6添加 OAuth1 库
点击编辑区左下方的红色加号,输入以下库ID追加OAuth1:
1CXDCY5sqT9ph64fFwSzVtXnbjpSfWdRymafDrtIZ7Z_hwysTY7IIhi7s7生成数据库
将函数选择从debug改为 getInfo,点击执行。第一次运行时会要求账户授权,授权完成后如果执行失败,再执行一次即可。
-7.png)
成功后会在Google Drive里生成名为 ZAIM_DB 的电子表格。
-6.png)
8设置乐天Pay账户ID
打开 ZAIM_DB 里的「支払方法」Sheet,找到对应乐天Pay那一行的ID(8位数字),在Script Property里添加:
RAKUTEN_PAYMENT_ID: [8位数字ID]9设置默认Category和Genre
在 ZAIM_DB 的「カテゴリと内訳」Sheet里找到想设为默认分类的ID,添加到Script Property:
DEFAULT_CATEGORY_ID: [カテゴリーID]
DEFAULT_GENRE_ID: [内訳ID]10设置常用商户的分类
在 ZAIM_DB 的「登録カテゴリー」Sheet里可以预先设置常用商户对应的分类,脚本会优先匹配这张表。比如设置”吉野家”对应餐饮类,之后每次在吉野家消费就会自动归入餐饮,不需要手动改。
-5.png)
11运行测试
将执行函数改为 rakutenPayToZaim,点击执行,出现类似下图的日志输出即表示运行正常。
-4.png)
-3.png)
12设置定期自动运行
点击左侧菜单中的「トリガー」(触发器),根据个人习惯设置运行频率。可以选择每天、每小时等多种间隔。
-2.png)
-1.png)
最终实现的功能
- 按设定的时间间隔,自动读取乐天Pay发来的支付通知邮件
- 支持两种邮件格式:实体店铺支付 / 在线支付
- 对照过去2天内Zaim的记录,已存在的条目不重复添加
- 支付日期取自邮件内容,记账日期为实际消费日
- 出金账户对应Zaim中手动设置的乐天Pay(名称可以不同,通过ID匹配)
- 店铺名称来自邮件:实体店铺记录店铺名,在线支付记录利用网站名
- Category和Genre可自行设定,常用商户可预先在数据库里配置自动分类
- 备注栏默认写入”システムから登録”,可在代码里自行修改
- 纯积分支付的记录会自动跳过(金额为0时不记账)
.png)
设置完成后,每次用乐天Pay消费都会在邮件发出后的下一个触发周期内自动写入Zaim,不需要再手动记账。对于把乐天Pay作为日常主力支付工具的人来说,这一套流程跑起来之后,记账这件事基本可以忘掉了。
-16-1024x1024.png)
.png)

