Skip to Content
Human ReviewOutcome callback reference

Outcome callback reference

Use this reference to implement and verify the Human Review v1 callback contract. Store a verified outcome before responding, then let a separate worker continue the application.

Receipt and execution are separate acknowledgments. Neither can substitute for the other.

Start with the Core Human Review lifecycle guide for the full pause, review, delivery, response, and application-worker flow.

Use the paths below with live data only after the selected release and environment are approved.

Paths and application authentication

Operationv1 path
Receive a verified outcomePOST /v1/review-outcomes on the registered application callback
Report application executionPOST /v1/review-outcomes/{deliveryId}/acknowledgments to VeriProof
Cancel a pending review casePOST /v1/review-cases/{reviewCaseId}/cancellation to VeriProof

Calls from the application to VeriProof use a current application credential with the review-continuations.write scope. Configure the SDK client to resolve that credential for every control call, so a long pause does not reuse the credential captured when the review started. The SDK handles the portal-issued credential format and its application binding; do not construct authentication headers yourself.

Policy evaluation binds an application-key workflow to its authenticated application. A replacement key must belong to that same application and tenant. Delegated workflows also retain their original user, authentication method, and client binding. An application key cannot replace that delegated identity. Neither identity grants permission to submit a human review decision.

Signing contract

VeriProof signs every callback with its own ed25519 key, following the Standard Webhooks  profile. There is no shared secret to store: your receiver checks the signature with VeriProof’s public keys.

VeriProof signs these exact bytes:

{webhook-id}.{webhook-timestamp}.{raw request body}
HeaderMeaning
webhook-idThe delivery ID. It matches deliveryId in the body and stays the same on every retry.
webhook-timestampUnix time in seconds when this attempt was signed
webhook-signatureOne or more space-separated signatures, each v1a,{base64 ed25519 signature}
webhook-key-versionThe signing key IDs, for troubleshooting only. Never pick a key from it.

VeriProof publishes its public keys at {API base URL}/.well-known/veriproof-webhook-keys.json. The SDK loads them from the endpoint you already configure, keeps them for up to an hour, and checks again at most once a minute when a signature doesn’t match a known key. If your application can’t reach VeriProof, pin the same key set JSON in configuration instead. Callbacks accept only v1a signatures.

During key rotation a callback carries two v1a signatures. Accept the callback when any listed signature verifies with a trusted key.

Reject repeated webhook-id, webhook-timestamp, or webhook-signature headers. The timestamp must be within 300 seconds of your clock, and the callback body can be at most 65,536 bytes.

Required verification order

Enforce the body limit

Reject an oversized request before buffering or parsing it.

Read the raw body once

Keep the exact bytes. Do not parse, normalize, re-encode, or reconstruct the JSON before signature verification.

Validate the timestamp

Parse the Unix timestamp and reject requests more than 300 seconds from your clock.

Check the signature

Verify each v1a signature over the signed bytes with every trusted public key. Accept the request when one matches. If none match, reload the key set (at most once a minute) and try again before rejecting.

Parse and validate

After signature verification, parse JSON and confirm deliveryId equals the webhook-id header. Then validate the schema plus every tenant, application, environment, case, workflow, branch, continuation, checkpointGeneration, boundStateFingerprint, decision version, callbackRegistrationVersion, and expiry binding.

Store before responding

Atomically store the event and delivery identity before returning the durable receipt.

If the key set can’t be loaded, respond with HTTP 503 so VeriProof retries the delivery later.

Verify review event webhooks

Review event webhooks use the same headers. They’re signed with v1a by default, or with v1 (HMAC-SHA256) when the subscription uses a whsec_ secret. Each SDK includes a small webhook verifier that checks either kind, so you don’t need to write signature code. Pass it the raw body, the headers, and the key set, the secret, or both. De-duplicate accepted events by webhook-id.

Connection tests do not resume work

The portal sends a signed continuation.connection_test before callback activation. Verify its signature and timestamp before reading it, and confirm its deliveryId matches webhook-id. It must have isConnectionTest: true and canReleaseContinuation: false. Do not create a review receipt, store a directive, or run application work for this event.

