After a sale – deliver what they bought
When an agent buys, Veto pings your app with a signed order.settled webhook and YOUR app delivers – ship it, send the file, grant access. Exactly like Stripe. Verify the Veto-Signature, then fulfill. Node, Python, and a curl to re-fetch the order.
When an agent buys, Veto pings your app, and your app delivers – exactly like Stripe. Veto tells you the money landed and hands you the order; you keep whatever fulfillment you already have.
That's the entire model. Veto is webhooks + an API: it does not store your files, hold licenses, ship your boxes, or grant your logins. It tells you a sale happened, here's what was bought and who bought it – and your app does the rest. If you've wired up a Stripe webhook before, you already know how this works.
The flow, in four steps
Tell Veto your app's URL, and copy your signing secret
In the dashboard, open Sale notifications, paste the URL Veto
should call when a sale happens (e.g. https://api.acme.example/webhooks/veto), and copy the
signing secret (whsec_…) it gives you. Keep that secret in your secret manager – it's how
you'll prove a delivery really came from Veto. It's shown once.
An agent buys → Veto POSTs your URL an order.settled event
The moment a checkout settles – the money is yours – Veto sends a signed POST to your URL.
The body tells you everything you need to fulfill: what was bought, the amount, the
receipt, and who and where to deliver (name + shipping address for physical goods).
Your app verifies the signature, then fulfills
First verify the delivery is really from Veto (one check against your whsec_… secret) –
so a stranger can't POST fake orders at your URL. Then fulfill: ship the box, email the
download or license, flip the "access granted" flag. The delivery is yours; the webhook is
just the trigger.
Optionally, call the REST API to look things up
If you want to double-check an order or reconcile later, hit the REST API with your
Authorization: Bearer veto_… key. Standard REST – nothing special to learn.
Physical and digital are the SAME webhook.
There is no per-product setup – no "delivery type" to configure, no file upload, no
license vault. Whether you're shipping a parcel or emailing a download link, Veto sends the
same order.settled event. The only difference is what your code does with it: ship a
box vs. send a link. That's it.
The order Veto sends you
It's a normal JSON POST. The envelope is a stable wrapper; the part you act on is
data.object – the order:
{
"id": "evt_01J…", // the event id – dedupe on this (same event can arrive twice)
"type": "order.settled", // "the money is yours" – fulfill on THIS type
"created": 1750000000, // when Veto sent it (unix seconds)
"livemode": true, // false in test mode, true for real money
"api_version": "v1",
"data": {
"object": {
"id": "ord_01J…", // the order id – your anchor for fulfillment
"session_id": "sess_…", // the checkout this came from
"merchant_id": "mrch_01J…", // which of your merchants was bought from
"agent_id": "11111111-1111-1111-1111-111111111111", // the buying agent
"total": { "currency": "USD", "subtotal": "39.00", "tax": "0.00", "total": "39.00" },
"items": [{ "sku": "slipper-cloud-9", "qty": 1 }], // WHAT to fulfill
"rail_name": "x402", // how it settled
"settlement_ref": "0x…txHash…", // proof of payment (on-chain tx hash for x402)
"receipt_id": "rcpt_01J…", // the signed receipt for this sale
"buyer": { // WHO and WHERE to deliver
"name": "Ada Lovelace",
"shipping_address": { // present for physical goods
"line1": "12 Mathematician's Way",
"city": "London",
"postal_code": "EC1A 1BB",
"country": "GB"
}
}
}
}
}In plain words: items is what to send, total is what they paid, buyer is who
and where to deliver (with a shipping_address for physical goods), and settlement_ref +
receipt_id are your proof of payment. The full field list and the other event types
(order.accepted, order.rejected, order.held) are in
Webhooks.
Verify, then fulfill
Here's the whole handler in three flavors. Node and Python receive the webhook, verify
the Veto-Signature, and then hit a // fulfill here stub – that stub is your code (ship /
email the file / grant access). curl shows how to re-fetch the order from the REST API if
you ever need to look one up.
The only Veto-specific line is verifyWebhook from @veto-protocol/checkout – it does the
HMAC check for you and never throws on bad input.
import { verifyWebhook } from '@veto-protocol/checkout';
export async function POST(req: Request) {
// 1. Read the RAW body – never JSON.parse → re-stringify before verifying.
const raw = await req.text();
const signature = req.headers.get('veto-signature'); // "t=…,v1=…"
// 2. Verify it really came from Veto (positional args; returns { ok, reason? }).
const result = verifyWebhook(raw, signature, process.env.VETO_WEBHOOK_SECRET!);
if (!result.ok) {
return new Response(`bad signature: ${result.reason}`, { status: 400 });
}
const event = JSON.parse(raw);
// 3. Fulfill on order.settled (the rail confirmed the money is yours).
if (event.type === 'order.settled') {
const order = event.data.object;
// ── fulfill here ────────────────────────────────────────────────
// PHYSICAL: createShipment({ to: { name: order.buyer.name,
// ...order.buyer.shipping_address },
// lines: order.items, reference: order.id });
// DIGITAL: const link = await mintDownloadUrl(order.items, order.receipt_id);
// await emailBuyer(order.buyer, link); // or grant account access
// ────────────────────────────────────────────────────────────────
}
// 4. Return 2xx fast (<10s) to ack. Anything else triggers a retry.
return new Response('ok', { status: 200 });
}No SDK needed – the signature is a standard HMAC-SHA256 of "<t>.<rawBody>", so any language
can verify it. This is exactly what verifyWebhook does under the hood.
import hmac, hashlib, time, json
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["VETO_WEBHOOK_SECRET"] # your whsec_… from Sale notifications
@app.post("/webhooks/veto")
def veto_webhook():
raw = request.get_data(as_text=True) # the RAW body – verify these exact bytes
header = request.headers.get("Veto-Signature", "") # "t=…,v1=…"
# Parse "t=<unix>,v1=<hex>" into t and the list of v1 signatures.
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t = parts.get("t")
got = [v for k, v in (p.split("=", 1) for p in header.split(",")) if k == "v1"]
if not t or not got:
return "missing signature", 400
# Reject replays: the timestamp must be within ±300s of now.
if abs(int(time.time()) - int(t)) > 300:
return "stale", 400
# Recompute HMAC-SHA256(secret, "t.rawBody") and constant-time compare.
expected = hmac.new(SECRET.encode(), f"{t}.{raw}".encode(), hashlib.sha256).hexdigest()
if not any(hmac.compare_digest(expected, v) for v in got):
return "bad signature", 400
event = json.loads(raw)
# Fulfill on order.settled (the money is yours).
if event["type"] == "order.settled":
order = event["data"]["object"]
# ── fulfill here ────────────────────────────────────────────
# PHYSICAL: ship_it(to=order["buyer"], lines=order["items"])
# DIGITAL: link = mint_download(order["items"], order["receipt_id"])
# email_buyer(order["buyer"], link) # or grant access
# ────────────────────────────────────────────────────────────
pass
return "ok", 200 # return 2xx fast (<10s) or Veto retriesYou don't need to call anything to fulfill – the webhook already carries the order. But if you want to look one up later (to reconcile, or if you missed a delivery), read the orders feed with your API key:
curl "https://api.veto-ai.com/v1/orders?merchant_id=mrch_01J…&limit=50" \
-H "Authorization: Bearer veto_test_8f2c…"{
"data": [
{
"id": "ord_01J…",
"merchant_id": "mrch_01J…",
"decision": "accept",
"total": { "currency": "USD", "subtotal": "39.00", "tax": "0.00", "total": "39.00" },
"rail_name": "x402",
"settlement_ref": "0x…txHash…",
"receipt_id": "rcpt_01J…",
"created_at": "2026-06-24T12:00:00.000Z"
}
],
"has_more": false,
"next_cursor": null
}Standard REST: Authorization: Bearer veto_… and JSON back. Full field list in
Orders.
One rule worth repeating: verify the RAW body.
The signature is computed over the exact bytes Veto sent. If you parse the JSON and
re-serialize it before verifying, the keys or whitespace can shift and the check will fail.
Read the raw body first (req.text() in Node, request.get_data() in Flask), verify, then
parse. Also: delivery is at-least-once, so make fulfillment safe to run twice – dedupe on
the event id (or order.id).
That's the whole model – webhook in, you fulfill
Same as Stripe: Veto tells you a sale happened and hands you the order; your app turns that into a delivered product – a parcel, a download, an unlocked account. Nothing proprietary to learn, no delivery engine to configure. Just a signed webhook and the fulfillment you already have.
Sale notifications – set your URL + secret
Where you tell Veto which URL to call and copy your whsec_… signing secret.
Webhooks – the full contract
Every event type, the exact order object, the Veto-Signature HMAC scheme, dedupe, and the
retry backoff.
How Veto works – discover, pay, deliver
The whole arc in plain English, from discovery through the delivery webhook.
How Veto works – discover, pay, deliver
The whole model in plain English. An agent discovers your checkout, pays you USDC over x402, and then YOU deliver the goods – triggered by a signed order.settled webhook that carries what was bought, the amount, the receipt, and the buyer's shipping details.
Set up Veto end-to-end (hosted)
Zero to a live agent purchase on the hosted platform – sign in, mint keys, create a merchant, add a product, set your receiving address, publish, connect your site, and watch an AI agent buy it over x402.