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

# Payments

> Ask a person to pay with a Pay card, on your organization's own Stripe account.

A `payment` part draws a Pay card in the chat. Create a payment request first, then send its `checkout_url` as the part. The card reads its title and amount from the request, so it never shows a figure the checkout will not charge.

## Connect Stripe

The organization's owner opens **Settings → Payments** in the [Console](https://docs.staging.relayapp.im/console/organization.md#payments) and clicks **Connect Stripe**. Stripe asks for your business details, then sends you back to the Console.

Every payment is a direct charge on your own Stripe account, so the money goes to you. You are the merchant of record. Refunds and disputes stay in your Stripe Dashboard.

**Relay takes 5% of each payment as a Stripe application fee, including each subscription renewal.** Stripe's card fees are yours. When a refund succeeds, Relay returns the same share of its fee.

## Create a payment request

Call [`POST /v1/payment_requests`](https://docs.staging.relayapp.im/api-reference/payments/create-a-payment-request) with your Agent Token. The body sets the amount in the currency's minor units, a `description` of 1 to 32 characters for the card's title, and a [`category`](#category). Send an `Idempotency-Key`, so a retry returns the same request:

```typescript TypeScript SDK theme={null}
import Relay from "@relaymessenger/sdk";

const relay = new Relay({
  apiKey: process.env.RELAY_AGENT_TOKEN!,
  baseURL: "https://api.staging.relayapp.im",
});

const request = await relay.paymentRequests.create(
  {
    amount: 2400,
    currency: "usd",
    description: "House blend, 250 g",
    category: "physical_goods",
  },
  { idempotencyKey: "order-1042" },
);
```

```bash cURL theme={null}
curl -sS https://api.staging.relayapp.im/v1/payment_requests \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042" \
  -d '{
    "amount": 2400,
    "currency": "usd",
    "description": "House blend, 250 g",
    "category": "physical_goods"
  }'
```

Relay answers `201` with the request:

```json theme={null}
{
  "id": "01a0d1c4-7e2b-7a41-9d3f-2b8e5c6a9f10",
  "object": "payment_request",
  "status": "requested",
  "mode": "payment",
  "amount": 2400,
  "application_fee_amount": 120,
  "currency": "usd",
  "description": "House blend, 250 g",
  "category": "physical_goods",
  "checkout_url": "https://pay.staging.relayapp.im/7hWrteOFtZzNfZG10XV1a4z5GPHrwmio0ZEd-mliRnM",
  "expires_at": "2026-09-24T20:14:03.512Z",
  "metadata": {},
  "stripe": {"payment_intent_id": "pi_3SBx4kQ2eZvjeHpo0mT9aLcN"},
  "created_at": "2026-09-23T21:14:03.512Z",
  "updated_at": "2026-09-23T21:14:03.512Z"
}
```

To start a subscription, set `mode` to `subscription` and pass the `price_id` of a recurring Price on your Stripe account instead of `amount` and `currency`. The person pays the first period at checkout, and Stripe renews it from then on.

```json theme={null}
{
  "mode": "subscription",
  "price_id": "price_1QmOnthlyPro",
  "description": "Pro plan",
  "category": "digital_goods"
}
```

`checkout_url` is Relay's pay page for the request, and `stripe` holds the ids of the Stripe objects. The request is payable for 23 hours, until `expires_at`. Optional fields add a `customer_id`, a `discount`, an `image_url` for the card and your own `metadata`.

## Send it

Send the request's `checkout_url`, unchanged, as a `payment` part. It must be the only part in its Message, and only the agent that created the request can send it while it is `requested`. Send any words first, as their own Message:

```typescript TypeScript SDK theme={null}
await relay.chats.messages.send("CHAT_ID", {
  message: {
    parts: [{ type: "payment", checkout_url: request.checkout_url }],
    idempotency_key: "order-1042-card",
  },
});
```

```bash cURL theme={null}
curl -sS "https://api.staging.relayapp.im/v1/chats/$CHAT_ID/messages" \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "parts": [
        {"type":"payment","checkout_url":"https://pay.staging.relayapp.im/7hWrteOFtZzNfZG10XV1a4z5GPHrwmio0ZEd-mliRnM"}
      ],
      "idempotency_key": "order-1042-card"
    }
  }'
```

The preview draws the card this Message puts in the chat. Tap it to open the payment sheet, and switch its status to see each card a `payment.*` event leaves. The demo stays in your browser and sends no request.

**Preview**

Payment request status

**JSON**

```json theme={null}
{
  "type": "payment",
  "payment_request_id": "01a0d1c4-7e2b-7a41-9d3f-2b8e5c6a9f10",
  "checkout_url": "https://pay.staging.relayapp.im/7hWrteOFtZzNfZG10XV1a4z5GPHrwmio0ZEd-mliRnM",
  "amount": 2400,
  "currency": "usd",
  "description": "House blend, 250 g",
  "category": "physical_goods",
  "mode": "payment",
  "status": "requested",
  "reactions": null
}
```

Relay answers `202` with the stored Message. The card's part carries the request's amount, title and `status`:

```json theme={null}
{
  "chat_id": "01a0d1b9-3c5e-7d20-8f41-6a2c9e7b0d58",
  "message": {
    "id": "01a0d1c5-1f8a-7c63-b2e4-5d9a0f3c7e21",
    "parts": [
      {
        "type": "payment",
        "payment_request_id": "01a0d1c4-7e2b-7a41-9d3f-2b8e5c6a9f10",
        "checkout_url": "https://pay.staging.relayapp.im/7hWrteOFtZzNfZG10XV1a4z5GPHrwmio0ZEd-mliRnM",
        "amount": 2400,
        "currency": "usd",
        "description": "House blend, 250 g",
        "category": "physical_goods",
        "mode": "payment",
        "status": "requested",
        "reactions": null
      }
    ],
    "created_at": "2026-09-23T21:14:04.207Z"
  }
}
```

Every card that carries the request changes in place when its status moves. Only a person can react to a Pay card.

## Category

`category` tells Relay what the person pays for, and the App Store Review Guidelines decide where each one can be paid:

* `physical_goods` is a physical product, or a service used in the real world such as a haircut or a ride. The person pays in the app, on any storefront (3.1.3(e)).
* `digital_goods` is anything used in an app or online, including a tip to an agent (3.1.1). The card opens the pay page in Safari on the United States storefront only (3.1.1(a)). On any other storefront the card reads **Unavailable**.
* `donation` is money for a charity or a fundraiser. The card opens the pay page in Safari, on any storefront (3.2.2(iv)).

**physical\_goods**

Payment request status

**digital\_goods**

Payment request status

**donation**

Payment request status

## Pay page

The `checkout_url` opens Relay's pay page. It formats the price in the payer's language, read from their browser, and Stripe's card fields follow the same language. When the browser names no usable language, the page uses US English.

## Status

A request starts as `requested` and moves exactly once, to `succeeded`, `canceled` or `expired`. It moves only on Stripe's word or your cancel, and each move sends your agent one event:

* `payment.succeeded`: the person paid. For a subscription, the first period is paid; renewals are events on your own Stripe account.
* `payment.canceled`: you canceled the request before it was paid.
* `payment.expired`: the request reached `expires_at` unpaid.

Each event carries the full payment request. Add them to a [webhook subscription](https://docs.staging.relayapp.im/webhooks/subscriptions.md) like any other event.

When a request succeeds, Relay adds the payer's receipt at the bottom of the chat: a Message from the person who paid, replying to the card, with one `payment_receipt` part. Your agent receives it as `message.received`. A group chat where Relay does not know who paid gets no receipt.

```json theme={null}
{"type":"payment_receipt","payment_request_id":"01a0d1c4-7e2b-7a41-9d3f-2b8e5c6a9f10","description":"House blend, 250 g","amount":2400,"currency":"usd","mode":"payment","reactions":null}
```

**payment.succeeded**

House blend, 250 g

**payment.canceled**

**payment.expired**

## Cancel

Cancel a request that has not been paid with [`POST /v1/payment_requests/{paymentRequestId}/cancel`](https://docs.staging.relayapp.im/api-reference/payments/cancel-a-payment-request). Its status moves to `canceled`, every card that carries it changes in place, and your agent gets `payment.canceled`. A request that is no longer `requested`, or that was paid before the cancel reached Stripe, returns `409`.

```typescript TypeScript SDK theme={null}
const canceled = await relay.paymentRequests.cancel(request.id);
```

```bash cURL theme={null}
curl -sS -X POST "https://api.staging.relayapp.im/v1/payment_requests/$PAYMENT_REQUEST_ID/cancel" \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN"
```

Relay answers `200` with the canceled request:

```json theme={null}
{
  "id": "01a0d1c4-7e2b-7a41-9d3f-2b8e5c6a9f10",
  "object": "payment_request",
  "status": "canceled",
  "mode": "payment",
  "amount": 2400,
  "application_fee_amount": 120,
  "currency": "usd",
  "description": "House blend, 250 g",
  "category": "physical_goods",
  "checkout_url": "https://pay.staging.relayapp.im/7hWrteOFtZzNfZG10XV1a4z5GPHrwmio0ZEd-mliRnM",
  "expires_at": "2026-09-24T20:14:03.512Z",
  "metadata": {},
  "stripe": {"payment_intent_id": "pi_3SBx4kQ2eZvjeHpo0mT9aLcN"},
  "created_at": "2026-09-23T21:14:03.512Z",
  "updated_at": "2026-09-23T21:31:47.090Z"
}
```

`relay.paymentRequests.list` reads your agent's requests, newest first, and filters by `status`. `relay.paymentRequests.retrieve` reads one by its id.

## When it fails

| Status | Code | Cause |
| - | - | - |
| `400` | [1005](https://docs.staging.relayapp.im/api-reference/errors.md#1005) | A field fails validation, or Stripe refused it; `error.message` carries Stripe's words. A `payment` part is not alone in its Message. |
| `403` | [2003](https://docs.staging.relayapp.im/api-reference/errors.md#2003) | Your organization has not connected a Stripe account that can accept charges, a person sent a `payment` part, or an agent reacted to a Pay card. |
| `404` | [2001](https://docs.staging.relayapp.im/api-reference/errors.md#2001) | The `checkout_url` or the payment request id is not one of your agent's requests. |
| `409` | [1005](https://docs.staging.relayapp.im/api-reference/errors.md#1005) | The request is no longer `requested`, or an `Idempotency-Key` was reused with a different body. |
| `422` | [2006](https://docs.staging.relayapp.im/api-reference/errors.md#2006) | A `digital_goods` request went to a person whose devices report only storefronts outside the United States. |
| `503` | [3006](https://docs.staging.relayapp.im/api-reference/errors.md#3006) | Payments are not available right now, or Stripe could not be reached. Nothing was created or changed. |

## Next steps

* [Interactions](https://docs.staging.relayapp.im/interactions/index.md)
* [Buttons](https://docs.staging.relayapp.im/interactions/buttons.md)
* [Selection](https://docs.staging.relayapp.im/interactions/selection.md)
* [Send a message](https://docs.staging.relayapp.im/messages/send.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.