Skip to main content
External providers (custody, payment, or any off-chain service a settlement template parks on) report the outcome of an action by POSTing to /v1/callbacks/{provider}. The callback advances the settlement’s state machine, so everything on this page - the signature, the acceptance window, what your callback may touch, and how your status value is read - is part of the contract, not an implementation detail.
This is the inbound direction: a provider calls KeyStone. For the outbound direction - KeyStone notifying your platform as settlements progress - see Webhooks. Both directions share one signature scheme, so if you have implemented webhook verification you already know how to sign a callback.

Request

{provider} is the provider name KeyStone registered for you when your signing secret was issued: lowercase letters, digits, underscore and hyphen. It selects the secret your signature is verified against.

Signing a callback

Each provider holds its own signing secret, issued by KeyStone out of band. Sign every request:
  • t is the current Unix timestamp in seconds.
  • v1 is HMAC_SHA256(secret, "{t}." + body) in lowercase hex, where body is the exact raw request bytes.
  • Send X-Callback-Signature: t=<t>,v1=<v1>.
Three rules keep signatures verifiable:
  • Sign the exact bytes you send. Serialize the body once, sign those bytes, send those bytes. Re-serializing between signing and sending changes whitespace or key order and the signature stops matching.
  • Stay inside the acceptance window. The timestamp must be within 300 seconds of KeyStone’s clock, in either direction - stale and future-dated signatures are both refused. If your clock drifts more than a few minutes, fix the clock; the window is not configurable per provider.
  • Sign at send time, retries included. A retry is a new request: sign it freshly rather than replaying an old header, or the retry dies with the window.
A missing header returns 401 MISSING_CALLBACK_SIGNATURE. Everything else that fails authentication - an unregistered provider name, a timestamp outside the window, a signature that does not verify - returns one uniform 401 INVALID_CALLBACK_SIGNATURE, deliberately without saying which.

Secret rotation

KeyStone rotates your secret by activating the new one alongside the old: during the rotation window a signature under either secret verifies, so you switch your signer whenever suits you inside the window that was agreed when the rotation was scheduled. After the window the old secret is removed and signatures under it return 401.

What a callback may touch

A settlement accepts your callback only while it is parked on you: its current state must be one whose settlement template binds an action with resolution: "webhook" naming your provider. That binding is pinned when the settlement is created, so it never changes mid-flight. Concretely:
  • While a settlement sits in a state parked on your provider, your callback resolves it - success advances along the machine, failure routes to that state’s failure target.
  • A callback against a settlement in any other state - a state parked on a different provider, a state the engine or an operator resolves, or a state with no binding at all - is refused with 409 NO_VALID_TRANSITION. Retrying will not change it; the settlement is simply not waiting on you.
  • Compliance gates are closed to callbacks in both directions, whatever the status: a state with a compliance_check action bound is resolved by the engine’s own screening or through the compliance-decision endpoint. See Compliance.
This scoping is why callbacks are per-provider signed: your secret acts on exactly the settlements whose product flow parks on you, and only while parked there.

Accepted status values

status is matched after trimming surrounding whitespace and lowercasing, so FAILED, Failed and failed are the same value. Every failure word is read as a decided outcome, error included. If your provider retries an errored action, report the outcome once it is decided rather than reporting the transient error.
Anything KeyStone cannot read is never treated as a success. A status outside both vocabularies may well be a failure reported in a spelling this API does not carry, and advancing a settlement on that reading is the one outcome that cannot be undone. If your provider’s vocabulary is not listed above, tell us before you go live rather than discovering it as a parked settlement.
A parked settlement returns 200 with "state": "MANUAL_REVIEW": the callback was accepted and recorded, and retrying it returns the same 200 without recording anything further. Any URL embedded in a recorded value is reduced to its origin - scheme, host, port - before it is written, so error text pasted into status cannot publish a credentialed endpoint onto the settlement timeline. Resolution is an operator action, not a provider one.

Response

state is the settlement’s state after the callback, which is the target for a success, the failure target for a failure, and MANUAL_REVIEW for a status in neither vocabulary. One success is different: a success that moves a settlement out of COMPLIANCE_CLEARED on an on-chain settlement is handed to the settlement engine, which performs the on-chain setup and then advances. That response carries "state": "COMPLIANCE_CLEARED" - the current state, not the target - and the transition lands moments later. Do not assert that a success response’s state equals the target.

Retries and idempotency

Retrying a callback never applies a transition twice.
  • Re-sending the exact signed bytes of a delivery that was accepted, inside the acceptance window, returns 200 with the settlement’s current state and writes nothing. This is what happens when you lose our 200 and retry the identical request - the answer you get is the one you missed.
  • A settlement already in the state your callback produces returns 200 without re-processing. This covers the park: retrying the callback that parked a settlement returns 200 with "state": "MANUAL_REVIEW" again, and records nothing further.
  • A settlement in a terminal state (FINALIZED, REJECTED, ROLLED_BACK, TIMED_OUT) returns 200 for any callback. Terminal settlements are never rejected, because that would put a retrying provider into a loop.
  • A settlement in MANUAL_REVIEW accepts only a status that parks - one in neither vocabulary. A failure status against it is refused with 409: the park is operator-owned, and a failure word would route the settlement somewhere new rather than land where it already is.
  • A 4xx means stop retrying, with one exception: 409 SETTLEMENT_BUSY means the settlement is momentarily held or its on-chain setup is being recorded; retry it after a short delay, signed freshly. Every other refusal is deterministic and no number of re-signed retries will change it. A 401 on a request that verified locally usually means your clock drifted past the window or your secret was rotated out - re-sign and check with KeyStone, do not blind-retry.

When a callback is refused