Every SDK review runtime answers a valid connection test with HTTP 202 and this body, without writing a review outcome:

{ "schemaVersion": "1.0", "eventId": "test-...", "deliveryId": "test-delivery-...", "state": "connection_test_received", "canReleaseContinuation": false }
SDKCallback methodConnection-test result
PythonHumanReviewRuntime.receive(202, body)
TypeScriptHumanReviewRuntime.receivestatus: 202 with connectionTest
.NETHumanReviewRuntime.ReceiveAsyncStatusCode 202, IsConnectionTest, ConnectionTest
JavaHumanReviewRuntime.receive or ReviewCallbackReceiver.acceptstatusCode() 202, isConnectionTest(), connectionTest()
GoHumanReviewRuntime.ReceiveCallbackStatusCode 202, IsConnectionTest(), ConnectionTest
RustHumanReviewRuntime::receive_callbackHumanReviewReceiveResponse::ConnectionTest, status_code() 202

Go’s Receive and Rust’s receive accept review outcomes only, so use the callback methods above in your handler. Direct callers of Python’s verify_review_callback must explicitly set allow_connection_test=True and handle this event separately. Its default remains outcome-only verification.

Successful connection testing proves endpoint verification, not review delivery or application resumption.

Preserve the raw body in web frameworks

The SDK receiver must receive the bytes that arrived on the wire. Do not pass an object produced by a JSON body parser and do not serialize an object back to JSON for verification.

The customer application team owns and operates this listener. VeriProof delivers the signed outcome, and the SDK verifies it, but neither owns the application’s endpoint, checkpoint store, queue, worker, revision path, or protected side effect. The listener must stop after durable storage and worker signaling; it must not resume an agent or run a protected tool.

TypeScript with Express

Register a route-scoped raw parser before any JSON parser that can consume the same route:

import express from 'express' const app = express() app.post( '/v1/review-outcomes', express.raw({ type: 'application/json', limit: '64kb' }), async (request, response) => { if (!Buffer.isBuffer(request.body)) { return response.status(400).json({ code: 'raw_body_required' }) } const valuesByName = new Map<string, string[]>() for (let index = 0; index < request.rawHeaders.length; index += 2) { const name = request.rawHeaders[index].toLowerCase() const values = valuesByName.get(name) ?? [] values.push(request.rawHeaders[index + 1]) valuesByName.set(name, values) } const result = await receiver.receive( new Uint8Array(request.body), { getAll: name => valuesByName.get(name.toLowerCase()) ?? [], }, ) return response.status(result.status).json( result.receipt ?? { code: 'delivery_conflict' }, ) }, ) app.use(express.json())

Do not put express.json() before this callback route. If a shared middleware, gateway, or serverless adapter parses the body first, configure that layer to retain and pass the original bytes.

The runnable TypeScript example uses Node’s HTTP server directly at sdk/typescript/veriproof-sdk-core/examples/human-review-receiver.mjs.

Python with ASGI

Read the ASGI request body once and pass those bytes directly to the review runtime that await vp.reviews(...) returns:

async def receive_outcome(request: Request) -> JSONResponse: raw_body = await request.body() headers: dict[str, list[str]] = {} for name, value in request.scope["headers"]: headers.setdefault(name.decode("latin-1"), []).append(value.decode("latin-1")) try: status, receipt = await reviews.receive(raw_body, headers) except ReviewCallbackError as error: status = 503 if error.code == "key_set_unavailable" else 400 receipt = {"code": error.code} return JSONResponse(receipt, status_code=status)

Do not call request.json() first and do not verify a JSON value reconstructed by middleware. Preserve repeated security headers so the runtime can reject duplicates.

The runnable Python example applies the same raw-bytes pattern with the standard library HTTP server at sdk/python/veriproof-sdk/examples/human-review-webhook/app.py.

Durable receipt is not execution acknowledgment

For first acceptance, return HTTP 202 with a durable-review-outcome-receipt body:

{ "schemaVersion": "1.0", "receiptId": "receipt_01", "deliveryId": "delivery_01", "eventId": "event_01", "state": "durably_received", "durablyReceivedAt": "2026-07-25T12:00:00Z", "replayed": false }

