> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://mpp.dev/api/mcp` to find what you need.

# Stripe charge \[One-time payments using Shared Payment Tokens]

The Stripe implementation of the [charge](/intents/charge) intent.

The client creates a [Shared Payment Token (SPT)](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens) and sends it as a Credential. The server creates a Stripe `PaymentIntent` using the SPT, and settlement completes through Stripe's payment rails.

Use this method for single API calls, content access, or one-off purchases where you want to accept cards, wallets, or other Stripe-supported payment methods.

## Server

Use `stripe.charge` to require a one-time Stripe payment before returning a response. The method handles Challenge generation, Credential verification, PaymentIntent creation, and Receipt generation.

You can provide either a `client` (a pre-configured Stripe SDK instance) or a raw `secretKey`. Using `client` is recommended — it lets you configure retries, API version, and other options on the Stripe instance you control.

### With Stripe SDK client (recommended)

```ts twoslash

import Stripe from 'stripe'
import { Mppx, stripe } from 'mppx/server'

const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!)

const mppx = Mppx.create({
  methods: [
    stripe.charge({
      client: stripeClient, // [!code hl]
      networkId: 'internal',
      paymentMethodTypes: ['card'],
    }),
  ],
})

export async function handler(request: Request) {
  const result = await mppx.charge({
    amount: '1',
    currency: 'usd',
    decimals: 2,
    description: 'Premium API access',
  })(request)

  if (result.status === 402) return result.challenge

  return result.withReceipt(Response.json({ data: '...' }))
}
```

### With secret key

If you don't need to customize the Stripe SDK instance, pass a `secretKey` directly and mppx makes raw API calls to Stripe.

```ts twoslash
import { Mppx, stripe } from 'mppx/server'

const mppx = Mppx.create({
  methods: [
    stripe.charge({
      secretKey: process.env.STRIPE_SECRET_KEY!, // [!code hl]
      networkId: 'internal',
      paymentMethodTypes: ['card'],
    }),
  ],
})
```

### With metadata

Include `metadata` in the `stripe.charge` configuration to forward key-value pairs to Stripe. The metadata appears in the Challenge and attaches to the Stripe `PaymentIntent`.

```ts twoslash

import Stripe from 'stripe'
import { Mppx, stripe } from 'mppx/server'

const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!)

const mppx = Mppx.create({
  methods: [
    stripe.charge({
      client: stripeClient,
      metadata: { plan: 'pro' }, // [!code hl]
      networkId: 'internal',
      paymentMethodTypes: ['card'],
    }),
  ],
})
```

### With multiple payment method types

Allow multiple payment methods, like cards and Link, by specifying them in `paymentMethodTypes`.

```ts twoslash

import Stripe from 'stripe'
import { Mppx, stripe } from 'mppx/server'

const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!)

const mppx = Mppx.create({
  methods: [
    stripe.charge({
      client: stripeClient,
      networkId: 'internal',
      paymentMethodTypes: ['card', 'link'], // [!code hl]
    }),
  ],
})
```

### Payment links

Set `html` on the method to render a Stripe Elements payment form when a browser visits the endpoint.

```ts twoslash

import Stripe from 'stripe'
import { Mppx, stripe } from 'mppx/server'

const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!)

const mppx = Mppx.create({
  methods: [
    stripe.charge({
      client: stripeClient,
      // [!code hl:start]
      html: {
        createTokenUrl: '/api/create-spt',
        publishableKey: process.env.STRIPE_PUBLISHABLE_KEY!,
      },
      // [!code hl:end]
      networkId: 'internal',
      paymentMethodTypes: ['card'],
    }),
  ],
})
```

### html.createTokenUrl

* **Type:** `string`

