Ingest
The reporting path. A self-hosted SDK posts terminal gate outcomes here so hosted reputation, orders, receipts, and webhooks stay current. Idempotent so retries are safe.
The /v1/ingest/* routes are how a self-hosted SDK keeps the hosted control plane in
sync. After the SDK runs the gate locally, it fire-and-forget POSTs the outcome here; Veto
records orders/receipts, applies the reputation delta, and fans out webhooks. Every route is
idempotent on session.id, so SDK retries never double-count.
| Auth | API key · scope ingest:write (all three routes) |
|---|
Anchored vs. unanchored
Order/receipt rows are persisted only when the session anchors to a known merchant (by
mrch_ id). When the merchant is unknown, the call still succeeds and the reputation delta
still runs (reputation is global) — it just doesn't write order rows.
POST /v1/ingest/session
Upsert a session snapshot. Idempotent on session.id; persists only when the session anchors
to a known merchant, otherwise acknowledges without persisting.
Request
{ "session": { "id": "…", "merchantId": "mrch_01J…", "agentId": "…", "state": "awaiting_payment", "...": "a CheckoutSession" } }Response — 200
{ "ok": true, "session_id": "…", "persisted": true }| Status | Reason code | Meaning |
|---|---|---|
200 | — | Acknowledged (persisted says whether a row was written). |
400 | VALIDATION_FAILED | session.id missing. |
POST /v1/ingest/order — terminal accept
Report an accepted settle. In one transaction this writes the order + receipt rows (when
anchored), runs the reputation delta, and enqueues order.accepted + order.settled
webhook events.
Request
{
"session": { "id": "…", "state": "accepted", "agentId": "…", "merchantId": "mrch_01J…", "...": "a CheckoutSession" },
"receipt": "<compact JWS receipt>",
"settlement": { "reference": "0xabc…", "...": "a SettlementResult" },
"fulfillment": { "any": "json" }
}The session.state must be accepted.
Response — 201
{
"order_id": "ord_01J…",
"receipt_id": "rcpt_01J…",
"event_ids": ["evt_01J…", "evt_01J…"],
"reputation": { "agentId": "…", "score": 540, "tier": "trusted" }
}Idempotent replay
A second POST for the same session.id does not re-run anything — it returns the original
result with "replayed": true:
{ "order_id": "ord_01J…", "receipt_id": "rcpt_01J…", "event_ids": [], "replayed": true }{ "order_id": null, "receipt_id": null, "event_ids": [], "replayed": true, "reputation_score": 540 }Status codes
| Status | Reason code | Meaning |
|---|---|---|
201 | — | Recorded (or replayed). |
400 | VALIDATION_FAILED | Missing session or state !== "accepted". |
curl -X POST https://api.veto-ai.com/v1/ingest/order \
-H "Authorization: Bearer veto_test_8f2c…" \
-H "Content-Type: application/json" \
-d '{ "session": { "id": "sess_1", "state": "accepted", "agentId": "…", "merchantId": "mrch_01J…" }, "receipt": "<jws>" }'POST /v1/ingest/decision — terminal reject / hold
Report a non-accept outcome. Runs the reputation delta and fires order.rejected or
order.held depending on the verdict.
Request
{ "session": { "id": "…", "agentId": "…", "merchantId": "mrch_01J…", "verdict": { "decision": "hold", "...": "…" }, "...": "a CheckoutSession" } }The webhook type is order.held when verdict.decision === "hold", else order.rejected.
Response — 200
{
"ok": true,
"decision": "hold",
"event_ids": ["evt_01J…"],
"reputation": { "agentId": "…", "score": 480, "tier": "standard" }
}| Status | Reason code | Meaning |
|---|---|---|
200 | — | Recorded. |
400 | VALIDATION_FAILED | session missing. |
Why two accept events
order.accepted fires before irreversible capture; order.settled confirms the rail.
Subscribers should fulfill on order.settled. See Webhooks.
Manifest & JWKS serving
The public, agent-facing discovery surface served in hosted mode — the manifest, the receipt JWKS, and a path-form catalog mirror, with ETag caching and open CORS.
Reputation
Bidirectional agent and merchant reputation. The hot path never 404s — an unknown id returns a neutral 200 so the SDK degrades safely.