space ocr
ガイド記事料金ドキュメント
developer

OCR APIとWebhookで買掛金管理の自動化パイプラインを構築する

信頼性の高い買掛金管理の自動化パイプラインを構築する方法を解説します。非同期OCR APIで請求書をアップロードし、セキュアなWebhook経由で構造化データを受け取るフローを学びましょう。

7 分で読了· 2026-08-31

請求書の処理は、多くの企業でボトルネックとなりがちな業務です。手作業によるデータ入力は時間がかかり、ミスも起こりやすいものです。OCR APIを導入しても、処理完了をポーリング(定期的な問い合わせ)で確認するシステムは非効率的です。システムの複雑さが増し、遅延や不要なトラフィックの原因にもなります。「処理はまだ終わらないか?」と常に確認し続けるような運用は、本当の意味での自動化とは言えません。理想的な買掛金管理ワークフローは、データが準備できた瞬間に即座に応答する、イベント駆動型であるべきです。

買掛金管理の自動化フローを流れる請求書
請求書をインプットし、構造化データとWebhookをアウトプット。

これこそが、space-ocrが採用する非同期処理の基本思想です。リクエストを送信して接続を維持し続ける代わりに、請求書を一括でアップロードすれば、すぐに他の作業に戻ることができます。APIは、ファイルが正常に受信されたことを示すジョブIDのリストを即座に返します。その後、当社のエンジンが各画像を処理します。請求書の読み取りが成功すると、アプリケーションのエンドポイントに直接通知が送信されます。この通知はWebhook、つまり構造化データを含んだシンプルなHTTP POSTリクエストによって行われます。

フローは非常にシンプルです。まず、請求書の画像を POST /upload に一度だけリクエストします。形式は multipart/form-data で、1リクエストあたり files は最大20件、1ファイル20MB、リクエスト全体で28MBまでです。超過分には 413 が返ります。既定は非同期のため、レスポンスにはファイルごとの jobs[] が入り、各要素が jobId と status: "pending" を持ちます。その後、あなたのサーバーはWebhook経由で ocr.completed イベントを受信します。イベントの外側はすべて共通の envelope(event / deliveryId / occurredAt / apiVersion / data)で、data.result には GET /jobs/{jobId} と同じ v2 構造 { values, cells, review, image } が入ります。values が請求書のデータそのもので、「弥生サンブル」のような取引先名から各明細項目までを含みます。cells[path] にはその値のページ上の box と quad、review.flagged には人が確認すべきパスの一覧が入ります。また、信頼性を高めるために、すべてのアップロードに Idempotency-Key ヘッダーを含めることができます。ネットワークエラーでリクエストを再試行する必要がある場合でも、同じキーを送信すれば、処理ジョブが重複して作成されることはありません。

フィールドの定義は、請求書のアップロード先となるシート側で一度だけ宣言します(POST /create の columns)。宣言はモデルには渡らないため、抽出精度が上がるわけではありません。増えるのは、確認対象を示すフラグと決定論的なパース結果です。

  • invoice_no — required と pattern。番号が無ければ missing、書式が崩れていれば pattern_mismatch が立ちます。
  • invoice_date — type: "date"。解釈済みの値が data.normalized に入り、印字された日付を日付として読めない場合は type_mismatch になります。
  • total — type: "number" と min: 0。負数や、数値として読めない金額は out_of_range / type_mismatch として表面化します。
  • supplier — 支払先が固定なら enum、そうでなければ near / not_near で名称の近くに印字されるべき(あるいは印字されてはならない)語を指定し、near_mismatch / near_conflict として現れます。

宣言によって values が書き換わることはありません。解釈済みの値は data.normalized に入り、理由は review.flagged[].reasons に配列で並びます。順位付きの配列で、先頭が代表の理由です。

webhook-receiver.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
const express = require('express');
const crypto = require('crypto');

const app = express();
const webhookSecret = process.env.SPACE_OCR_WEBHOOK_SECRET;

// The signature covers the raw bytes, so keep a copy before the JSON parser runs.
// Raise the parser limit too: an ocr.completed payload carries a cell per field.
const rawBodySaver = (req, res, buf, encoding) => {
  if (buf && buf.length) {
    req.rawBody = buf.toString(encoding || 'utf8');
  }
};
app.use(express.json({ verify: rawBodySaver, limit: '5mb' }));

