/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:tis the current Unix timestamp in seconds.v1isHMAC_SHA256(secret, "{t}." + body)in lowercase hex, wherebodyis the exact raw request bytes.- Send
X-Callback-Signature: t=<t>,v1=<v1>.
- 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.
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 return401.
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 withresolution: "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_checkaction bound is resolved by the engine’s own screening or through the compliance-decision endpoint. See Compliance.
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.
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
200with the settlement’s current state and writes nothing. This is what happens when you lose our200and retry the identical request - the answer you get is the one you missed. - A settlement already in the state your callback produces returns
200without re-processing. This covers the park: retrying the callback that parked a settlement returns200with"state": "MANUAL_REVIEW"again, and records nothing further. - A settlement in a terminal state (
FINALIZED,REJECTED,ROLLED_BACK,TIMED_OUT) returns200for any callback. Terminal settlements are never rejected, because that would put a retrying provider into a loop. - A settlement in
MANUAL_REVIEWaccepts only a status that parks - one in neither vocabulary. A failure status against it is refused with409: the park is operator-owned, and a failure word would route the settlement somewhere new rather than land where it already is. - A
4xxmeans stop retrying, with one exception:409 SETTLEMENT_BUSYmeans 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. A401on 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.