An exact replay returns the prior result with HTTP 200 and replayed: true. A conflicting delivery returns HTTP 409. A durable receipt means only that the application safely stored the event. It does not mean the protected action resumed.

After the application applies the directive, send the execution acknowledgment to the outcome’s acknowledgmentUrl. The body records one of:

  • resumed
  • terminated
  • remained_paused
  • superseded
  • failed

The acknowledgment response records the same state, its server timestamp, and whether the request was an exact replay.

When VeriProof is still recording the receipt

VeriProof records your durable receipt when your callback response reaches it. If your application resumes quickly, its acknowledgment can arrive first. In that case VeriProof answers HTTP 409 with a Retry-After header (in seconds) and this body:

{ "error": "durable_receipt_pending", "code": "durable_receipt_pending", "message": "VeriProof is still recording the receipt for this delivery. Send the same acknowledgment again after the Retry-After interval." }

The X-Veriproof-Error-Code header carries the same code. Nothing was recorded. Send the identical acknowledgment, with the same idempotency key, after the Retry-After interval. The SDK acknowledgment clients do this for you within their retry limit; if the receipt still isn’t recorded, they keep the acknowledgment in your review store and send it on the next flush.

Every other 409 from this endpoint is final. Don’t retry it.

Outcome, directive, and resume-mode rules

OutcomeDirectiveResume modeApplication behavior
approvedmay_resumecontinue_originalAfter application-state and current VeriProof policy checks pass, resume once with the original protected payload.
response_submittedmay_resumeresume_with_responseGive the bounded typed response and permitted replacements to the exact paused branch, then resume once.
response_submittedmay_resumeroute_to_revisionEnter the application’s revision path. Do not execute the superseded protected payload.
response_submittedremain_pausednoneRecord the typed response and keep the checkpoint paused.
rejectedmust_not_resumenoneEnd the protected branch without releasing or executing the work.
follow_up_requiredremain_pausednoneKeep the checkpoint paused for a later decision version.
cancelledmust_not_resumenoneEnd or supersede the paused work according to application state.
expiredmust_not_resumenoneDo not resume from an expired review result.

response_submitted with continue_original is invalid. VeriProof and the SDK fail closed on an unsupported outcome, directive, and resume-mode combination. The callback may include application-visible typed answers, permitted replace operations, feedback, and their binding digests. It must not include reviewer identity, the private review note, raw evidence, checkpoint data, the protected payload, prompt history, secrets, or unrelated context.

Do not dispatch by session alone, latest pause, action type, or branch label. The exact continuation, generation, and fingerprint must match.

For either advancing response_submitted mode, recheck the saved application state and current VeriProof policy before work continues. A permitted replacement must use op: "replace", target a registered JSON Pointer path, match the registered value kind and classification, and match both the saved protected-payload digest and editable-scope digest. Reject root replacement, unregistered or sensitive paths, executable expressions, tool-name changes, and add, remove, move, or copy. Apply the accepted replacement only to the locally held payload.

Event order and replay

eventSequence and decisionVersion are monotonic for their bound case. A duplicate event ID is the same business event. A later decision version may supersede an earlier follow-up result, but it must not cause a second side effect. Reject a result after deliveryExpiresAt and reject an outcome tied to an old callback registration unless the registered replay policy explicitly allows it.

Cancel a pending case

Cancellation binds the tenant, application, review case, workflow instance, branch, continuation, checkpoint generation, expected case version, and idempotency key. Supported reasons are:

  • end_user_cancelled
  • parent_workflow_ended
  • safer_alternative_completed
  • checkpoint_unavailable
  • checkpoint_superseded

VeriProof can also cancel a pending case itself. You receive a cancelled outcome with one of these reasons, and the action stays stopped:

  • application_lifecycle: the application was retired, the tenant was offboarded, or access was removed.
  • policy_rule_withdrawn: every rule that asked for this review was turned off, set to observe or Shadow, no longer covers the application, or no longer escalates. If you retry the action, it is checked against current policy.

An exact idempotent replay returns the prior result. A conflicting request hash returns HTTP 409. Cancellation must not erase an already recorded decision.

Last updated on