// X-Spaceocr-Signature: t=<unix_ms>,v1=<hex>
// Canonical string: `${t}.${rawBody}` — HMAC-SHA256 with your webhook secret.
function verifySignature(secret, header, rawBody) {
  const m = /^t=(\d+),v1=([a-f0-9]+)$/.exec(header || '');
  if (!m) return false;
  const [, t, v1] = m;

  // Replay guard: reject a timestamp that drifts more than 5 minutes.
  if (Math.abs(Date.now() - Number(t)) > 5 * 60 * 1000) return false;

  const expected = Buffer.from(
    crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'),
    'hex',
  );
  const received = Buffer.from(v1, 'hex');
  return (
    expected.length === received.length &&
    crypto.timingSafeEqual(expected, received)
  );
}

app.post('/webhook', (req, res) => {
  const signature = req.get('X-Spaceocr-Signature');
  if (!verifySignature(webhookSecret, signature, req.rawBody)) {
    return res.status(400).send('Invalid signature');
  }

  // A redelivery reuses the same id, so it doubles as the de-duplication key.
  const deliveryId = req.get('X-Spaceocr-Delivery');
  const event = req.body; // event / deliveryId / occurredAt / apiVersion / data

  switch (event.event) {
    case 'ocr.completed': {
      // result = { values, cells, review, image } — the same v2 shape as GET /jobs/{jobId}.
      const { path, mode, result } = event.data;
      const flagged = result.review?.flagged || [];
      console.log(`[${deliveryId}] ${path} (${mode})`, result.values);
      if (flagged.length) {
        // Each entry is { path, reasons }; cells[path] holds box / quad / evidence.
        console.warn(
          `${flagged.length} field(s) to review:`,
          flagged.map((f) => `${f.path} -> ${f.reasons[0]}`),
        );
      }
      // TODO: post result.values to your accounting system, and hold the
      //       flagged paths for a person before the invoice is approved.
      break;
    }
    case 'ocr.failed':
      // The scan is refunded automatically; keep the payload for the audit trail.
      console.error(`[${deliveryId}] OCR failed:`, event.data);
      break;
    case 'webhook.test':
      console.log(`[${deliveryId}] test delivery received`);
      break;
    default:
      console.log(`Unhandled event type: ${event.event}`);
  }

  // Anything other than 2xx is retried: 1m -> 5m -> 30m -> 2h, 5 attempts in total.
  // Keep this fast — hand slow work to a queue instead of holding the response open.
  res.status(200).send({ received: true });
});

app.listen(3000, () => console.log('Webhook receiver listening on port 3000'));
✓ Verified

space-ocrはどのようにしてページ上のデータを特定するのでしょうか?まず、基盤となる言語モデルが、抽出したテキストとその位置を示す可能性のあるトークンヒントを返します。次に、当社のエンジンが重要な検証ステップを実行します。抽出された値を、ページ上で実際に検出されたOCRシンボルと一文字ずつ照合するのです。このプロセスにより、0.0から1.0までのmatch_ratioスコアが生成されます。スコアが0.85以上であれば、信頼性の高い一致と判断できます。最終的な座標は、元の画像のピクセルサイズに依存しない、0から1000の範囲で正規化されたバウンディングボックスとして返されます。

