# Add a payment method

> The portal.payment_methods.setup operation: POST /api/v1/portal/me/payment_methods/setup in the REST API and the portal_payment_methods_setup tool in the MCP server.

Starts adding a card for the signed-in customer: returns the URL of a hosted page (a checkout in `setup` mode)
where the card is entered at the brand's processor (the Stripe Payment Element, or the fake processor's test
cards in development). Nothing is charged. The card is saved when the processor confirms it and becomes the
default; the page then returns to the portal's payment methods. The page works for 24 hours.

Customer portal only (`portal:write`).

## At a glance

- **REST:** `POST /api/v1/portal/me/payment_methods/setup`
- **MCP tool:** `portal_payment_methods_setup`
- **Operation ID:** `portal.payment_methods.setup`
- **Scope:** Brand
- **Permission:** `portal:write`
- **Risk tier:** `write`
- **Dry run:** Not supported
- **Idempotency:** `Idempotency-Key` required
- **Modes:** Test and live
- **Authentication:** A customer signed in to the customer portal (API keys can't call it)
- **Since:** M9

## Request

### Body

This operation takes no body.

## Response

`201 Created` with a JSON object:

| Field       | Type                            | Description                                                      |
| ----------- | ------------------------------- | ---------------------------------------------------------------- |
| `object`    | `"portal_payment_method_setup"` |                                                                  |
| `url`       | string                          | The hosted page where you add the card at the brand's processor. |
| `expiresAt` | date-time                       |                                                                  |

## Examples

### Add a card

```http
POST /api/v1/portal/me/payment_methods/setup
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
```

Response (`201`):

```json
{
  "object": "portal_payment_method_setup",
  "url": "https://ownrecord.io/c/genno/cs_01m4tx5nj8emwbyf7y9vcf48b8?key=Zm9yLWRvY3Mtb25seS4uLi4uLi4uLi4uLi4",
  "expiresAt": "2026-10-21T09:00:00.000Z"
}
```

## Errors

| Code                                                                                         | HTTP status | When                                                                                                               | What to do                                                                                                                  |
| -------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed)       | 400         | The account's ID or another field is malformed.                                                                    | Fix the fields listed in `details.issues` and send the request again.                                                       |
| [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found)     | 404         | You have no such account (another customer's reads the same).                                                      | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode.                        |
| [`customer_deleted`](https://ownrecord.io/docs/reference/errors.md#customer_deleted)         | 400         | Your account was deleted by the brand.                                                                             | Create a new customer for this buyer. Deleted customers stay readable for the records they belong to, but can't be changed. |
| [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed) | 403         | The portal session is in the other mode than the request.                                                          | Use test mode, or credentials for the target's mode. Developers need live access granted by an organization owner or admin. |
| [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied)       | 403         | The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations). | Ask an organization owner or admin for a role or scope that allows this action.                                             |

It can also return these [common errors](https://ownrecord.io/docs/reference.md#common-errors): [`api_version_unsupported`](https://ownrecord.io/docs/reference/errors.md#api_version_unsupported), [`invalid_json`](https://ownrecord.io/docs/reference/errors.md#invalid_json), [`unsupported_media_type`](https://ownrecord.io/docs/reference/errors.md#unsupported_media_type), [`authentication_required`](https://ownrecord.io/docs/reference/errors.md#authentication_required), [`api_key_invalid`](https://ownrecord.io/docs/reference/errors.md#api_key_invalid), [`scope_required`](https://ownrecord.io/docs/reference/errors.md#scope_required), [`idempotency_key_required`](https://ownrecord.io/docs/reference/errors.md#idempotency_key_required), [`idempotency_key_reused`](https://ownrecord.io/docs/reference/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://ownrecord.io/docs/reference/errors.md#idempotency_in_progress), [`dry_run_not_supported`](https://ownrecord.io/docs/reference/errors.md#dry_run_not_supported), [`rate_limited`](https://ownrecord.io/docs/reference/errors.md#rate_limited), [`internal_error`](https://ownrecord.io/docs/reference/errors.md#internal_error).
