店舗の予約を自社のカレンダーに表示する
このクイックスタートでは、レシートローラーに入った店舗の予約を API で取得し、Google カレンダーや Outlook に取り込める iCalendar ファイル(.ics)に書き出す Node.js スクリプトを作ります。今日から7日分の予約を読み、開始・終了時刻、お客様の名前、担当の席やスタッフをカレンダーの予定にします。予約の作成や変更は行わない、読み取り専用の連携です。
始める前に
- デベロッパーアプリを登録し、OAuth でアクセストークンを取得できること。手順は 利用開始までの流れ と はじめてのAPI呼び出し:アプリ登録から取引一覧の取得まで をご覧ください。
- アプリの API スコープで
store.reservations.readにチェックを入れ、認可 URL のscopeに含めること。このスコープだけで、ここで使う予約一覧と予約枠の取得がすべてできます。 - 連携先のビジネスアカウントが、デベロッパーAPI を含むプラン(スターター以上)であること。ベーシックや試用中のビジネスアカウントでは、トークン交換が
401invalid_grantで拒否されます。 - テスト用の環境(サンドボックス)はまだありません。実際のビジネスアカウントに、予約管理を設定した店舗と、確認用の予約を用意してください。
- Node.js 20 以上。追加のパッケージは不要です。
1. 店舗IDを確認する
予約 API はすべて店舗単位で、storeId が必須です。どう調べるかは、アプリの「認可時に1つのビジネスアカウントへトークンを紐付ける」設定で変わります。
紐付けがオン(既定)の場合
トークンは認可されたビジネスアカウントそのものとして動き、そのビジネスアカウントのすべての店舗の予約を読めます。トークンにユーザーが含まれないため、/api/v1/me 系のエンドポイントは 403 no_user_context を返し、店舗一覧は API から取れません。店舗IDは管理画面で確認します。予約管理画面の URL の /stores/ の次の部分が店舗IDです。
https://receiptroller.io/ja-JP/organizations/{organizationId}/stores/{storeId}/reservations
この場合、以降の呼び出しに organizationId は不要です。
紐付けがオフの場合(ユーザーとして動くトークン)
トークンはサインインしたユーザーとして動きます。まず所属するビジネスアカウントを取得します。
curl -s "https://receiptroller.io/api/v1/me/organizations" \
-H "Authorization: Bearer {access_token}"
{
"organizations": [
{ "id": "{organizationId}", "name": "サンプル整体院", "role": "OperationsManager", "roles": ["OperationsManager"] }
],
"page": 1,
"pageSize": 100,
"totalCount": 1,
"totalPages": 1
}
次に、そのビジネスアカウントでこのユーザーが何をできるかを店舗ごとに取得します。capabilities に reservations.read がある店舗が、予約を読める店舗です。
curl -s "https://receiptroller.io/api/v1/me/permissions?organizationId={organizationId}" \
-H "Authorization: Bearer {access_token}"
{
"organizationId": "{organizationId}",
"roles": [],
"capabilities": [],
"stores": [
{
"storeId": "{storeId}",
"name": "渋谷店",
"storeRoles": ["StoreStaff"],
"capabilities": ["reservations.read"]
}
]
}
店舗の住所なども必要なら GET /api/v1/me/organizations/{organizationId}/stores で一覧を取れます。こちらはビジネスアカウントの全店舗を返すので、予約を読めるかどうかは上の /me/permissions で判断してください。ユーザーとして動くトークンでは、予約 API の呼び出しすべてに ?organizationId={organizationId} を付けます。
店舗での役割と予約の権限
ユーザーとして動くトークンでは、そのユーザーの役割で読める範囲が決まります。
| 役割 | 予約の閲覧 | 予約の作成・変更、予約枠の運用 |
|---|---|---|
店舗のスタッフ(StoreStaff) | その店舗のみ可 | 不可 |
店舗のシフトリーダー(ShiftLead)・店長(StoreManager) | その店舗のみ可 | その店舗のみ可 |
ビジネスアカウントの OperationsManager・SuperUser | 全店舗で可 | 全店舗で可 |
紐付けがオンのトークンには役割の判定がなく、ビジネスアカウントの全店舗を扱えます(書き込みには store.reservations.write が別に必要です)。
2. 予約の一覧を取得する
GET /api/v1/reservations に店舗IDと期間を渡します。
| パラメータ | 内容 |
|---|---|
storeId | 必須。店舗ID |
from / to | 予約日の範囲(yyyy-MM-dd)。両端の日を含みます。省略すると期間で絞り込みません |
status | 1つのステータスだけに絞り込みます(省略時はすべて) |
organizationId | ユーザーとして動くトークンのときに指定 |
curl -s "https://receiptroller.io/api/v1/reservations?storeId={storeId}&from=2026-11-02&to=2026-11-08" \
-H "Authorization: Bearer {access_token}"
{
"count": 1,
"reservations": [
{
"id": "{reservationId}",
"storeId": "{storeId}",
"slotId": "{slotId}",
"reservationDate": "2026-11-02",
"startTime": "10:00",
"endTime": "11:00",
"durationMinutes": 60,
"status": "Confirmed",
"customer": { "name": "山田 花子", "phone": "", "email": "" },
"partySize": 1,
"assignedResource": "個室A",
"notes": { "customer": "", "staff": "" },
"bookingSource": "Web",
"createdAt": "2026-10-28T03:15:42.1234567Z",
"updatedAt": "2026-10-28T03:15:42.1234567Z"
}
]
}
結果は予約日・開始時刻の早い順に並びます。ページングはないため、期間は必要な分だけに絞ってください。
カレンダーに使う項目
| 項目 | 内容 |
|---|---|
id | 予約ID。カレンダーの予定IDに使うと、再取り込み時に同じ予定が更新されます |
reservationDate・startTime・endTime | 予約日(yyyy-MM-dd)と開始・終了時刻(HH:mm)。店舗の現地時刻です(次の節を参照) |
status | Pending(承認待ち)、Confirmed(確定)、Arrived(来店済み・対応中)、Completed(完了)、Cancelled(キャンセル)、NoShow(無断キャンセル) |
customer.name | お客様の名前 |
partySize | 人数 |
assignedResource | 割り当てられた席・部屋・スタッフの名前(IDではありません)。空のこともあります |
updatedAt | 最終更新日時(UTC) |
customer.phone・customer.email・notes も返りますが、カレンダーは店舗の外の人にも共有されがちです。必要な項目だけを書き出してください。
3. 時刻の扱い
レシートローラーは日時を UTC で保存しますが、予約の reservationDate・startTime・endTime は例外で、店舗の現地時刻をオフセットなしで返します。レシートローラーは予約時刻を日本時間(UTC+9)として扱っています。from・to もこの現地日付で指定します。
- カレンダーに UTC で書き出すときは、予約日と時刻を日本時間とみなして9時間を引きます(例:
2026-11-02の10:00は2026-11-02T01:00:00Z)。 - 「今日」は店舗の現地日付で計算します。UTC の日付を使うと、日本時間の0時〜9時のあいだは前日になります。
createdAt・updatedAtは UTC です。
4. 空いている予約枠を取得する(任意)
カレンダーに空き状況も載せたい場合は GET /api/v1/reservations/slots を使います。from・to を省略すると7日分を返しますが、起点が UTC の日付になるため、明示的に指定してください。
curl -s "https://receiptroller.io/api/v1/reservations/slots?storeId={storeId}&from=2026-11-02&to=2026-11-08" \
-H "Authorization: Bearer {access_token}"
{
"count": 1,
"slots": [
{
"id": "{slotId}",
"date": "2026-11-02",
"startTime": "11:00",
"endTime": "12:00",
"durationMinutes": 60,
"capacity": 2,
"capacityUnit": "Bookings",
"bookedCount": 1,
"remainingCapacity": 1,
"isAvailable": true,
"assignedResource": "個室A"
}
]
}
空きは remainingCapacity と isAvailable で判断します。時刻の扱いは予約と同じく現地時刻です。
5. 7日分の予約を .ics に書き出す
次のスクリプトを reservations-to-ics.mjs として保存します。キャンセルと無断キャンセルは除き、承認待ちは仮の予定(TENTATIVE)として書き出します。
// reservations-to-ics.mjs — 今日から7日分の予約を reservations.ics に書き出します
import { writeFile } from "node:fs/promises";
const API = "https://receiptroller.io/api/v1";
const token = process.env.RR_ACCESS_TOKEN;
const storeId = process.env.RR_STORE_ID;
const organizationId = process.env.RR_ORGANIZATION_ID; // ユーザーとして動くトークンのときだけ
const STORE_UTC_OFFSET_HOURS = 9; // 予約時刻は日本時間
if (!token || !storeId) {
console.error("RR_ACCESS_TOKEN と RR_STORE_ID を設定してください");
process.exit(1);
}
async function get(path, params = {}) {
const url = new URL(API + path);
const query = { storeId, organizationId, ...params };
for (const [key, value] of Object.entries(query)) {
if (value) url.searchParams.set(key, value);
}
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) {
throw new Error(`${res.status} ${path} ${await res.text()}`);
}
return res.json();
}
function storeToday() {
const storeNow = new Date(Date.now() + STORE_UTC_OFFSET_HOURS * 3600_000);
return storeNow.toISOString().slice(0, 10);
}
function addDays(date, days) {
const d = new Date(`${date}T00:00:00Z`);
d.setUTCDate(d.getUTCDate() + days);
return d.toISOString().slice(0, 10);
}
// 店舗の現地日付 + "HH:mm" を UTC の Date にする
function storeTimeToUtc(date, time) {
const [year, month, day] = date.split("-").map(Number);
const [hour, minute] = time.split(":").map(Number);
return new Date(Date.UTC(year, month - 1, day, hour - STORE_UTC_OFFSET_HOURS, minute));
}
// 20261102T010000Z の形
function icsUtc(date) {
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
}
function icsText(value) {
return String(value ?? "")
.replace(/\\/g, "\\\\")
.replace(/;/g, "\\;")
.replace(/,/g, "\\,")
.replace(/\r?\n/g, "\\n");
}
// RFC 5545: 75オクテットを超える行は、空白で始まる次の行に折り返す
function fold(line) {
const parts = [];
let current = "";
let octets = 0;
for (const ch of line) {
const size = Buffer.byteLength(ch);
if (octets + size > 75) {
parts.push(current);
current = " ";
octets = 1;
}
current += ch;
octets += size;
}
parts.push(current);
return parts.join("\r\n");
}
const from = storeToday();
const to = addDays(from, 6);
const { reservations } = await get("/reservations", { from, to });
const stamp = icsUtc(new Date());
const lines = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//example.com//reservations-to-ics//JA",
"CALSCALE:GREGORIAN",
];
for (const r of reservations) {
if (r.status === "Cancelled" || r.status === "NoShow") continue;
const start = storeTimeToUtc(r.reservationDate, r.startTime);
let end = storeTimeToUtc(r.reservationDate, r.endTime);
if (end <= start) {
end = new Date(start.getTime() + (r.durationMinutes || 60) * 60_000);
}
const party = r.partySize > 0 ? `(${r.partySize}名)` : "";
const resource = r.assignedResource;
lines.push(
"BEGIN:VEVENT",
`UID:${r.id}@example.com`, // example.com は自社のドメインに置き換えてください
`DTSTAMP:${stamp}`,
`DTSTART:${icsUtc(start)}`,
`DTEND:${icsUtc(end)}`,
`SUMMARY:${icsText(`${r.customer.name}${party}`)}`,
`STATUS:${r.status === "Pending" ? "TENTATIVE" : "CONFIRMED"}`
);
if (resource) lines.push(`LOCATION:${icsText(resource)}`);
lines.push("END:VEVENT");
}
lines.push("END:VCALENDAR");
await writeFile("reservations.ics", lines.map(fold).join("\r\n") + "\r\n");
console.log(`${from} から ${to} までの予約を reservations.ics に書き出しました`);
実行します。
RR_ACCESS_TOKEN={access_token} RR_STORE_ID={storeId} node reservations-to-ics.mjs
ユーザーとして動くトークンでは RR_ORGANIZATION_ID={organizationId} も付けます。できあがった reservations.ics の中身は次のようになります。
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//example.com//reservations-to-ics//JA
CALSCALE:GREGORIAN
BEGIN:VEVENT
UID:{reservationId}@example.com
DTSTAMP:20261101T230000Z
DTSTART:20261102T010000Z
DTEND:20261102T020000Z
SUMMARY:山田 花子(1名)
STATUS:CONFIRMED
LOCATION:個室A
END:VEVENT
END:VCALENDAR
カレンダーアプリでこのファイルを読み込むと、日本時間の10:00〜11:00に予定が入ります。UID は予約IDから作るので、同じファイルを作り直しても予定は重複せず更新されます。
6. 定期的に更新する
予約の作成・変更を知らせる Webhook イベントはありません。カレンダーを最新に保つには、スクリプトを定期実行してファイルを作り直します。
- 間隔は15分〜1時間程度で十分です。1回の実行で呼ぶ API は予約一覧の1回だけです。
- 「この日時以降に更新された予約」で絞り込むパラメータはありません。毎回7日分をまとめて取得し、差分が必要なら
idとupdatedAtを前回の結果と比べてください。 - 書き出したファイルを Web サーバーに置き、カレンダーアプリの「URL で追加」で購読すると、キャンセルされた予約もファイルから消えた時点でカレンダーから消えます。一度だけ取り込む方法では、取り込み後の変更は反映されません。ファイルにはお客様の名前が含まれるため、推測されにくい URL にして公開範囲を限ってください。
- アクセストークンの有効期限は8時間です。定期実行ではリフレッシュトークンで更新する処理を入れてください(はじめてのAPI呼び出し:アプリ登録から取引一覧の取得まで)。
よくあるエラー
| 応答 | 原因と対処 |
|---|---|
トークン交換で 401 invalid_grant | ビジネスアカウントのプランにデベロッパーAPI が含まれていません。スターター以上のプランが必要です。 |
401(本文なし) | トークンが無効か期限切れです。リフレッシュトークンで取り直してください。 |
403(本文なし) | トークンに store.reservations.read がない、ユーザーとして動くトークンで organizationId を付けていない、またはそのユーザーにこの店舗での役割がない、のいずれかです。紐付けがオンのトークンで別のビジネスアカウントの organizationId を指定した場合も 403 になります。 |
403 no_user_context | 紐付けがオンのトークンで /api/v1/me 系を呼んでいます。店舗IDは管理画面の URL で確認してください。 |
403 not_a_member | ユーザーがそのビジネスアカウントのメンバーではありません。承認前の招待は含まれません。 |
400 {"error": "storeId is required"} | storeId を付けてください。 |
200 で "count": 0 | 期間内に予約がないか、店舗IDが違います。存在しない店舗IDでもエラーにはならず、空の一覧が返ります。 |
API の 401・403 には本文がありません。原因の切り分けは 403/401が返る(権限・スコープ) も参考にしてください。
次のステップ
-
はじめてのAPI呼び出し:アプリ登録から取引一覧の取得までOAuth認可コードフロー(PKCE)でトークンを取得し、ビジネスアカウントを確認して取引一覧を取得、トークンを更新するまでを通して行います。
-
MCPでAIエージェントをレシートローラーに接続するClaudeをMCPコネクタとして接続する手順と、動的クライアント登録・PKCE・JSON-RPCで自作のMCPクライアントから呼び出す方法を解説します。
-
Webhookで新しい売上を受け取り、Slackに通知するreceipt.created Webhookを登録し、Node.jsで署名検証と重複除去をしてSlackに売上を投稿するまでを手順で説明します。
-
商品マスタを自社システムと同期する手元のCSV・JSONを読み、SKUで照合して商品を作成・差分更新するNode.jsスクリプトを、商品APIで作ります。