このような自動化は、誰もが気軽に利用できるべきだと考えています。API経由で処理される画像は、1枚あたり ¥10(税込)です。すべてのアカウントには、毎月100スキャン分の無料枠が付与されます。さらに重要な点として、画像が読み取れずにOCRジョブが失敗した場合は、料金は一切かかりません。コストはデータ抽出の成功にのみ連動するため、リスクを抑えながら買掛金管理の自動化を始めることができます。

  1. 公開エンドポイントを用意する
    サーバーには公開URLが必要です。ローカルでの開発には、ngrokのようなサービスを利用してローカルサーバーをインターネットに公開します。
  2. Webhook URLを登録する
    space-ocr のダッシュボードの Webhook 設定で公開エンドポイントのURLを追加するか、`PUT /webhook` に `url` と `active: true` を送って登録します。署名用シークレットが平文で返るのは、新規発行時と `rotateSecret` での再発行時の一度だけなので、その場で安全に保管してください。
  3. 署名検証を実装する
    `X-Spaceocr-Signature` を `t=<unix_ms>,v1=<hex>` として解析し、`${t}.${rawBody}` に対する HMAC-SHA256 をシークレットで計算して `v1` と時間一定比較し、timestamp が5分以上ずれていれば拒否します。実装できたら `POST /webhook/test` でテスト配信を送り、経路全体を確認します。
  4. ocr.completedイベントを処理する
    有効なイベントが届いたら `data.result`(`{ values, cells, review, image }`)を読み取ります。`values` を会計システムに書き込み、`review.flagged` の各項目(`path` と順位付きの `reasons`)は担当者の確認に回します。`cells[path]` を使えば、その値がページのどこにあるかも合わせて提示できます。
  5. 請求書を非同期でアップロードする
    ファイルを添付して `POST /upload` リクエストを送信します(1リクエスト最大20件)。APIはOCRの完了を待たずに、ファイルごとの `jobId` を含む `jobs[]` を即座に返します。
  6. イベントの受信を通知し、監視する
    エンドポイントは、受信を確認するために速やかに2xxステータスコードを返す必要があります。2xx以外は 1分 → 5分 → 30分 → 2時間 のスケジュールで再試行されます。配信状況は `GET /webhooks/deliveries`(status での絞り込み、`attempts` と `responsePreview` の確認)またはダッシュボードで追跡でき、失敗した配信は `POST /webhooks/deliveries/{deliveryId}/redeliver` で再送できます。
Webhookのエンドポイントがダウンした場合はどうなりますか?
配信は指数関数的バックオフ(1分 → 5分 → 30分 → 2時間)で、合計5回まで再試行します。再試行の対象は 5xx サーバーエラー、408、429、タイムアウトで、それ以外の 4xx は即座に dead 扱いになります。配信ログは30日間保存され、`POST /webhooks/deliveries/{deliveryId}/redeliver` で手動再送もできます。deliveryId は同じものが再利用され、ステータスは pending に戻ります。
Webhookリクエストが本当にspace-ocrから送信されたものであることを確認するにはどうすればよいですか?
すべての配信に `X-Spaceocr-Signature` が付き、形式は `t=<unix_ms>,v1=<hex>` です。あわせて `X-Spaceocr-Timestamp` / `X-Spaceocr-Event` / `X-Spaceocr-Delivery` も届きます。検証は、canonical 文字列 `${t}.${rawBody}` に対してWebhookシークレットで HMAC-SHA256 を計算し、その hex を `v1` と時間一定比較(timing-safe)で突き合わせる形です。timestamp が5分以上ずれている場合は拒否してください。署名の対象は生のボディなので、JSONパースで整形される前に検証する必要があります。
1回のAPIコールで複数の請求書をアップロードできますか?
はい。`POST /upload` は multipart/form-data に対応し、1リクエストに `files` を最大20件まで含められます。上限は1ファイル20MB、リクエスト全体で28MBで、超えると 413 が返ります。レスポンスはファイルごとの `jobs[]` で、`jobId` は `GET /jobs/{jobId}` でそのまま照会できます。`wait=true` を付けると同期処理になり、画像1枚につき最大30秒待って `results[]` を返します。間に合わなかったものは `pending` のまま返ります。
失敗したOCRジョブに料金はかかりますか?
いいえ。OCRジョブが失敗した場合、`ocr.failed`イベントがトリガーされ、スキャン費用は自動的に返金されます。料金が発生するのは、抽出が成功した場合のみです。
ポーリングとWebhookの違いは何ですか?
ポーリングでは、処理完了を確認するために`GET /jobs/{jobId}`を繰り返し呼び出す必要があります。一方、Webhookはイベント駆動型です。当社のサーバーが、処理完了と同時に結果をあなたのエンドポイントにプッシュするため、より効率的でリアルタイムな更新が可能です。
アプリケーションから安全にアップロードを再試行するにはどうすればよいですか?
`/upload`エンドポイントは`Idempotency-Key`ヘッダーに対応しています。24時間以内に同じキーを送信した場合、重複したジョブを作成する代わりに、キャッシュされた元のレスポンスが返されます。これにより、二重処理を防ぐことができます。

買掛金管理ワークフローの自動化を始めましょう

APIキーを取得して、毎月100スキャンの無料枠で開発を始めましょう。

関連記事