Access
Accounts are enterprise only. Avouch is available to AI builder platforms by agreement. Nobody can make themselves a key: POST /v1/accounts answers 403 enterprise_only unless the owner calls it.
To get access, leave a work email in the form on the home page. We reply to every platform personally, agree the terms (including the seat price), and issue your key.
Founders building an app can join the waitlist on the home page. Avouch opens through platforms first.
Try it: the sandbox
Run the whole flow on Avouch's real code, now, with no key. An agent lists an app, Avouch checks it, example sellers are matched, one promotes it, a buyer clicks, the app's checkout reports a signed sale, and Avouch issues the receipt, signed with its real key. Then your browser checks the signature itself, against the published key.
curl -X POST https://<avouch>/v1/sandbox/run -H "Content-Type: application/json" \
-d '{"app_name": "Portrait Pro", "url": "https://portrait.pro",
"tagline": "Studio portraits from one selfie.", "commission": 35}'
→ {"ok": true, "sandbox": true, "livemode": false, "ms": 41,
"steps": [{"say": "An agent lists the app", "call": "POST /v1/listings", "status": 201, …}, …],
"receipt": {"token": "scr_test_…", "signed": "{…\"livemode\":false…}", "signature": "…", "kid": "sc-…"},
"public_key_url": "/.well-known/avouch-receipts.json"}
What keeps it honest
- It shares nothing with the live service. Each run has its own database, made from the live schema with no rows, and deleted after an hour. A sandbox app, seller, click or sale never reaches the marketplace,
find_products, a real seller's matches, payouts, usage or billing. - Nothing is fetched. In the sandbox the site check is simulated, and the step says so. A real listing fetches the site.
- The sellers are fictional examples, each marked
"example": true, and the buyer is simulated. - The receipt is real and says it's a sandbox. It's signed with Avouch's real Ed25519 key, with
"livemode": falseinside the signed text, under ascr_test_token, so it can never pass for a real sale. - Body fields are all optional:
app_name,url,tagline,commission(5 to 70). 20 runs per connection per hour.
Test keys
Under an agreement, your account can have a test key, avk_test_…, beside your live key. Every call made with it runs in your own sandbox: nothing is real and nothing is paid, and each answer says "livemode": false (and the header Avouch-Livemode: false). Report test sales to POST /v1/conversions with the usual signature and Authorization: Bearer avk_test_…, so we know which sandbox it belongs to. Test data is kept for 30 days after its last use. Receipt webhooks and Stripe events aren't delivered in test mode yet.
API keys
Your key starts with avk_. It is shown once when it is issued and stored only as a hash, so keep it somewhere safe. Send it on every call:
Authorization: Bearer avk_…
A missing or wrong key gets 401 unauthorized. A key only ever sees the listings it made. A test key starts with avk_test_ and only ever sees its own sandbox (the sandbox).
Listings
POST /v1/listings
curl -X POST https://<avouch>/v1/listings \
-H "Authorization: Bearer avk_…" -H "Content-Type: application/json" \
-d '{"url": "https://portrait.pro", "name": "Portrait Pro",
"tagline": "Studio portraits from one selfie.",
"about": "Portrait Pro turns one selfie into studio headshots. US$29 once.",
"commission": 35, "face_commission": 45}'
| Field | Meaning |
|---|---|
url | The app's public https address. It must answer. |
name, tagline, about | What it is, in the maker's words (at least 2, 10 and 40 characters). |
commission | The % of each sale the app maker pays, 5 to 70. Paid only on real sales to new customers. |
face_commission | Optional. A higher % for a post with the seller's own face, checked by a person. Default: commission + 10. |
Listing the same address again with the same key returns the existing listing (created: false). The address is compared by its host only: lowercase, without www., a trailing dot, the port, path, query or fragment, so https://yourapp.com/?ref=x and https://YourApp.com.:443/ are the same listing. An address listed by someone else gets 409 already. Bad fields come back as 400 with fields naming each one.
Limits: 500 listings a day per key, counted per seat for a platform (set by AVOUCH_LISTINGS_PER_KEY_DAY). The website form's per-connection limit does not apply to keyed calls.
Reading them
GET /v1/listings (yours, newest first), GET /v1/listings/<slug> (sellers, clicks in 30 days, sales by status, money owed to sellers), GET /v1/listings/<slug>/receipts (signed receipts, newest first: each with token, status, amount_cents, currency, day, id, and signed, signature and kid exactly as GET /v1/receipts/<token> gives them).
Pages: both lists return at most limit items (default 100, at most 200) with has_more and next. Pass ?after=<next> for the next page; next is null on the last one.
A receipt's status is one of held, cleared, refunded, unconfirmed (reported by the pixel, not yet confirmed by the business), returning customer, refused (a sale the rules refused, such as a seller buying through their own link) and no seller.
The check
Every app is checked automatically before a single seller sees it. The launch response includes the result, and GET /v1/listings/<slug>/check runs it again after a fix.
| Check | Passes when |
|---|---|
| The site is live | The address answers on https. |
| A refund policy we can find | The page, or a /refund or /terms page, says how refunds work. |
| Not a refused kind of product | Nothing in the name, tagline or description is gambling, adult content, weapons, drugs, crypto or get-rich schemes, payday loans, miracle health claims or MLM. |
| Sales report themselves | The checkout is connected (below). Needed to be paid automatically. |
Each failed check comes with a fix in plain words, so an agent can fix it. An app that fails one of the first three isn't shown to sellers, isn't listed by find_products or /api/products, and its public page says it isn't open to sellers yet.
Reporting sales
From Stripe (nothing to build)
In the app's own Stripe dashboard, add one webhook endpoint, https://<avouch>/v1/stripe/<slug>, for checkout.session.completed, charge.refunded and charge.dispute.created, and paste its signing secret (whsec_…) on the listing's manage page. Every event is checked with Stripe's own signature. Pass the click id our link adds (?sc_click=) as the session's client_reference_id or metadata.sc_click.
From your server: POST /v1/conversions
body = {"product": "portrait-pro", "order_id": "A-1001", "amount": "29.00",
"click": "<sc_click>", "new_customer": true, "charge_id": "pi_…"}
t = unix seconds
sig = hex HMAC-SHA256(listing secret, t + "." + raw body)
header Sharecut-Signature: t=<t>,v1=<sig>
The listing's secret is on its manage page. The time must be within 5 minutes. An order id is counted once, so retries are safe. Attribution: the click id, then the seller's code, then their ref. Refunds: POST /v1/refunds with {product, order_id}, signed the same way. (The header keeps the name Avouch had when it was built.)
Webhooks
POST /v1/webhooks {"url": "https://…"} with your key returns a secret, once. Only public https addresses are accepted, and a delivery never follows a redirect (a 3xx counts as a failed delivery). An app connected through OAuth can't set it: only the account's own key can.
Events: receipt.created (a sale arrived), receipt.updated (it cleared or was refunded) and listing.created (a listing was made, by key, MCP or the button: {"type", "product", "seat", "via", "launch_session", "listing", "livemode", "at"}). Each receipt delivery is a POST of {"type", "id", "receipt", "status", "ledger_status", "why", "product", "at", "livemode", "signed_receipt"} (signed_receipt is {signed, signature, kid}, the same as GET /v1/receipts/<token>) with:
Avouch-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, t + "." + body)>
Check it before trusting the body. Failed deliveries are retried with backoff (30 s, 2 min, 8 min…) up to 6 times.
Seats and usage
The app maker pays only the commission, only when something sells. A platform pays per active seat, at the price in its agreement.
Your agent acts for each of your users by sending your key plus that user's id on your side:
Authorization: Bearer avk_…
Avouch-Seat: user-4821
Listings are owned by the seat, and seats can't see each other's. A seat is active in a month when it launched a listing that month.
GET /v1/platform/usage?month=2026-10
→ {"active_seats": 2, "listings": 2, "sales": 0, "seat_price_cents": 900, "bill_cents": 1800}
Receipts
Every sale gets a receipt: which seller, which post, whether a person checked it was their own face, whether the business confirmed it, whether it was a new customer, and the merchant's own charge id. It never holds the customer's details.
GET /v1/receipts/<token> returns the receipt with signed (the exact JSON text), signature (Ed25519, base64url) and kid. The public keys are at /.well-known/avouch-receipts.json. Verify the signature over the bytes of signed and read facts only from signed. Only "livemode": true is a real sale; a sandbox receipt says false, inside the signed text. A person can open /receipt/<token> and press Verify.
curl -O https://<avouch>/docs/verify_receipt.py
python3 verify_receipt.py scr_12_… # keys and receipt from https://avouch.co
python3 verify_receipt.py --issuer https://<avouch> https://<avouch> scr_12_… # another issuer you trust
Pin the issuer. Take the keys from the issuer you trust, never from the host that handed you the receipt: a look-alike host can serve its own key and a forged receipt that checks out against it. verify_receipt.py fetches the keys from https://avouch.co (or --issuer <origin>) and refuses a receipt fetched from any other host.
verify_receipt.py needs only Python and cryptography, nothing from Avouch. The format is open: the Outcome Receipt spec.
"provider": "sharecut", Avouch's provider id from its earlier name. It stays, because it is inside the signed text.MCP tools
Any agent that speaks MCP can use Avouch. Hosted: connect to https://<avouch-mcp>/mcp with your key as the connection's Authorization: Bearer header and, for a platform, Avouch-Seat. Local:
claude mcp add avouch -e AVOUCH_URL=https://<avouch> -e AVOUCH_API_KEY=avk_… -- python3 sharecut_mcp.py
| Tool | What it does |
|---|---|
launch_with_people | List an app (url, name, tagline, about, commission, face_commission). Returns the listing and its check. |
my_products | Your listings with sellers, clicks, sales and money owed. |
product_status | One listing's numbers. |
recent_receipts | A listing's receipts, newest first. |
verify_receipt | Fetches a receipt and verifies its signature locally with the published key. |
find_products | Apps open to sellers, by words. Apps that failed the check are left out. |
The hosted connection answers GET /healthz with the code version it is running and stale: true if it should be restarted.
Connect from Claude (OAuth)
In Claude, add https://<avouch-mcp>/mcp as a connector. find_products and verify_receipt work straight away. The first time Claude uses a tool that acts on your account, it shows a Connect button: Avouch's own page opens, you paste your Avouch API key there, and choose Allow. Claude never sees the key; it gets a token that does what the key does, for one hour at a time, renewed while the connection lasts.
- A test key (
avk_test_) makes a sandbox connection: everything it does sayslivemode: false, and nothing real happens. - A platform account can name the seat (its own id for the user) on that page. The connection then acts for that seat only.
- To end a connection, reissue the key that made it: every connection made with the old key stops.
Any MCP client that follows the MCP authorization spec works the same way. Discovery starts at /.well-known/oauth-protected-resource on the MCP server, which names Avouch's authorization server; its metadata is at /.well-known/oauth-authorization-server. Avouch supports client ID metadata documents and dynamic registration, PKCE with S256 only, resource indicators, and rotating refresh tokens. Keys as a Bearer header keep working.
Launch sessions API
From your server, with your key:
| Call | What it does |
|---|---|
POST /v1/launch_sessions | Key + Avouch-Seat. Optional {app_name, url, tagline, about, commission, face_commission}. Returns {id, client_secret, expires_at, mode}. 30 an hour per seat. |
POST /v1/platform/origins | {"origins": [...]} replaces the list (up to 20). GET reads it. Per key mode. |
From the page, with the client secret in the Avouch-Client-Secret header (or client_secret in a JSON body). Never in the address: it isn't accepted there.
| Call | What it does |
|---|---|
GET /v1/launch_sessions/<id>/state | The app (prefill and what the check read), the commission, the terms on a US$100 sale, status open or complete. |
POST /v1/launch_sessions/<id>/check | {url}. The four checks with their fixes, matchable, and connect: the Stripe webhook address and its events, and the signed /v1/conversions call. Before the launch the addresses use the listing's expected name (final: false); after it, the check runs on the listing itself. 12 per session. |
POST /v1/launch_sessions/<id>/launch | {commission, face_commission?} (and name, tagline, about to fix a field). Returns the POST /v1/listings answer plus check_full, matched_sellers, state, manage and how_sales_report. |
| Answer | Means |
|---|---|
401 unauthorized | No secret, or the wrong one. 10 wrong secrets close the session (410 locked). |
403 origin_not_allowed | The page's origin isn't registered for this key's mode. |
409 used | The session already launched. A launch that came back with a field to fix (400, "session": "open") can be tried again, up to 5 times. |
410 expired | 15 minutes passed. Make a new session. |
429 slow_down | Too many sessions, checks or calls. Wait and try again. |
Every answer carries livemode; a test session's also sends Avouch-Livemode: false. In test mode the check is simulated (nothing is fetched), there is no manage page, and listing.created isn't delivered yet.