> ## Documentation Index
> Fetch the complete documentation index at: https://docs.adaptyvbio.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Pay with a Shared Payment Token (MPP-SPT)

> Read the challenge, mint a bounded Stripe Shared Payment Token, and settle the invoice as a single card charge.

# Pay with a Shared Payment Token (MPP-SPT)

This rail settles from a card rather than a crypto balance. Your agent takes a
`402` challenge that says what the token must authorize, mints a
[Stripe Shared Payment Token](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens)
bounded to those values, and hands it back; Stripe redeems it as one card
charge. The API never sees the underlying payment method, only the token.

Shared Payment Tokens are Stripe's mechanism for
[agentic commerce](https://docs.stripe.com/agentic-commerce): a limited-scope
credential the buyer issues and the seller redeems, capped by amount, currency,
recipient, and expiry.

You need a token whose role grants `Invoice:Pay` — **Member**, **Billing**, or
**Admin**. Creating the experiment additionally needs `Experiment:Create`,
which **Member** and **Admin** carry but **Billing** does not.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant You as Your agent
    participant API
    participant Stripe

    You->>API: POST /experiments (X-Adaptyv-Payment-Method: mpp-spt)
    API-->>You: experiment id
    Note over API: Adaptyv prices the work
    You->>API: POST /quotes/{quote_id}/confirm
    API-->>You: invoice_id + payment.pay_url
    You->>API: POST /invoices/{invoice_id}/pay (no credential)
    API-->>You: 402 + WWW-Authenticate header
    You->>Stripe: mint an SPT bounded to the challenge values
    Stripe-->>You: spt_…
    You->>API: POST /invoices/{invoice_id}/pay (Authorization: Payment …)
    API->>Stripe: charge the SPT
    Stripe-->>API: pi_… succeeded
    API-->>You: 200 — paid + settlement_reference (payment_intent_id)
```

## Read the challenge

Create the experiment with `X-Adaptyv-Payment-Method: mpp-spt` to pin the rail,
confirm the quote, then call `/pay` with the method header and no credential.
This call commits to nothing — it opens the payment session and answers `402`
with the challenge in the `WWW-Authenticate` response header:

```bash theme={null}
curl -i -X POST "$FOUNDRY_API_URL/invoices/$INVOICE_ID/pay" \
  -H "Authorization: Bearer $FOUNDRY_API_TOKEN" \
  -H "X-Adaptyv-Payment-Method: mpp-spt"
```

```http theme={null}
WWW-Authenticate: Payment id="dK9sQ2p…", realm="adaptyv-foundry", method="stripe",
  intent="charge", request="eyJhbW91bnQiOiIxMjUwMCIsImN…",
  expires="2026-05-27T12:05:00+00:00", description="Experiment 1f0b2c3d payment"
```

That arrives as one line; it is wrapped here to fit. `id` is opaque — echo it
back verbatim. It is not a Stripe object id.

The challenge is the whole handshake. `request` is base64url-encoded JSON
carrying everything the token must encode:

```json theme={null}
{
  "amount": "12500",
  "currency": "usd",
  "description": "Experiment 1f0b2c3d payment",
  "externalId": "in_1OqLk2LkdIwHu7ix6OboRpXl",
  "methodDetails": {
    "networkId": "profile_…",
    "paymentMethodTypes": ["card", "link"]
  }
}
```

`amount` is in the currency's minor unit — cents for `usd`, so `"12500"` is
\$125.00.

Keep the whole challenge, not just the decoded values — your credential echoes
it back, as [below](#retry-with-the-credential). This is also why no endpoint
hands you these values without a `402`: the API settles against the challenge it
stored when it opened the session, so parameters obtained any other way would
echo a challenge no session is holding. A credential sent before a challenge is
answered with `409`.

`501` means this environment does not serve the rail at all; `422` means the
invoice is not in a payable state.

The challenge arrives under the standard `WWW-Authenticate` name, so a stock
MPP client reads it without special handling.

## Mint the SPT

Mint the SPT against `methodDetails.networkId`, denominated in `currency`,
capped at `amount`, and expiring no earlier than the challenge's `expires` —
roughly five minutes out.

Bound the token to those four values. The API re-checks them against the
challenge before handing the token to Stripe, and Stripe refuses a token that
authorizes less than the amount due, targets another recipient, uses another
currency, or has expired. Bound looser and you are carrying risk for nothing.

The challenge is valid for five minutes. Mint and settle inside that window. A
credential echoing an expired challenge is refused with `409`; call `/pay` again
without a credential to take a fresh challenge on the same invoice.

## Retry with the credential

The credential is base64url without padding, wrapping the token and an echo of
the challenge:

```json theme={null}
{
  "challenge": {
    "id": "…", "realm": "…", "method": "…",
    "intent": "…", "request": "…", "expires": "…"
  },
  "payload": { "spt": "spt_…", "externalId": "…" }
}
```

Copy every `challenge` value verbatim from the `WWW-Authenticate` parameters,
`request` included as the raw string you received. The API compares your echo
against the challenge it issued, so a re-encoded `request` is refused even
though it decodes to the same JSON.

Send the credential in the `Authorization` header under the `Payment` scheme,
and move your API token to `X-Adaptyv-Api-Key`:

```bash theme={null}
curl -i -X POST "$FOUNDRY_API_URL/invoices/$INVOICE_ID/pay" \
  -H "X-Adaptyv-Api-Key: $FOUNDRY_API_TOKEN" \
  -H "Authorization: Payment $SPT_CREDENTIAL" \
  -H "X-Adaptyv-Payment-Method: mpp-spt"
```

<Warning>
  Do not send `Authorization` twice. Only the first value is read, so a bearer
  token there hides the payment credential behind it and you are re-challenged
  forever with nothing to explain why. The two credentials need two headers.
  Send this request with a raw HTTP client rather than a generated SDK.
</Warning>

The API validates the credential and charges the token. No charge exists until
this request.

```json theme={null}
{
  "id": "in_1OqLk2LkdIwHu7ix6OboRpXl",
  "status": "paid",
  "already_paid": false,
  "settlement_reference": {
    "type": "stripe_payment_intent",
    "payment_intent_id": "pi_3Tw3Ah..."
  }
}
```

<Note>
  If the charge succeeds but the response never reaches you, repeat the call. The
  API returns the charge it already made rather than making a second one.
</Note>
