VetoVetoDocs
Getting Started

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:

order.settled – the POST body Veto sends your app
{
  "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.

webhook.ts – verify with the SDK, then fulfill
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.

webhook.py – verify the HMAC by hand, then fulfill
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 retries

You 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:

GET /v1/orders – re-fetch recent orders
curl "https://api.veto-ai.com/v1/orders?merchant_id=mrch_01J…&limit=50" \
  -H "Authorization: Bearer veto_test_8f2c…"
200 – newest first
{
  "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.