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.

Built an app yourself? Read the plain guide for app makers.

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 and may be shown (testing: matched to 3 of up to 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: a test sale, end to end has every step. A test listing's signing secret comes back when it is made, and from GET /v1/listings/<slug>/manage. Your sandbox's receipt webhooks are delivered (livemode: false), POST /v1/webhooks/test sends one on demand, and Stripe test events go to the sandbox's own endpoint. Signed in to the console with the test key, you see the sandbox. Test data is kept for 30 days after its last use.

    A test sale, end to end

    With your test key you can run the whole money path yourself, against your own sandbox: make a test listing, get a click from an example seller, send a signed sale, read its receipt and verify it. Nothing is real and nobody is paid; every answer says "livemode": false.

    1. A test listing, and its signing secret

    curl -X POST https://<avouch>/v1/listings -H "Authorization: Bearer avk_test_…" -H "Avouch-Seat: user-4821" \
      -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}'
    → 201 {"ok": true, "created": true, "slug": "portrait-pro", "seat": …, "livemode": false,
           "signing_secret": "scsec_…", "signing_secret_note": "Test mode: … shown again at GET /v1/listings/portrait-pro/manage", …}

    A test listing has no manage page, so its signing_secret comes back once, here. GET /v1/listings/<slug>/manage with the test key (and the seat) shows it again, with the exact request to send. Keep it on your server. A live key gets 403 test_mode_only there: a live listing's secret is only on its owner's manage page.

    2. A click from an example seller

    curl -X POST https://<avouch>/v1/listings/portrait-pro/test_click -H "Authorization: Bearer avk_test_…" -H "Avouch-Seat: user-4821"
    → 201 {"ok": true, "click": "ck_…", "seller": {"name": "… (example)", "example": true, …}}

    Your sandbox has no real sellers. Without a click, a sale is recorded as unattributed and has no receipt.

    3. The signed sale

    Sign the exact bytes you send. Python:

    import hashlib, hmac, json, time, urllib.request
    SECRET = "scsec_…"                       # the listing's signing secret
    body = json.dumps({"product": "portrait-pro", "order_id": "A-1001", "amount": "29.00", "currency": "usd",
                       "click": "ck_…", "new_customer": True, "charge_id": "pi_test_1"}, separators=(",", ":")).encode()
    t = str(int(time.time()))
    sig = hmac.new(SECRET.encode(), t.encode() + b"." + body, hashlib.sha256).hexdigest()
    req = urllib.request.Request("https://<avouch>/v1/conversions", data=body, method="POST", headers={
        "Content-Type": "application/json", "Sharecut-Signature": "t=%s,v1=%s" % (t, sig),
        "Authorization": "Bearer avk_test_…"})       # test mode only: the test key says which sandbox
    print(urllib.request.urlopen(req).read().decode())

    Or with curl and openssl:

    SECRET='scsec_…'
    BODY='{"product":"portrait-pro","order_id":"A-1002","amount":"29.00","currency":"usd","click":"ck_…","new_customer":true}'
    T=$(date +%s)
    SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
    curl -X POST https://<avouch>/v1/conversions -H "Content-Type: application/json" \
      -H "Sharecut-Signature: t=$T,v1=$SIG" -H "Authorization: Bearer avk_test_…" --data-raw "$BODY"
    → {"ok": true, "status": "held", "ledger_status": "held", "receipt": "scr_test_a2_14_…", "test": true, "livemode": false, …}

    4. The receipt, and checking it

    curl https://<avouch>/v1/receipts/scr_test_a2_14_…
    python3 verify_receipt.py --issuer https://<avouch> https://<avouch> scr_test_a2_14_…
    → VERIFIED. Signed facts: … status held … livemode False
      SANDBOX RECEIPT (livemode false): genuinely signed, but an example from a sandbox. It is not a real sale.

    A sandbox receipt lives on the Avouch that made it, so name that host as the issuer (locally, --issuer http://127.0.0.1:8830 http://127.0.0.1:8830 …). Then try a refund (step 5 of Reporting sales), a webhook test delivery (Webhooks), and the console with your test key: its test-mode view shows these listings, sales and receipts.

    Stripe in test mode: GET /v1/listings/<slug>/manage gives the sandbox's own Stripe test endpoint, /v1/stripe/test/<sandbox>/<slug>. Add it in your Stripe test dashboard and send its signing secret with POST /v1/listings/<slug>/manage {"stripe_whsec": "whsec_…"} and the test key. Live events are refused there.

    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).

    Reissue or revoke a key

    • From the console (/console, signed in with the key): Reissue this key. The new key is shown once on the page. The old key stops at once, every connection made with it (Claude, other OAuth apps) ends, and console sessions made with the old key end. Signed in with the test key, it reissues the test key.
    • By the owner, for an account: POST /admin/accounts/<id>/key issues a new live key (the old one and its connections end), and POST /admin/accounts/<id>/key/revoke stops it with no new key until one is reissued. POST /admin/accounts/<id>/test-key does the same for the test key. Write to us if your key leaked and you can't reach the console.

    Listings

    POST /v1/listings

    curl -X POST https://<avouch>/v1/listings \
      -H "Authorization: Bearer avk_…" -H "Avouch-Seat: user-4821" -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}'
    → 201 {"ok": true, "created": true, "slug": "portrait-pro", "name": "Portrait Pro", "url": "https://portrait.pro",
           "status": "live", "seat": "user-4821", "commission_pct": 35, "sellers": 0, "clicks_30d": 0,
           "sales": {"held": 0, "unconfirmed": 0, "cleared": 0, "paid": 0, "refunded": 0, "refused": 0,
                     "returning customer": 0, "unattributed": 0},
           "owed_to_sellers_cents": 0, "page": "https://<avouch>/p/portrait-pro",
           "check": {"ready_for_sellers": true, "sales_report_themselves": false, "fixes": ["Connect the checkout…"]},
           "livemode": true}
    FieldMeaning
    urlThe app's public https address. It must answer. An http:// address is listed as https://, and the answer says so in url_note.
    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. Default 30, the same for this call, the MCP tool launch_with_people, a launch session and the website form. 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: at least commission, at most 90. Default: commission + 10.

    In this order: bad fields come back first as 400 invalid with fields naming each one. Then the address: listing the same address again from the same seat returns the existing listing (created: false); from another seat of the same account it returns that listing too (created: false, yours: false; it stays that seat's, with its numbers); an address another account listed gets 409 already. 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. Then the site is fetched (400 unreachable if it doesn't answer).

    A failed listing, for example:

    → 400 {"ok": false, "error": "invalid", "detail": "Some fields need fixing: about, commission.",
           "fields": {"about": "A few sentences: what it is, who it is for, what it costs.",
                      "commission": "A share between 5 and 70 percent."}, "livemode": true}

    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 (the seat's, newest first), GET /v1/listings/<slug> (sellers promoting it, 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; a whole number from 1 to 200, anything else is a 400 naming it) with has_more and next. Pass ?after=<next> for the next page; next is null on the last one. after is an id from a previous page (a whole number up to 9223372036854775807).

    GET /v1/listings?limit=2
    → {"ok": true, "seat": "user-4821", "listings": [{"id": 41, "slug": "third-app", …}, {"id": 38, "slug": "portrait-pro", …}],
       "has_more": true, "next": 38, "livemode": true}
    GET /v1/listings?limit=2&after=38
    → {"ok": true, "seat": "user-4821", "listings": [{"id": 12, …}], "has_more": false, "next": null, "livemode": true}

    Sale statuses

    One set of words everywhere: a receipt's status, the receipts list, a listing's sales counts, webhooks, the answers of /v1/conversions and /v1/refunds, the MCP tools and the console. Where the ledger has its own word, answers also carry it as ledger_status.

    StatusMeans
    heldReported and confirmed by the business, inside the refund window. A refund now reverses it.
    unconfirmedReported by the pixel, not yet confirmed by the business with a signed call.
    clearedPast the refund window: it stuck, and the seller is owed their share.
    paidCleared, and the seller has been paid.
    refundedRefunded (or charged back) inside the window: nothing is owed.
    refusedThe rules refused it, such as a seller buying through their own link. Nothing is owed.
    returning customerThe business said the buyer wasn't new, and the listing pays for new customers only. Nothing is owed.
    unattributedNo click, code or ref matched a seller of the listing. Recorded, nothing is owed, and it has no receipt.

    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 (always one string, for one check), so an agent can fix it; a listing's check.fixes is the list of what's left. matchable (or ready_for_sellers) means the first three pass; passed means all four do; ok only means the call worked. 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. In test mode the endpoint is the sandbox's own (A test sale).

    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>

    Sign the exact raw bytes you send: serialise the body once, sign those bytes, send those bytes. Runnable Python and curl versions are in A test sale; for a live listing leave out the Authorization header. The listing's secret is on its manage page. The time must be within 5 minutes. amount is a string with at most two decimals (or amount_cents, a whole number). An order id is counted once, so retries are safe (duplicate: true). Attribution: the click id, then the seller's code, then their ref; with none, the sale is unattributed and has no receipt. In test mode, whether a sale is a test comes from your test key, never from the body. (The header keeps the name Avouch had when it was built.)

    Refunds: POST /v1/refunds

    body = {"product": "portrait-pro", "order_id": "A-1001"}          # signed the same way; add "chargeback": true for a dispute
    → inside the refund window:   {"ok": true, "status": "refunded", "ledger_status": "reversed"}
    → after the sale cleared:     {"ok": true, "status": "cleared", "ledger_status": "payable", "kept": true,
                                   "detail": "This sale had cleared, so the affiliate keeps the commission."}
    → a chargeback after payout:  {"ok": true, "status": "charged_back", "detail": "Charged back after payout: it comes off the affiliate's next payouts."}
    → again:                      {"ok": true, "status": "refunded", "duplicate": true}       unknown order: 404

    Cleared means kept. A refund inside the refund window reverses the sale and nothing is owed. After the window, the sale has cleared: a plain refund is the business's own, and the seller keeps the commission (the answer says kept: true). A chargeback (fraud) is different: before payout it is reversed; after payout the commission is taken off the seller's next payouts.

    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.

    CallWhat it does
    GET /v1/webhooksThe address that is set (never the secret) and its deliveries: delivered, waiting, failed, and the last error.
    POST /v1/webhooks again409 already_set, naming the address. To replace it and get a new secret send {"url": …, "rotate": true}: the old secret stops at once.
    DELETE /v1/webhooksRemoves it. Events still waiting are dropped.
    POST /v1/webhooks/testSends one signed webhook.test event now and says whether it arrived. With a sale on the account, it carries that sale's receipt.created body (with its signed receipt) as sample, so you can test your verifier. Works with a test key (livemode: false).

    Events: receipt.created (a sale arrived), receipt.updated (it cleared, was paid 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. A webhook set with your test key gets your sandbox's receipt events (livemode: false); listing.created is sent for live listings only.

    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 that made them, and seats can't see each other's. Reads with a seat return "seat".
    • No Avouch-Seat (or Avouch-Seat: account) means your platform's own seat, "account". It is billed like any other seat, so a listing made without the header still counts. A read without the header reads that seat and says so: "seat": "account", and a seat_note with how many listings your users' seats hold. A listing that belongs to a user's seat is a 404 that names the seat it looked in.
    • Launch sessions need a real seat (400 seat_required without one). An OAuth connection's seat is fixed when it is allowed.
    • A seat is active in a month when it made a listing (by key, MCP or the button) or a deal that month.
    GET /v1/platform/usage?month=2026-10
    → {"ok": true, "month": "2026-10", "active_seats": 2, "listings": 2, "sales": 0, "seat_price_cents": 900, "bill_cents": 1800, "livemode": true}

    No key is 401, a month that isn't YYYY-MM is 400, and a direct (non-platform) account is 403 not_a_platform. With the test key it reports your sandbox.

    Receipts

    Every sale a seller brought 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. A sale no seller brought (unattributed) is recorded but has no receipt. Its status is one of the sale statuses.

    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-production-02c1.up.railway.app
    python3 verify_receipt.py --issuer https://<avouch> https://<avouch> scr_12_…   # another issuer you trust
    python3 verify_receipt.py --issuer https://<avouch> https://<avouch> scr_test_a2_14_…   # a sandbox receipt: name the host that made it

    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-production-02c1.up.railway.app (or --issuer <origin>) and refuses a receipt fetched from any other host. A sandbox receipt (scr_test_) lives on the Avouch you tested against, so pass that host with --issuer.

    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 (without it the tools act for your platform's own seat, account). Local:

    claude mcp add avouch -e AVOUCH_URL=https://<avouch> -e AVOUCH_API_KEY=avk_… -e AVOUCH_SEAT=user-4821 -- python3 sharecut_mcp.py
    ToolWhat it does
    launch_with_peopleList an app (url, name, tagline, about, commission, default 30 as in REST, face_commission). Returns the listing and its check. Listing the same app again returns it.
    my_productsYour listings with sellers, clicks, sales by status and money owed.
    product_statusOne listing's numbers, its sales counted under the sale statuses.
    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.

    Every tool returns its answer twice: as JSON text, and as the same object in structuredContent. A failure ("ok": false: a field to fix, not_found, an unreachable Avouch) also sets isError: true, so an agent that checks isError never reads a failure as a success. A wrong key or token gets HTTP 401 with WWW-Authenticate, which says when the key itself isn't active.

    Running the hosted server yourself: set AVOUCH_MCP_RESOURCE to the MCP server's public URL exactly as users add it (for example https://<avouch-mcp>/mcp), with the same value on the Avouch app. OAuth tokens are issued only for that resource, so without it every sign-in ends in invalid_target. On Avouch's own hosted server it is already set. The hosted connection answers GET /healthz with the code version it is running, its resource, 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; with none, it acts for the platform's own seat, account.
    • To end a connection, reissue the key that made it (API keys: the console's Reissue, or the owner's route): every connection made with the old key stops at once. One connection alone ends with POST /oauth/revoke from its client.

    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 JSON object {app_name, url, tagline, about, commission, face_commission} (anything else, a list included, is 400). Returns {id, client_secret, expires_at, mode}. 30 an hour per seat.
    POST /v1/platform/origins{"origins": [...]}, a JSON list, 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 (default 30), the terms on a US$100 sale in cents (you_pay_cents, to_the_seller_cents, to_avouch_cents, you_keep_cents), status open or complete, and next.
    POST /v1/launch_sessions/<id>/check{url}. The four checks with their fixes, matchable, passed, 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 url, name, tagline, about to set or fix a field (a url here is used as if it had been sent to /check). Returns the POST /v1/listings answer plus check_full, matched_sellers (sellers it is matched to now), seller_cap (how many it may be shown at this stage), state (both, in words: testing: matched to 3 of up to 5 sellers), stage, manage and how_sales_report.

    A launch whose fields need fixing is answered before the session is used, so it doesn't spend a try:

    POST /v1/launch_sessions/lcs_…/launch  {"commission": 30, "url": "https://blank-test.example.app"}
    → 400 {"ok": false, "error": "invalid", "session": "open", "tries_left": 5,
           "fields": {"name": "Give the product its name.", "tagline": "One line on what it does, at least ten characters.",
                      "about": "A few sentences: what it is, who it is for, what it costs."},
           "detail": "Fix these and launch again (this didn't use a try): about, name, tagline.", "livemode": true}
    AnswerMeans
    400 invalidA field to fix (fields), the session still open. Doesn't use a try.
    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 got as far as fetching the site and failed (400 unreachable, "session": "open") can be tried again, up to 5 tries (409 too_many_tries after).
    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 (your server reads the test listing's signing secret from GET /v1/listings/<slug>/manage; the page never gets a secret), the Stripe address is the sandbox's own test endpoint, and listing.created isn't delivered.

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