店舗の予約を自社のカレンダーに表示する

クイックスタート API 予約 Reservations カレンダー iCalendar ics Node.js OAuth

このクイックスタートでは、レシートローラーに入った店舗の予約を API で取得し、Google カレンダーや Outlook に取り込める iCalendar ファイル(.ics)に書き出す Node.js スクリプトを作ります。今日から7日分の予約を読み、開始・終了時刻、お客様の名前、担当の席やスタッフをカレンダーの予定にします。予約の作成や変更は行わない、読み取り専用の連携です。

始める前に

  • デベロッパーアプリを登録し、OAuth でアクセストークンを取得できること。手順は 利用開始までの流れ と はじめてのAPI呼び出し:アプリ登録から取引一覧の取得まで をご覧ください。
  • アプリの API スコープで store.reservations.read にチェックを入れ、認可 URL の scope に含めること。このスコープだけで、ここで使う予約一覧と予約枠の取得がすべてできます。
  • 連携先のビジネスアカウントが、デベロッパーAPI を含むプラン(スターター以上)であること。ベーシックや試用中のビジネスアカウントでは、トークン交換が 401 invalid_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)。両端の日を含みます。省略すると期間で絞り込みません
status1つのステータスだけに絞り込みます(省略時はすべて)
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)。店舗の現地時刻です(次の節を参照)
statusPending(承認待ち)、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が返る(権限・スコープ) も参考にしてください。

次のステップ

公開日: 2026-10-26 更新日: 2026-10-26