avouch

Connect a platform to Avouch.

Everything a platform's engineers need: how access works, the API and its keys, listing an app, the check every app passes, reporting sales, webhooks, seats, the signed receipts, and the MCP tools an agent uses.

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.

Put the Get customers button in your product

One button in your builder launches your user's app to real sellers. It works like Stripe Checkout: your server makes a short-lived launch session with your key, and your page gets only that session's client secret. Your key never goes near a browser.

1. Once: register the pages that show the button

curl -X POST https://<avouch>/v1/platform/origins -H "Authorization: Bearer avk_…" \
  -H "Content-Type: application/json" -d '{"origins": ["https://builder.example", "http://localhost:3000"]}'

Only these origins can use your sessions. A call from any other page is refused, and the browser gets no CORS answer for it. Origins are https (http only for localhost), with no path. Your test key registers its own list, for test sessions.

2. Each time a user opens the button: your server makes a session

curl -X POST https://<avouch>/v1/launch_sessions \
  -H "Authorization: Bearer avk_…" -H "Avouch-Seat: user-4821" -H "Content-Type: application/json" \
  -d '{"app_name": "Portrait Pro", "url": "https://portrait.pro",
       "tagline": "Studio portraits from one selfie.",
       "about": "Portrait Pro turns one selfie into studio headshots. US$29 once.", "commission": 35}'
→ {"ok": true, "id": "lcs_…", "client_secret": "lcs_…_secret_…", "expires_at": "2026-10-17T09:15:00Z",
   "mode": "live", "livemode": true, "seat": "user-4821"}

Everything in the body is optional: it's what the button starts with. Avouch-Seat is required: it's your id for the user, the listing is theirs, and it's the seat you're billed for. A test key (avk_test_…) makes a test session: everything it launches goes into your sandbox and says "livemode": false. A session made from a browser (the browser's own Sec-Fetch-* headers) is refused, so a key pasted into a page fails loudly instead of leaking.

3. Your page renders the button

<script src="https://<avouch>/button.js" async></script>
<avouch-button session="lcs_…" secret="lcs_…_secret_…"></avouch-button>

Two attributes, both from the session: session (its id) and secret (its client secret). app-name, app-url, tagline and commission still work and win over the prefill. Without session, the button runs the sandbox as before.

4. What happens next

  • Pressed, it shows the app and the terms in dollars (what a US$100 sale pays the seller, Avouch and the maker).
  • Launch checks the app for real (it's live, a refund policy we can find, not a refused kind) and lists it under your account and the user's seat, exactly as POST /v1/listings and launch_with_people do.
  • It shows the check, how many sellers it's matched to, its state (testing with 5 sellers; each confirmed sale earns it 4 more), how sales report (the exact Stripe webhook address and /v1/conversions details for this listing), and the app owner's manage link, once.
  • Your page gets an avouch:launched event ({sandbox, livemode, listing}), and your server gets a signed listing.created webhook.
  • The seat is active this month. A session launches once and lasts 15 minutes; make a new one each time the button is shown.

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": false inside the signed text, under a scr_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}'
    FieldMeaning
    urlThe app's public https address. It must answer.
    name, tagline, aboutWhat it is, in the maker's words (at least 2, 10 and 40 characters).
    commissionThe % of each sale the app maker pays, 5 to 70. Paid only on real sales to new customers.
    face_commissionOptional. 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.

    CheckPasses when
    The site is liveThe address answers on https.
    A refund policy we can findThe page, or a /refund or /terms page, says how refunds work.
    Not a refused kind of productNothing 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 themselvesThe 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.

    Receipts carry "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
    ToolWhat it does
    launch_with_peopleList an app (url, name, tagline, about, commission, face_commission). Returns the listing and its check.
    my_productsYour listings with sellers, clicks, sales and money owed.
    product_statusOne listing's numbers.
    recent_receiptsA listing's receipts, newest first.
    verify_receiptFetches a receipt and verifies its signature locally with the published key.
    find_productsApps 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 says livemode: 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:

    CallWhat it does
    POST /v1/launch_sessionsKey + 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.

    CallWhat it does
    GET /v1/launch_sessions/<id>/stateThe 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.
    AnswerMeans
    401 unauthorizedNo secret, or the wrong one. 10 wrong secrets close the session (410 locked).
    403 origin_not_allowedThe page's origin isn't registered for this key's mode.
    409 usedThe 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 expired15 minutes passed. Make a new session.
    429 slow_downToo 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.

    © 2026 Avouch · PrivacyPre-launch. Payouts to sellers open at launch.