# Top up your credits

> The portal.wallets.top_up operation: POST /api/v1/portal/me/wallets/top_up in the REST API and the portal_wallets_top_up tool in the MCP server.

Starts a top-up of the signed-in customer's credits with one of the brand's credit packs: a hosted checkout in
`topup` mode for the customer (a custom-amount pack takes `amount`). The customer pays there; tax is quoted as
for any top-up and the brand's risk rules apply. The wallet in the pack's currency is credited when the payment
is confirmed (and created then, for a first top-up); the checkout returns to the portal's wallets page.
Customer portal only (`portal:write`).

## At a glance

- **REST:** `POST /api/v1/portal/me/wallets/top_up`
- **MCP tool:** `portal_wallets_top_up`
- **Operation ID:** `portal.wallets.top_up`
- **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

A JSON object (`Content-Type: application/json`):

| Field             | Type         | Required | Description                                                                      |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------- |
| `priceId`         | `price_…` ID | Yes      | One of the brand's credit packs (`portal.credit_packs.list`).                    |
| `amount`          | object       | No       | A custom-amount pack: how much to top up, within the pack's minimum and maximum. |
| `amount.amount`   | string       | Yes      | Integer micros as a string.                                                      |
| `amount.currency` | string       | Yes      |                                                                                  |

## Response

`201 Created` with a JSON object:

| Field       | Type              | Description                                                                         |
| ----------- | ----------------- | ----------------------------------------------------------------------------------- |
| `object`    | `"portal_top_up"` |                                                                                     |
| `url`       | string            | The brand's checkout page for this top-up; it returns to the portal's wallets page. |
| `expiresAt` | date-time         |                                                                                     |

## Examples

### Buy the €25 pack

```http
POST /api/v1/portal/me/wallets/top_up
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c9"
}
```

Response (`201`):

```json
{
  "object": "portal_top_up",
  "url": "https://ownrecord.io/c/genno/cs_01m4tx5nj8emwbyf7y9vcf48cc?key=ExampleOnlyNotARealSessionKey000",
  "expiresAt": "2026-11-06T09:30:00.000Z"
}
```

## Errors

| Code                                                                                                     | HTTP status | When                                                                                                                              | What to do                                                                                                                                                                                                                  |
| -------------------------------------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed)                   | 400         | A 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         | The brand sells no such credit pack (or it was archived).                                                                         | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode.                                                                                                                        |
| [`checkout_amount_invalid`](https://ownrecord.io/docs/reference/errors.md#checkout_amount_invalid)       | 400         | A custom amount is missing, outside the pack's minimum and maximum, or given for a fixed pack.                                    | Give a quantity from 1 to 1,000 for fixed prices; for a custom-amount price give `customAmount` within the price's minimum and maximum, in whole minor units of its currency, with quantity 1. `param` names the line item. |
| [`checkout_currency_mismatch`](https://ownrecord.io/docs/reference/errors.md#checkout_currency_mismatch) | 400         | The custom amount isn't in the pack's currency.                                                                                   | Put prices of one currency in a session; create one session per currency.                                                                                                                                                   |
| [`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 subscription and wallet 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).
