Daichodo

Change-detection webhooks

Register the numbers you care about and we POST to your endpoint the moment a change to one of them appears in the NTA's diff data.

Lookup is served free by the government, and it will tell you a number's history if you ask about that number. What it will not do is tell you unprompted — you would have to poll every number you care about, every day. That is what change detection replaces.

1. Register an endpoint

Add a URL under Change detection in the dashboard. A signing secret is shown once, at creation, and cannot be retrieved again.

The URL is constrained:

  • https only. Over http both the payload and the signature header are readable and strippable by anything on the path.
  • Loopback, private and link-local addresses are refused.

2. Watch some numbers

Add a registration number (T + 13 digits) or a corporate number (13 digits). Paste them with hyphens or full-width dashes if that is what you have — the server normalises to the form the register itself publishes.

Change detection is included from the Standard plan up.

3. Receive

{
  "id": 918273,
  "type": "registry.change",
  "registry": "invoice",
  "subject_key": "T8000000000001",
  "process": "02",
  "correct": null,
  "effective_on": "2026-08-11",
  "observed_at": "2026-08-12T02:14:33+00:00",
  "record": { "name": "株式会社サンプル", "...": "..." }
}

process is the NTA's own 事業者処理区分, passed through verbatim rather than mapped onto a vocabulary we invented.

CodeMeaning
01新規登録 — newly registered
02変更 — changed
03失効 — lapsed
04取消 — revoked

Codes appear in live data that are in none of the published documentation. When one does we still send it, with the raw value intact — a code we cannot interpret yet is still data nobody can re-fetch later.

4. Verify the signature

Every request carries a Daichodo-Signature header:

Daichodo-Signature: t=1786400000,v1=<hex hmac-sha256 of "{t}.{raw body}">

Deliberately the same construction Stripe uses. If you have implemented their verification, this ports with a changed header name.

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));
  // The timestamp is inside the signed material, so rewriting `t` to dodge this
  // window invalidates the signature it came with.
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;

  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');

  // timingSafeEqual throws on a length mismatch, so check that first.
  if (expected.length !== parts.v1.length) return false;
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(
        secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Verify against the raw request body. Parsing the JSON and re-serialising it changes the bytes, and the signature will not match.

Retries

ResponseWhat we do
2xxDelivered
408, 429Retried
Other 4xxNot retried
3xx, 5xx, connection failure, timeoutRetried

A 4xx that is not 408 or 429 is your endpoint saying "this request is wrong", and it will say the same thing next time. Retrying is load, not recovery.

Up to eight attempts, spaced out. Redirects are not followed.

Every attempt is visible under Recent deliveries in the dashboard. The record is written before anything is sent, so a notification that never arrived is still there — a log written only on success is empty in exactly the case you need it.

Make your handler idempotent

The same notification can arrive twice. id identifies the registry event, so dedupe on it.