---
title: "JP-Verify Invoice Facts — many invoices' facts in one call (JSON), optionally under one signed RFC 6962 Merkle root"
method: POST
path: "/v1/invoice/check/batch"
tags: ["Compliance"]
---

# JP-Verify Invoice Facts — many invoices' facts in one call (JSON), optionally under one signed RFC 6962 Merkle root

`POST /v1/invoice/check/batch`

## Request

- Base URL: `https://jp-verify.obolpay.xyz`
- URL: `https://jp-verify.obolpay.xyz/v1/invoice/check/batch`
- Auth: one of:
  - none
  - API key in header `X-API-Key`

Check MANY invoices from their facts — ``{"documents": [<InvoiceCheckRequest without
"sign">, …], "sign": false, "bundle": false}`` — and get one positional entry per
document: ``results[i]`` is ``{"index": i, "ok": true, "report": …}`` with exactly the
report ``POST /v1/invoice/check`` returns for that document (byte-identical for the same
facts — the same rules, sources, limitations and ``rule_set_version``), or ``{"index": i,
"ok": false, "error": …, "invalid": [...]}`` for a document that fails request-level
validation (the single route's free-400 reasons, free here too). ``summary`` counts
``pass`` / ``fail`` / ``unknown`` / ``not_applicable`` over the checked reports; ``count``
/ ``checked`` / ``invalid_count`` say how many were submitted, checked and refused. One
register snapshot serves the whole batch (one ``data_as_of``).

**NOT tax advice** — the same register facts, document checks and published schedule as the
single route, per document; no verdict, no deductibility determination, no buyer-side
input.

Ceilings, refused FREE before any document is parsed: the body over 4 MiB (413); more
documents than the caller's tier batch ceiling — 100 Sandbox/Starter, 500 Growth, 1,000
Reseller — (400 naming the count); ``documents`` missing or not a list (400); nothing
checkable (400 with the per-document reasons). Metered ``checked`` units, all-or-nothing:
``limiter.precheck`` refuses an exhausted caller before the work with a 429 that names the
shortfall (``N documents need N units; M remaining — nothing was charged``), and the
atomic consume after the work charges nothing when it denies. A stale or unreadable
register is a free 503 for the whole batch.

``"sign": true`` (Growth / Reseller): every checked entry carries ``signed_record`` — the
single route's Ed25519 envelope over ``{"invoice_check": report}`` with a compact ``jws``
sibling, so one document is verifiable on its own. ``"bundle": true`` (Growth / Reseller):
``bundle`` is an RFC 6962 §2.1 Merkle tree over the checked reports in ``results`` order —
``leaf = SHA-256(0x00 ‖ canonical JSON of {"invoice_check": report})`` (sorted keys, no
whitespace, UTF-8), interior nodes ``SHA-256(0x01 ‖ left ‖ right)``, the split at the
largest power of two below the size — with one §2.1.1 audit path per leaf
(``proofs[j]``: ``index`` = leaf position, ``document_index`` = the ``results`` index,
``tree_size``, ``path`` leaf-level sibling first) and a signed record over
``{"invoice_check_batch": {root, count, rule_set_version, data_as_of}}`` (also at the top
level as ``signed_record``); verify a document with 15 lines of stdlib: leaf =
sha256(0x00 ‖ canonical JSON of ``{"invoice_check": report}`` — sorted keys, no spaces),
node = sha256(0x01 ‖ left ‖ right) (RFC 6962 §2.1), fold the document's ``path`` from its
leaf up to ``root``, then verify the signed record over ``{root, count,
rule_set_version, data_as_of}`` against ``GET /pubkey`` — the worked recipe is in the
field reference INVOICE_CHECK.md §2c, available on request. Other tiers get ``null`` and
a note for either flag.

Opt-in ``Idempotency-Key``: a retry with the same key + body replays the first response
(``Idempotent-Replayed: true``) instead of being re-metered; this route caches bodies up
to 4 MiB (``JPVERIFY_IDEMPOTENCY_BATCH_MAX_CACHED_BYTES``) and a larger response is served
with ``X-JPVerify-Idempotency: not-cached`` — a retry would reprocess and re-meter, so
keep a batch to about 200 documents when replay protection matters. Names and amounts are
consumed in memory only — never stored, logged or echoed; the bundle hashes the reports,
not the documents.

## Headers

- `X-API-Key` string

## Request body

- object: ``POST /v1/invoice/check/batch`` body (1.4.0) — the facts of MANY invoices in one call, each element an ``InvoiceCheckRequest`` without its ``sign`` key (signing is decided once, at the top level). Positional: ``results[i]`` answers ``documents[i]``. Metered ``len(parsed)`` units — the documents that parsed — after the whole batch was checked; a document that fails request-level validation is a free ``ok: false`` entry, a batch in which nothing parses is a free 400, and a batch over the caller's tier ceiling is refused free before any element is read. Names and amounts are consumed in memory only and never stored, logged or echoed.
  - `documents` object[], required: 1..N invoices, each the facts of ONE document exactly as POST /v1/invoice/check takes them (a per-document `sign` key is ignored — see the top-level flag); N is capped by the caller's tier batch ceiling (100 Sandbox/Starter · 500 Growth · 1,000 Reseller) and the body by 4 MiB, both refused free
    - `document_type` string, nullable: qualified_invoice (適格請求書, default) | simplified_qualified_invoice (適格簡易請求書) | return_invoice (適格返還請求書 — 'credit note' in Peppol/UBL vocabulary; JP PINT models it as a negative Invoice (380)) — as claimed by the caller, never inferred from the sign of the amounts; industry eligibility is not checked
    - `supplier_t_number` string, nullable: the 登録番号 AS PRINTED (NFKC-folded, whitespace removed); absent = the document shows no number; a malformed value is a metered finding reported WITHOUT its text; longer than 32 characters (not number-shaped) or not UTF-8 encodable = free 400
    - `seller_name` string, nullable: issuer name as printed (<= 200 chars after NFKC/strip); compared with the register name, never echoed
    - `buyer_name` string, nullable: recipient name as printed; only its presence is used, never echoed
    - `buyer_name_present` boolean, nullable: alternative to buyer_name: whether the recipient's name is printed
    - `issue_date` string, required: REQUIRED — the date printed on the document ('YYYY-MM-DD', ibt-002)
    - `transaction_date` string, nullable: 課税資産の譲渡等を行った年月日 (goods: delivery; services: completion) — the evaluation date; mutually exclusive with transaction_period. On a return_invoice: the ORIGINAL supply (57条の4③二's second date), which is the register date of record (基通1-8-18/1-8-19)
    - `transaction_period` object, nullable: ``transaction_period`` for ``POST /v1/invoice/check`` (まとめ請求 — 一定の期間).
      - `start` string, required: 'YYYY-MM-DD' — evaluated as one of the two dates
      - `end` string, required: 'YYYY-MM-DD' (>= start) — evaluated as the other date
    - `return_date` string, nullable: return_invoice only — 売上げに係る対価の返還等を行う年月日 (57条の4③二's first date), 'YYYY-MM-DD'; optional (ITEM-02 reports unknown without it); informational for the register status (基通1-8-19); a free 400 on any other document_type
    - `preceding_invoice_reference_present` boolean, nullable: return_invoice — a reference to the original invoice is printed (presence only; the reference itself is never sent)
    - `currency` string, nullable: ISO 4217 (default JPY)
    - `description_present` boolean, nullable: 資産又は役務の内容 is stated (null = not supplied)
    - `reduced_rate_marked` boolean, nullable: 軽減対象である旨 is marked, in any form per 基通1-8-4 (null = not supplied)
    - `rate_only_stated` boolean, nullable: 適格簡易請求書 printing 適用税率 but no 消費税額等
    - `tax_lines` object[], nullable: 0..10 entries, one per rate/category on the document
      - `rate_percent` union: 適用税率 in percent (10, 8, or 0); null when the document prints none
        - integer
        - number
        - string
      - `category` string, nullable: standard | reduced | old_rate_8 | exempt | out_of_scope | export | unknown (8% alone does not say which 8% — supply reduced or old_rate_8)
      - `amount_basis` string, nullable: tax_exclusive (税抜) | tax_inclusive (税込) — which figure `amount` is
      - `amount` union: 税率ごとに区分した合計額 for this rate/category (signed)
        - integer
        - number
        - string
      - `tax_amount` union: 消費税額等 for this rate/category (yen, whole; signed)
        - integer
        - number
        - string
      - `tax_amount_jpy` union: the 消費税額等 in yen for this rate/category on a non-JPY document (基通1-8-16 / 問68: the statutory figure is the yen amount; a foreign-currency tax_amount alone is 参考) — ignored when currency is JPY
        - integer
        - number
        - string
    - `totals` object, nullable: ``totals`` for ``POST /v1/invoice/check`` — every key optional.
      - `tax_exclusive_total` union: 税抜合計
        - integer
        - number
        - string
      - `tax_total` union: 消費税額等の合計
        - integer
        - number
        - string
      - `tax_inclusive_total` union: 税込合計
        - integer
        - number
        - string
      - `payable_amount` union: 請求金額 (not reconciled — prepaid/rounding amounts legitimately differ)
        - integer
        - number
        - string
    - `sign` boolean, nullable: return an Ed25519-signed record (Growth/Reseller keys only; otherwise the response says so and signs nothing)
  - `sign` boolean, nullable: return `results[i].signed_record` — the single route's Ed25519 envelope (+ compact `jws`) over each checked document's report, so one document is verifiable without the batch (Growth/Reseller keys only; otherwise `signed_record: null` and a note)
  - `bundle` boolean, nullable: return `bundle`: an RFC 6962 §2.1 Merkle tree over the checked reports (leaf = SHA-256(0x00 ‖ canonical JSON of {"invoice_check": report})), one §2.1.1 audit path per checked document and a signed record over the root (Growth/Reseller keys only; otherwise `bundle: null` and a note)

## Response `200`

One positional entry per submitted document: `ok: true` with the single route's report (byte-identical for the same document), or `ok: false` with the request-level reason (free). `summary` counts rule statuses over the checked reports; `bundle` / `signed_record` are null unless requested on a Growth/Reseller key. Metered `checked` units, all-or-nothing. The example is the real response on the sample register (Growth key, `sign` + `bundle`) to the request example's first document plus a second document whose `issue_date` is malformed — refused free as `ok: false`.

- unknown

## Other responses

- `400`: Malformed input — refused before metering; nothing was charged.
- `401`: Unknown, inactive, or expired API key.
- `413`: Request body over the batch size cap — refused before it was buffered or parsed; nothing was charged.
- `422`: Validation Error
- `429`: Quota exceeded for the caller's tier.
- `503`: The register, or an optional feature, is unavailable or not configured on this deployment. A register 503 (since 1.4.1) also carries the mirror's `data_as_of` with the source fields — `source_latest_diff_date` (the newest 差分 the NTA 公表サイト listed when the sync last read it), `source_checked_at`, `source_gap_business_days` and `freshness.rule` (`stale` here) — so a caller can tell a mirror that is behind its source (`data_date` older than `source_latest_diff_date`) from a source that has not published (equal, but `source_checked_at` older than `freshness.source_check_max_hours`); `data_as_of` is null when the mirror is absent. Nothing was charged.

---

[API](https://skmtc.dev/obolpay/apis/jp-verify.md) · [All operations](https://skmtc.dev/obolpay/apis/jp-verify/llms.txt) · [OpenAPI document](https://skmtc.dev/obolpay/apis/jp-verify/revisions/bf8851690df0?raw)
