Sell anything else#
Physical goods. An hour of your time. Provisioning an account by hand. A job that runs for a week. Anything we cannot hand over for you.
That is a webhook checkout — the escape hatch. We take the payment, tell you
about it, and record the sale as owed. You deliver in your own system, then
close it out.
buyer ──pays──▶ checkout402 ──notifies──▶ you
│ │
receipt: pending ◀──you mark it fulfilled──┘
When to pick it#
The other three modes all end in an HTTP response at the moment of payment. Pick
webhook when yours cannot.
| You are selling | Mode |
|---|---|
| A call to an endpoint you run | relay |
| A tool on your MCP server | mcp |
| Bytes you already have | inline |
| Anything that leaves our hands | webhook |
If you can return the goods over HTTP, one of the other three is better: they
deliver in the same round trip as the payment, and the buyer never has to trust
you to follow up. webhook is what is left, and it is genuinely everything else.
Make one#
This is the one mode that cannot be inferred. There is no payload and no
origin_url to read it off, so you say it out loud:
Create a checkout → What kind of thing is it? → Something you fulfil. Nothing beyond a title, a price and a payout wallet is asked for, because there is nothing else to ask.
c402 checkouts create \
--title "Signed print, shipped worldwide" \
--description "A4 giclée, signed and numbered. Ships in 3 working days." \
--price 40.00 \
--fulfillment webhook \
--reusable
{"title": "Signed print, shipped worldwide", "price_usd": "40.00",
"fulfillment": "webhook"}
curl -X POST https://api.checkout402.com/v1/checkouts \
-H "Authorization: Bearer c402_live_…" \
-H "content-type: application/json" \
-d '{"title": "Signed print, shipped worldwide",
"price_usd": "40.00",
"fulfillment": "webhook",
"reusable": true}'
A webhook checkout is not how you get webhooks
You do not need a registered endpoint to create one, and registering one
does not make a checkout webhook. Every settled payment fires
checkout.paid to every endpoint you have registered, in all four modes.
What this mode changes is that we have no goods to hand over.
Describe it properly. The buyer of a relay call can inspect a schema; the buyer
of a promise has only your words. description and constraints are where
"ships in 3 working days" and "EU only" belong, and they are shown in the 402
before anyone pays.
What happens when someone pays#
The money moves exactly as it does in every other mode: buyer to your wallet, on chain, in one transaction with the fee split out. Nothing about settlement is different here.
The delivery is recorded pending. The buyer's response says so:
{"mode": "webhook", "payload": null, "status": "pending",
"note": "the seller has been notified and will fulfil this separately.
Your receipt shows the current status."}
…and so does their receipt at /r/pay_…, which carries delivery_status
throughout.
Why pending, and not delivered
This used to record delivered at the moment we notified you. That meant
the buyer's receipt asserted they had received something the instant we
sent an HTTP request to you — before you had done anything at all, and
possibly before you had read it. Every delivery metric downstream was
measuring our own outbound notification and calling it a delivery.
pending is a true statement about a real state: paid, not yet fulfilled.
It is also not an alarm — a pending sale is the normal case for this mode,
not a failure, and it is counted separately from one.
Closing it out#
Fulfilment happens off-platform, so we cannot observe it. You are the only one who can say what happened.
c402 owed # what you have been paid for
c402 fulfilled pay_dVo7SDQ… --note "Tracking RM123456789CH"
c402 fulfilled pay_dVo7SDQ… --failed --note "Out of stock"
curl -X POST https://api.checkout402.com/v1/receipts/pay_dVo7SDQ…/fulfilled \
-H "Authorization: Bearer c402_live_…" \
-H "content-type: application/json" \
-d '{"status": "delivered", "note": "Tracking RM123456789CH"}'
status is delivered or failed; anything else is a 400. The queue
itself is GET /v1/receipts/awaiting-fulfilment.
list_awaiting_fulfilment — no arguments — returns the sales you owe.
mark_fulfilled takes receipt_id, an optional status and an optional
note.
The note is the useful part: it is what the buyer reads on their receipt, so it is where a tracking number, a reference, or a reason belongs.
/v1/analytics carries the same queue as delivery.awaiting_fulfilment, beside
your revenue — which is the correct place for it, since an unfulfilled sale is
cash you are holding against work you have not done.
Say when you cannot
--failed is not an error to hide. It is you stating that the buyer will
not get what they paid for, which is what starts a refund conversation
instead of leaving them waiting for something that is not coming.
The other three modes hand over the goods at payment, so there is nothing for you
to confirm and /fulfilled refuses them with a 400 naming the mode.
Being told you were paid#
Register an endpoint and every settled payment is posted to it, signed:
c402 webhooks add https://yourco.com/hooks/c402
The signing secret is returned once — like an API key, only its use is verifying HMAC-SHA256 signatures on each delivery. Verify against the raw request bytes, with a constant-time compare, and reject stale timestamps. The full recipe, including working code, is in Webhooks.
For low volume, polling c402 owed is honestly fine and has fewer moving parts.
It cannot be sold as a package#
calls: N is refused at create:
a webhook checkout cannot be sold as a package — the count would measure our
notifications to you, not anything the buyer received. Drop `calls`.
A package is N deliveries for one payment, and here we make no deliveries. The
counter would tick on our messages to you, which is not a thing the buyer bought.
Sell N of something by pricing N of it in one checkout, and describe what N
is in the title.
The exposure the buyer is accepting#
Nothing ages a pending sale
There is no timeout, no automatic refund, and no escalation. A sale that
settles and is never closed out stays pending indefinitely — it will
appear in your queue and on the buyer's receipt forever, and nothing in the
system acts on it.
Settlement is non-custodial: the USDC moved from the buyer to your wallet in one transaction at purchase, and we never held it. So we cannot refund it either. There is no balance to claw back.
A buyer who paid and was never fulfilled has one channel, and it needs no account:
curl -X POST https://api.checkout402.com/v1/checkouts/chk_abc123/reports \
-H "content-type: application/json" \
-d '{"category": "not_delivered", "receipt_id": "pay_dVo7SDQ…"}'
That makes the problem visible to you and to us, with our own delivery records attached — see Reports. It does not produce a refund, because nothing here can.
This is the mode where the buyer is most exposed, and it is worth being blunt about what that means for you: they are extending you credit on a promise, in a product where every other mode delivers before the connection closes. Three things follow.
- Close sales out quickly. The queue is the product working; a long queue is not.
- Use
--failedearly. A buyer told "no" today can spend their money elsewhere. A buyer told nothing for a month files a report. - Say what you are committing to in
descriptionandconstraints, before they pay, not after.
What next#
- What can I sell? — the four types side by side
- Payments & receipts — what settled, and proving it
- Payout wallets — where the money lands
- API reference — every field, and the reports endpoint