A same-origin URL on your server that accepts a `POST` with `{ paymentMethod, amount, currency, expiresAt }` and returns `{ spt: string }`. This is the same endpoint used by the [client-side `createToken` callback](#client).

### html.publishableKey

* **Type:** `string`

Your Stripe publishable key (`pk_live_...` or `pk_test_...`), embedded in the payment page for Stripe.js initialization.

Programmatic clients with `Authorization` headers are unaffected.

See the [payment links guide](/guides/payment-links) for a full walkthrough and live demo.

### Server parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `client` | `StripeClient` | One of `client` or `secretKey` | Pre-configured Stripe SDK instance (`new Stripe(...)`) |
| `secretKey` | `string` | One of `client` or `secretKey` | Stripe secret API key (mppx makes raw API calls) |
| `metadata` | `Record<string, string>` | Optional | Key-value pairs forwarded to Stripe |
| `networkId` | `string` | Required | Stripe [Business Network](https://docs.stripe.com/get-started/account/profile) profile ID |
| `paymentMethodTypes` | `string[]` | Required | Allowed Stripe payment method types |

## Client

:::tip[For agents]
The [Link CLI](/tools/wallet#link-cli) handles `stripe.charge` end-to-end—`link-cli mpp pay` parses the 402 Challenge, creates an SPT from the user's Link wallet, and retries the request with the Credential. No code required.
:::

Use `stripe` with `Mppx.create` to automatically handle `402` responses. The client parses the Challenge, creates an SPT through the `createToken` callback, and retries with the Credential.

SPT creation requires a Stripe secret key, so the client accepts a `createToken` callback that proxies through a server endpoint. You can optionally pass a `client` (a Stripe.js instance from `@stripe/stripe-js`) which is forwarded to the `createToken` callback for use with Elements.

### Simple (known payment method)

If you already have a payment method ID (for example a test card or a stored method), pass it as `paymentMethod` and mppx handles the full 402 → SPT → retry flow automatically.

```ts twoslash

import { loadStripe } from '@stripe/stripe-js'
import { Mppx, stripe } from 'mppx/client'

const stripeJs = (await loadStripe('pk_test_...'))!

Mppx.create({
  methods: [
    stripe({
      client: stripeJs,
      createToken: async (params) => {
        const res = await fetch('/api/create-spt', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(params),
        })
        if (!res.ok) throw new Error('Failed to create SPT')
        return (await res.json()).spt
      },
      paymentMethod: 'pm_card_visa', // [!code hl]
    }),
  ],
})

// fetch() now handles 402 → credential → retry automatically
const response = await fetch('https://api.example.com/resource')
// @log: Response { status: 200, ... }
```

### With Stripe Elements

For interactive payment collection, use `onChallenge` to render Stripe Elements when a 402 is received. The user enters card details, you create a payment method, then pass it to `createCredential`.

```ts twoslash

import { loadStripe } from '@stripe/stripe-js'
import { Receipt } from 'mppx'
import { Mppx, stripe } from 'mppx/client'

const stripeJs = (await loadStripe('pk_test_...'))!

const mppx = Mppx.create({
  methods: [
    stripe.charge({
      client: stripeJs,
      createToken: async ({ amount, currency, expiresAt, metadata, networkId, paymentMethod }) => {
        const response = await fetch('/api/create-spt', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ paymentMethod, amount, currency, networkId, expiresAt, metadata }),
        })
        if (!response.ok) throw new Error('Failed to create SPT')
        return (await response.json()).spt
      },
    }),
  ],
  onChallenge: async (challenge, { createCredential }) => {
    // Extract payment method types from the challenge
    const methodDetails = challenge.request.methodDetails as
      | { paymentMethodTypes?: string[] }
      | undefined
    const paymentMethodTypes = methodDetails?.paymentMethodTypes ?? ['card']

    // Create Stripe Elements for payment collection
    const elements = stripeJs.elements({
      mode: 'payment',
      amount: Number(challenge.request.amount),
      currency: challenge.request.currency as string,
      paymentMethodTypes,
      paymentMethodCreation: 'manual',
    })

    // Mount the payment element (you'd mount this to a DOM container)
    const paymentElement = elements.create('payment')
    paymentElement.mount('#payment-element')

    // After user submits the form:
    await elements.submit()
    const { paymentMethod } = await stripeJs.createPaymentMethod({ elements })

    // Create credential with the collected payment method
    return createCredential({ paymentMethod: paymentMethod!.id })
  },
  polyfill: false,
})

const response = await mppx.fetch('/api/resource')
const receipt = Receipt.fromResponse(response)
```

## SPT creation proxy endpoint

The `createToken` callback proxies through your own server because SPT creation requires a Stripe secret key.

:::warning[Security: server-side authorization]
The server **must** derive SPT parameters (amount, currency, expiry, limits) itself rather than accepting them from the client. A thin proxy that forwards client-supplied parameters effectively delegates payment authorization to an untrusted client.

Send only:

* An authenticated session (cookie or bearer token)
* A server-known resource identifier (for example, `orderId`, `quoteId`, `toolCallId`)

The server then looks up the approved amount, currency, recipient, expiry, and rate/spend limits from its own records.
:::

```ts
// Example: server derives all SPT parameters from a known order
export async function POST(request: Request) {
  // 1. Authenticate the caller (session cookie, bearer token, etc.)
  const session = await getSession(request)
  if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 })

  // 2. Accept only a server-known resource identifier from the client
  const { orderId, paymentMethod } = await request.json()

  // 3. Look up the authorized payment parameters server-side
  const order = await db.orders.get(orderId)
  if (!order) return Response.json({ error: 'Order not found' }, { status: 404 })
  if (order.userId !== session.userId)
    return Response.json({ error: 'Forbidden' }, { status: 403 })

  // 4. Server derives SPT parameters — the client never specifies amount/currency/expiry
  const body = new URLSearchParams({
    payment_method: paymentMethod,
    'usage_limits[currency]': order.currency,
    'usage_limits[max_amount]': order.amount.toString(),
    'usage_limits[expires_at]': Math.floor(
      (Date.now() + 5 * 60 * 1000) / 1000,
    ).toString(),
  })

  const response = await fetch(
    'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens',
    {
      method: 'POST',
      headers: {
        Authorization: `Basic ${btoa(`${process.env.STRIPE_SECRET_KEY}:`)}`,
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body,
    },
  )

  if (!response.ok) {
    const error = await response.json()
    return Response.json({ error: error.error.message }, { status: 400 })
  }

  const { id: spt } = await response.json()
  return Response.json({ spt })
}
```

:::info
The `test_helpers/shared_payment/granted_tokens` endpoint is for testing. In production, SPTs are created through the agent-side `issued_tokens` API.
:::

### Client parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `client` | `StripeJs` | Optional | Stripe.js instance from `@stripe/stripe-js` — forwarded to `createToken` for use with Elements |
| `createToken` | `(params) => Promise<string>` | Required | Callback to create an SPT (proxied through a server endpoint) |
| `externalId` | `string` | Optional | Client reference ID included in the Credential payload |
| `paymentMethod` | `string` | Optional | Default Stripe payment method ID (overridden by `context.paymentMethod`) |

### `createToken` callback parameters

The `createToken` callback receives a single object with the following fields:

| Field | Type | Description |
| --- | --- | --- |
| `amount` | `string` | Payment amount in smallest currency unit |
| `challenge` | `Challenge` | The parsed Challenge from the server |
| `client` | `StripeJs \| undefined` | Stripe.js instance, if provided to `stripe.charge()` |
| `currency` | `string` | Three-letter ISO currency code |
| `expiresAt` | `number` | SPT expiration as a Unix timestamp (seconds) |
| `metadata` | `Record<string, string>` | Optional metadata from the Challenge |
| `networkId` | `string \| undefined` | Stripe Business Network profile ID |
| `paymentMethod` | `string \| undefined` | Stripe payment method ID |

## Request fields

The Challenge request includes the base charge fields plus Stripe method details.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amount` | `string` | Required | Amount in the smallest currency unit |
| `currency` | `string` | Required | ISO currency code |
| `decimals` | `number` | Required | Number of decimal places in the amount (for example, `2` for cents) |
| `description` | `string` | Optional | Human-readable payment description |
| `expires` | `string` | Optional | ISO 8601 expiration timestamp (defaults to 5 minutes) |
| `externalId` | `string` | Optional | Merchant reference ID |
| `methodDetails.metadata` | `Record<string, string>` | Optional | Metadata forwarded to Stripe |
| `methodDetails.networkId` | `string` | Required | Stripe Business Network profile ID |
| `methodDetails.paymentMethodTypes` | `string[]` | Required | Allowed Stripe payment method types |

## Credential payload

The Credential payload contains the SPT and an optional client reference ID.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `externalId` | `string` | Optional | Client reference ID |
| `spt` | `string` | Required | Shared Payment Token ID (starts with `spt_`) |

## Specification

[IETF Specification](https://paymentauth.org/draft-stripe-charge-00) — Read the full specification
