# Portal tools

> MCP tools for Portal: portal_account_get, portal_brand_get, portal_credit_balance_get, portal_credit_notes_list, portal_credit_packs_list, portal_export_get, portal_invoices_get, portal_invoices_list, portal_invoices_pay, portal_login_links_request, portal_payment_methods_delete, portal_payment_methods_list, portal_payment_methods_set_default, portal_payment_methods_setup, portal_payment_methods_use_for_subscriptions, portal_sessions_create, portal_sessions_redeem, portal_sessions_revoke, portal_subscriptions_apply_promotion_code, portal_subscriptions_cancel, portal_subscriptions_get, portal_subscriptions_list, portal_subscriptions_preview_change, portal_subscriptions_reactivate, portal_subscriptions_set_payment_method, portal_subscriptions_update, portal_tax_details_update, portal_tax_ids_add, portal_tax_ids_delete, portal_wallets_auto_reload_update, portal_wallets_list, portal_wallets_top_up, portal_wallets_transactions_list.

- `portal_account_get`: Get your customer account
- `portal_brand_get`: Get a brand's customer portal
- `portal_credit_balance_get`: Get your credit balance
- `portal_credit_notes_list`: List your credit notes
- `portal_credit_packs_list`: List the credit packs you can buy
- `portal_export_get`: Export your data
- `portal_invoices_get`: Get one of your invoices
- `portal_invoices_list`: List your invoices
- `portal_invoices_pay`: Pay one of your invoices by card
- `portal_login_links_request`: Email a customer a portal sign-in link
- `portal_payment_methods_delete`: Remove a payment method
- `portal_payment_methods_list`: List your payment methods
- `portal_payment_methods_set_default`: Make a payment method your default
- `portal_payment_methods_setup`: Add a payment method
- `portal_payment_methods_use_for_subscriptions`: Charge your default card for all your subscriptions
- `portal_sessions_create`: Generate a customer portal link
- `portal_sessions_redeem`: Sign in to the customer portal with a link
- `portal_sessions_revoke`: Sign out of the customer portal
- `portal_subscriptions_apply_promotion_code`: Apply a promotion code to your subscription
- `portal_subscriptions_cancel`: Cancel your subscription
- `portal_subscriptions_get`: Get one of your subscriptions
- `portal_subscriptions_list`: List your subscriptions
- `portal_subscriptions_preview_change`: Preview a plan or quantity change
- `portal_subscriptions_reactivate`: Keep your subscription
- `portal_subscriptions_set_payment_method`: Choose the card a subscription charges
- `portal_subscriptions_update`: Switch plan or change the quantity
- `portal_tax_details_update`: Update your billing details
- `portal_tax_ids_add`: Add a tax ID to your account
- `portal_tax_ids_delete`: Remove a tax ID from your account
- `portal_wallets_auto_reload_update`: Set up auto-reload
- `portal_wallets_list`: List your wallets and balances
- `portal_wallets_top_up`: Top up your credits
- `portal_wallets_transactions_list`: List your wallet's history

## `portal_account_get`

**Get your customer account.** Operation [`portal.account.get`](https://ownrecord.io/docs/reference/portal-account-get.md) · Brand level · permission `portal:read` · risk tier `read` · since M9

Returns the signed-in customer's own account with the brand: email, name, business name, billing address and
tax IDs with their latest check, and when they last signed in to the portal.

Customer portal only: the caller is the customer signed in to the brand's portal (`portal:read`).

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument    | Type         | Required | Description                                                                                                                              |
| ----------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `entity_id` | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                 |
| `brand_id`  | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand. |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field or pagination value is malformed.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**Your account**

Arguments:

```json
{
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz"
}
```

Result `output`:

```json
{
  "object": "portal_account",
  "customerId": "cus_01m4tx5nj8emwbyf7y9vcf48b1",
  "livemode": false,
  "email": "ana@example.com",
  "name": "Ana Novak",
  "businessName": "Alpen Apps GmbH",
  "type": "business",
  "locale": null,
  "billingAddress": {
    "line1": "Ring 1",
    "line2": null,
    "city": "Wien",
    "postalCode": "1010",
    "region": null,
    "country": "AT"
  },
  "taxIds": [
    {
      "id": "ATU00000000",
      "type": "eu_vat",
      "value": "ATU00000000",
      "country": "AT",
      "status": "valid",
      "checkedAt": "2026-10-20T09:00:00.000Z"
    }
  ],
  "portalLastLoginAt": "2026-10-20T09:00:00.000Z"
}
```

## `portal_brand_get`

**Get a brand's customer portal.** Operation [`portal.brand.get`](https://ownrecord.io/docs/reference/portal-brand-get.md) · Brand level · permission `portal:read` · risk tier `read` · since M9

Returns what a brand's customer portal shows before sign-in: the brand's name, logo, color and support links,
the seller (legal name, address and VAT ID), and the mode the portal's sign-in works in (`livemode`: test mode
for a brand that is still test-only, live mode once it is live).

Public: no API key needed. Draft and suspended brands have no portal (`resource_not_found`).

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument    | Type         | Required | Description                                                                                                                              |
| ----------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `brandSlug` | string       | yes      | The brand's slug, as in `/portal/{brandSlug}`. (1–64 characters)                                                                         |
| `entity_id` | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                 |
| `brand_id`  | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand. |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): `brandSlug` isn't a slug.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): No brand with this slug has a customer portal (draft and suspended brands have none).
- [`rate_limited`](https://ownrecord.io/docs/reference/errors.md#rate_limited): Too many requests from this address or client IP; wait `retry_after_ms`.

### Example

**Genno's portal (test-only brand)**

Arguments:

```json
{
  "brandSlug": "genno",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz"
}
```

Result `output`:

```json
{
  "object": "portal_brand",
  "slug": "genno",
  "name": "Genno",
  "livemode": false,
  "logoUrl": null,
  "primaryColor": "#5b3df5",
  "supportEmail": "help@genno.example",
  "supportUrl": null,
  "termsUrl": "https://genno.example/terms",
  "privacyUrl": "https://genno.example/privacy",
  "seller": {
    "legalName": "Etna Labs AG",
    "address": "Dammstrasse 16, 6300 Zug, Switzerland",
    "taxId": "CHE-181.927.786 MWST"
  },
  "locales": ["en", "hr"]
}
```

## `portal_credit_balance_get`

**Get your credit balance.** Operation [`portal.credit_balance.get`](https://ownrecord.io/docs/reference/portal-credit-balance-get.md) · Brand level · permission `portal:read` · risk tier `read` · since M9

Returns the signed-in customer's invoice credit balance in every currency they have one (proration credits,
goodwill credits and overpayments), which is applied automatically to their next invoices, and its latest
changes. It isn't a usage wallet. Read-only; customer portal only (`portal:read`).

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument    | Type         | Required | Description                                                                                                                              |
| ----------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `entity_id` | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                 |
| `brand_id`  | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand. |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field or pagination value is malformed.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**€50 of credit left**

Arguments:

```json
{
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz"
}
```

Result `output`:

```json
{
  "object": "portal_credit_balance",
  "balances": [
    {
      "amount": "50000000",
      "currency": "EUR"
    }
  ],
  "changes": [
    {
      "change": {
        "amount": "-200000000",
        "currency": "EUR"
      },
      "reason": "invoice_applied",
      "invoiceId": "inv_01m4tx5nj8emwbyf7y9vcf48b3",
      "effectiveAt": "2026-10-20T09:00:00.000Z"
    }
  ]
}
```

## `portal_credit_notes_list`

**List your credit notes.** Operation [`portal.credit_notes.list`](https://ownrecord.io/docs/reference/portal-credit-notes-list.md) · Brand level · permission `portal:read` · risk tier `read` · since M9

Lists the signed-in customer's credit notes (issued for refunds), newest first, each with the invoice it
corrects and a download link of its PDF (`pdfPath`; null for a moment after issue).

Paginate with `limit` (default 20) and `starting_after`. Customer portal only (`portal:read`).

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument         | Type         | Required | Description                                                                                                                              |
| ---------------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `limit`          | integer      | no       | Page size, 1 to 100 (default 20). (1–100)                                                                                                |
| `starting_after` | string       | no       | The `next_cursor` of the previous page. (1–64 characters)                                                                                |
| `entity_id`      | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                 |
| `brand_id`       | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand. |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field or pagination value is malformed.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**Your credit notes**

Arguments:

```json
{
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz"
}
```

Result `output`:

```json
{
  "object": "list",
  "data": [
    {
      "id": "cn_01m4tx5nj8emwbyf7y9vcf48b4",
      "object": "portal_credit_note",
      "number": "GEN-CN-2026-000003",
      "issueDate": "2026-10-22",
      "currency": "EUR",
      "total": {
        "amount": "30000000",
        "currency": "EUR"
      },
      "invoiceId": "inv_01m4tx5nj8emwbyf7y9vcf48b3",
      "invoiceNumber": "GEN-2026-000042",
      "pdfPath": "/api/v1/credit_notes/cn_01m4tx5nj8emwbyf7y9vcf48b4/pdf?token=eyJ…"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

## `portal_credit_packs_list`

**List the credit packs you can buy.** Operation [`portal.credit_packs.list`](https://ownrecord.io/docs/reference/portal-credit-packs-list.md) · Brand level · permission `portal:read` · risk tier `read` · since M9

Lists the brand's active credit packs in the portal's mode, newest first: fixed packs with their price, credits,
bonus credits and expiry, and custom-amount packs with their minimum, maximum and suggested amount. Top up with
`portal.wallets.top_up`. Customer portal only (`portal:read`).

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument         | Type         | Required | Description                                                                                                                              |
| ---------------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `limit`          | integer      | no       | Page size, 1 to 100 (default 20). (1–100)                                                                                                |
| `starting_after` | string       | no       | The `next_cursor` of the previous page. (1–64 characters)                                                                                |
| `entity_id`      | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                 |
| `brand_id`       | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand. |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).

### Example

**A €25 pack and any amount**

Arguments:

```json
{
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz"
}
```

Result `output`:

```json
{
  "object": "list",
  "data": [
    {
      "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c9",
      "object": "portal_credit_pack",
      "productName": "Genno credits",
      "name": "€25 credits",
      "currency": "EUR",
      "amount": {
        "amount": "25000000",
        "currency": "EUR"
      },
      "customAmount": null,
      "credits": {
        "amount": "25000000",
        "currency": "EUR"
      },
      "bonusCredits": null,
      "expiresAfterDays": null,
      "taxBehavior": "inclusive"
    },
    {
      "priceId": "price_01m4tx5nj8emwbyf7y9vcf48ca",
      "object": "portal_credit_pack",
      "productName": "Genno credits",
      "name": "Any amount",
      "currency": "EUR",
      "amount": null,
      "customAmount": {
        "min": {
          "amount": "5000000",
          "currency": "EUR"
        },
        "max": {
          "amount": "5000000000",
          "currency": "EUR"
        },
        "preset": {
          "amount": "50000000",
          "currency": "EUR"
        }
      },
      "credits": null,
      "bonusCredits": null,
      "expiresAfterDays": null,
      "taxBehavior": "inclusive"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

## `portal_export_get`

**Export your data.** Operation [`portal.export.get`](https://ownrecord.io/docs/reference/portal-export-get.md) · Brand level · permission `portal:read` · risk tier `read` · since M9

Returns everything the brand holds about the signed-in customer as one file: the account with its tax IDs,
location evidence, invoices and credit notes with their lines, payments, payment methods, subscriptions, wallets
and the credit balance (at most 1,000 records of each type).

- `json`: one document grouped by record type.
- `csv`: one row per value (`record_type,record_id,field,value`), nested fields as dotted paths.

The portal offers it as a download. Customer portal only (`portal:read`).

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument    | Type                 | Required | Description                                                                                                                              |
| ----------- | -------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `format`    | one of `json`, `csv` | no       | `json` (default) or `csv`. (default `"json"`)                                                                                            |
| `entity_id` | ID (`ent_…`)         | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                 |
| `brand_id`  | ID (`brd_…`)         | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand. |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field or pagination value is malformed.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**CSV export**

Arguments:

```json
{
  "format": "csv",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz"
}
```

Result `output`:

```json
{
  "object": "portal_export",
  "format": "csv",
  "filename": "genno-cus_01m4tx5nj8emwbyf7y9vcf48b1-2026-10-20.csv",
  "contentType": "text/csv; charset=utf-8",
  "content": "record_type,record_id,field,value\r\ncustomer,cus_01m4tx5nj8emwbyf7y9vcf48b1,email,ana@example.com\r\n"
}
```

## `portal_invoices_get`

**Get one of your invoices.** Operation [`portal.invoices.get`](https://ownrecord.io/docs/reference/portal-invoices-get.md) · Brand level · permission `portal:read` · risk tier `read` · since M9

Returns one of the signed-in customer's issued invoices: its lines, amounts, due date, the PDF link, the bank
transfer details of a `send_invoice` invoice (the invoice number is the payment reference) and whether it can be
paid by card now (`card`). Another customer's invoice reads as missing.

Customer portal only (`portal:read`).

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument    | Type         | Required | Description                                                                                                                              |
| ----------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `invoiceId` | ID (`inv_…`) | yes      | One of your invoices (`inv_…`).                                                                                                          |
| `entity_id` | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                 |
| `brand_id`  | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand. |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): The invoice's ID or another field is malformed.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such invoice (another customer's reads the same).
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**An open invoice**

Arguments:

```json
{
  "invoiceId": "inv_01m4tx5nj8emwbyf7y9vcf48b3",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz"
}
```

Result `output`:

```json
{
  "id": "inv_01m4tx5nj8emwbyf7y9vcf48b3",
  "object": "portal_invoice",
  "number": "GEN-2026-000042",
  "kind": "invoice",
  "status": "open",
  "issueDate": "2026-10-20",
  "dueDate": "2026-11-03",
  "currency": "EUR",
  "total": {
    "amount": "120000000",
    "currency": "EUR"
  },
  "creditApplied": null,
  "amountDue": {
    "amount": "120000000",
    "currency": "EUR"
  },
  "paidAt": null,
  "payable": true,
  "pdfPath": "/api/v1/invoices/inv_01m4tx5nj8emwbyf7y9vcf48b3/pdf?token=eyJ…",
  "sellerName": "Etna Labs AG",
  "lines": [
    {
      "description": "Genno Pro (monthly)",
      "quantity": 1,
      "amount": {
        "amount": "120000000",
        "currency": "EUR"
      }
    }
  ],
  "bankTransfer": {
    "accountHolder": "Etna Labs AG",
    "iban": "CH9300762011623852957",
    "bic": "EXMPCHZZXXX",
    "bankName": "Example Bank",
    "reference": "GEN-2026-000042"
  },
  "card": {
    "available": true,
    "unavailableReason": null,
    "component": "fake",
    "publishableKey": null
  }
}
```

## `portal_invoices_list`

**List your invoices.** Operation [`portal.invoices.list`](https://ownrecord.io/docs/reference/portal-invoices-list.md) · Brand level · permission `portal:read` · risk tier `read` · since M9

Lists the signed-in customer's issued invoices and receipts, newest first, with the amount due and a download
link of each PDF (`pdfPath`; null for a moment after issue, while the PDF is prepared). Open invoices with
something due are `payable` by card (`portal.invoices.pay`). Drafts aren't listed.

Paginate with `limit` (default 20) and `starting_after`. Customer portal only (`portal:read`).

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument         | Type         | Required | Description                                                                                                                              |
| ---------------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `limit`          | integer      | no       | Page size, 1 to 100 (default 20). (1–100)                                                                                                |
| `starting_after` | string       | no       | The `next_cursor` of the previous page. (1–64 characters)                                                                                |
| `entity_id`      | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                 |
| `brand_id`       | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand. |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field or pagination value is malformed.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**Your latest invoices**

Arguments:

```json
{
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz"
}
```

Result `output`:

```json
{
  "object": "list",
  "data": [
    {
      "id": "inv_01m4tx5nj8emwbyf7y9vcf48b3",
      "object": "portal_invoice",
      "number": "GEN-2026-000042",
      "kind": "invoice",
      "status": "open",
      "issueDate": "2026-10-20",
      "dueDate": "2026-11-03",
      "currency": "EUR",
      "total": {
        "amount": "120000000",
        "currency": "EUR"
      },
      "creditApplied": null,
      "amountDue": {
        "amount": "120000000",
        "currency": "EUR"
      },
      "paidAt": null,
      "payable": true,
      "pdfPath": "/api/v1/invoices/inv_01m4tx5nj8emwbyf7y9vcf48b3/pdf?token=eyJ…"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

## `portal_invoices_pay`

**Pay one of your invoices by card.** Operation [`portal.invoices.pay`](https://ownrecord.io/docs/reference/portal-invoices-pay.md) · Brand level · permission `portal:write` · risk tier `financial` · since M9

Starts a card payment of one of the signed-in customer's open invoices, for its amount due, at the brand's
processor: a one-off payment (no card is saved). `expectedAmount` is the amount due the customer saw; if it
changed meanwhile, the payment is refused.

- **fake processor:** the payment is confirmed here with the chosen test card (`paymentMethod`): `succeeded`,
  `processing`, `requires_action` (pay again to pass the fake 3-D Secure challenge) or a decline;
- **Stripe:** the answer is `client_confirm` with the Payment Element's `clientSecret`; the browser confirms;
- **Mollie:** the answer is `redirect` with `redirectUrl`, Mollie's payment page; the customer comes back to the
  invoice's hosted page.

The invoice is marked paid when the processor's webhook confirms the payment: read it again with
`portal.invoices.get`. Customer portal only (`portal:write`).

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: true`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invoiceId`       | ID (`inv_…`) | yes      | One of your invoices (`inv_…`).                                                                                                                                                                          |
| `expectedAmount`  | object       | yes      | The amount due you saw; if it changed meanwhile, the payment is refused (`invoice_payment_unavailable`).                                                                                                 |
| `paymentMethod`   | string       | no       | Fake processor only (development, CI): the test card, e.g. `pm_fake_visa`. (1–255 characters)                                                                                                            |
| `entity_id`       | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |
| `dry_run`         | boolean      | no       | true: check permissions and rules and compute the result, then roll everything back. Nothing is saved. (default `false`)                                                                                 |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** supported. `dry_run: true` checks permissions and rules and computes the result, then rolls everything back.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): The invoice's ID or another field is malformed.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such invoice (another customer's reads the same).
- [`invoice_payment_unavailable`](https://ownrecord.io/docs/reference/errors.md#invoice_payment_unavailable): The invoice is paid or voided, has nothing due, its amount due changed, or the brand has no processor.
- [`processor_payment_declined`](https://ownrecord.io/docs/reference/errors.md#processor_payment_declined): The card was declined; try another card or a bank transfer.
- [`processor_request_failed`](https://ownrecord.io/docs/reference/errors.md#processor_request_failed): The processor couldn't create or confirm the payment; try again.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**Pay with a fake test card**

Arguments:

```json
{
  "invoiceId": "inv_01m4tx5nj8emwbyf7y9vcf48b3",
  "expectedAmount": {
    "amount": "1200000000",
    "currency": "EUR"
  },
  "paymentMethod": "pm_fake_visa",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "object": "hosted_invoice_payment",
  "invoiceId": "inv_01m4tx5nj8emwbyf7y9vcf48b3",
  "outcome": "succeeded",
  "amount": {
    "amount": "1200000000",
    "currency": "EUR"
  },
  "clientSecret": null,
  "publishableKey": null,
  "redirectUrl": null
}
```

## `portal_login_links_request`

**Email a customer a portal sign-in link.** Operation [`portal.login_links.request`](https://ownrecord.io/docs/reference/portal-login-links-request.md) · Brand level · permission `portal:write` · risk tier `write` · since M9

Emails a one-time sign-in link for the brand's customer portal to the address, if it belongs to a customer of
the brand in the portal's mode. The link works once, for 15 minutes; only a hash of its token is stored.

The answer is the same whether or not the address belongs to a customer, so nobody can find out who the
brand's customers are. Requests are limited per address (5 per 15 minutes per brand) and per client IP (20 per
hour), whether or not the address is known (`rate_limited`).

Public: the portal's sign-in page calls it, no API key needed.

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: true`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `brandSlug`       | string       | yes      | The brand's slug, as in `/portal/{brandSlug}`. (1–64 characters)                                                                                                                                         |
| `email`           | string       | yes      | The address the customer uses with the brand. (at most 254 characters)                                                                                                                                   |
| `entity_id`       | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): `email` isn't an email address, or `brandSlug` isn't a slug.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): No brand with this slug has a customer portal (draft and suspended brands have none).
- [`rate_limited`](https://ownrecord.io/docs/reference/errors.md#rate_limited): Too many sign-in links were asked for this address (5 per 15 minutes per brand) or from this client IP (20 per hour); wait `retry_after_ms`.

### Example

**Ask for a sign-in link**

Arguments:

```json
{
  "brandSlug": "genno",
  "email": "ana@example.com",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "object": "portal_login_link_request",
  "email": "ana@example.com",
  "message": "If ana@example.com belongs to an account with Genno, we've sent it a sign-in link. The link works once, for 15 minutes."
}
```

## `portal_payment_methods_delete`

**Remove a payment method.** Operation [`portal.payment_methods.delete`](https://ownrecord.io/docs/reference/portal-payment-methods-delete.md) · Brand level · permission `portal:write` · risk tier `write` · since M9

Removes one of the signed-in customer's saved payment methods: it is never charged again and stops being the
default (subscriptions then charge the new default, if any). The processor detaches it too, right after. The
record stays for the history of the payments made with it. Customer portal only (`portal:write`).

Annotations: `readOnlyHint: false` · `destructiveHint: true` · `idempotentHint: true` · `openWorldHint: true`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paymentMethodId` | ID (`pm_…`)  | yes      | One of your saved payment methods (`pm_…`).                                                                                                                                                              |
| `entity_id`       | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): The payment method's ID or another field is malformed.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such payment method (another customer's reads the same).
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): Your payment methods changed at the same moment; retry.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**Remove an old card**

Arguments:

```json
{
  "paymentMethodId": "pm_01m4tx5nj8emwbyf7y9vcf48b6",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "id": "pm_01m4tx5nj8emwbyf7y9vcf48b6",
  "object": "portal_payment_method",
  "type": "card",
  "card": {
    "brand": "visa",
    "last4": "4242",
    "expMonth": 12,
    "expYear": 2029,
    "country": "DE"
  },
  "isDefault": false,
  "createdAt": "2026-10-20T09:00:00.000Z"
}
```

## `portal_payment_methods_list`

**List your payment methods.** Operation [`portal.payment_methods.list`](https://ownrecord.io/docs/reference/portal-payment-methods-list.md) · Brand level · permission `portal:read` · risk tier `read` · since M9

Lists the cards (and other methods) the signed-in customer saved with the brand, newest first: brand, last four
digits, expiry and country, and which one is the default that subscriptions charge. Removed methods aren't
listed. Paginate with `limit` (default 20) and `starting_after`. Customer portal only (`portal:read`).

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument         | Type         | Required | Description                                                                                                                              |
| ---------------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `limit`          | integer      | no       | Page size, 1 to 100 (default 20). (1–100)                                                                                                |
| `starting_after` | string       | no       | The `next_cursor` of the previous page. (1–64 characters)                                                                                |
| `entity_id`      | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                 |
| `brand_id`       | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand. |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field or pagination value is malformed.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**Two saved cards**

Arguments:

```json
{
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz"
}
```

Result `output`:

```json
{
  "object": "list",
  "data": [
    {
      "id": "pm_01m4tx5nj8emwbyf7y9vcf48b5",
      "object": "portal_payment_method",
      "type": "card",
      "card": {
        "brand": "visa",
        "last4": "4242",
        "expMonth": 12,
        "expYear": 2029,
        "country": "DE"
      },
      "isDefault": true,
      "createdAt": "2026-10-20T09:00:00.000Z"
    },
    {
      "id": "pm_01m4tx5nj8emwbyf7y9vcf48b6",
      "object": "portal_payment_method",
      "type": "card",
      "card": {
        "brand": "mastercard",
        "last4": "4444",
        "expMonth": 3,
        "expYear": 2028,
        "country": "AT"
      },
      "isDefault": false,
      "createdAt": "2026-10-20T09:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

## `portal_payment_methods_set_default`

**Make a payment method your default.** Operation [`portal.payment_methods.set_default`](https://ownrecord.io/docs/reference/portal-payment-methods-set-default.md) · Brand level · permission `portal:write` · risk tier `write` · since M9

Makes one of the signed-in customer's saved payment methods the default: subscriptions that don't name their own
method charge it from now on, and a past-due invoice is retried with it at once. Customer portal only
(`portal:write`).

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paymentMethodId` | ID (`pm_…`)  | yes      | One of your saved payment methods (`pm_…`).                                                                                                                                                              |
| `entity_id`       | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): The payment method's ID or another field is malformed.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such payment method (another customer's reads the same).
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): Your payment methods changed at the same moment; retry.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**Use the new card**

Arguments:

```json
{
  "paymentMethodId": "pm_01m4tx5nj8emwbyf7y9vcf48b6",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "id": "pm_01m4tx5nj8emwbyf7y9vcf48b6",
  "object": "portal_payment_method",
  "type": "card",
  "card": {
    "brand": "visa",
    "last4": "4242",
    "expMonth": 12,
    "expYear": 2029,
    "country": "DE"
  },
  "isDefault": true,
  "createdAt": "2026-10-20T09:00:00.000Z"
}
```

## `portal_payment_methods_setup`

**Add a payment method.** Operation [`portal.payment_methods.setup`](https://ownrecord.io/docs/reference/portal-payment-methods-setup.md) · Brand level · permission `portal:write` · risk tier `write` · since M9

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`).

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: false`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `entity_id`       | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): The account's ID or another field is malformed.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such account (another customer's reads the same).
- [`customer_deleted`](https://ownrecord.io/docs/reference/errors.md#customer_deleted): Your account was deleted by the brand.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**Add a card**

Arguments:

```json
{
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

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

## `portal_payment_methods_use_for_subscriptions`

**Charge your default card for all your subscriptions.** Operation [`portal.payment_methods.use_for_subscriptions`](https://ownrecord.io/docs/reference/portal-payment-methods-use-for-subscriptions.md) · Brand level · permission `portal:write` · risk tier `write` · since M13

Switches every running, card-charged subscription of the signed-in customer that charges another card to their
default card (`paymentMethodId`, the default card they saw): from then on each charges the default card, also
after the default changes again. A past-due invoice is retried with it at once. Subscriptions paid by bank
transfer, ended ones and those the brand's portal settings don't let you change are left as they are. Returns the
subscriptions it switched (none when every subscription already charges the default card). Customer portal only
(`portal:write`).

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: true`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paymentMethodId` | ID (`pm_…`)  | yes      | Your default card (`pm_…`), as you saw it.                                                                                                                                                               |
| `entity_id`       | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): The payment method's ID is malformed.
- [`subscription_payment_method_invalid`](https://ownrecord.io/docs/reference/errors.md#subscription_payment_method_invalid): The card isn't your default card (any more), or it was removed: reload your payment methods.
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): One of your subscriptions changed at the same moment; retry.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).

### Example

**Use the card you just added for your subscriptions**

Arguments:

```json
{
  "paymentMethodId": "pm_01m4tx5nj8emwbyf7y9vcf48cb",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "object": "list",
  "data": [
    {
      "id": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
      "object": "portal_subscription",
      "status": "active",
      "currency": "EUR",
      "items": [
        {
          "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c2",
          "productId": "prod_01m4tx5nj8emwbyf7y9vcf48c4",
          "productName": "Genno Plus",
          "priceName": "Plus, monthly",
          "interval": {
            "interval": "month",
            "intervalCount": 1
          },
          "usageType": "licensed",
          "unitAmount": {
            "amount": "9000000",
            "currency": "EUR"
          },
          "taxBehavior": "inclusive",
          "id": "si_01m4tx5nj8emwbyf7y9vcf48c1",
          "quantity": 1,
          "amount": {
            "amount": "9000000",
            "currency": "EUR"
          }
        }
      ],
      "collectionMethod": "charge_automatically",
      "paymentMethodId": null,
      "currentPeriodStart": "2026-10-20T09:00:00.000Z",
      "currentPeriodEnd": "2026-11-20T10:00:00.000Z",
      "trialEnd": null,
      "cancelAtPeriodEnd": false,
      "cancelAt": null,
      "canceledAt": null,
      "endedAt": null,
      "cancellation": null,
      "discounts": [],
      "scheduledChange": null,
      "actions": {
        "switch_plan": true,
        "change_quantity": false,
        "apply_promotion_code": true,
        "cancel": true,
        "reactivate": false,
        "change_payment_method": true
      },
      "cancelBehavior": "period_end",
      "collectCancelReason": true,
      "version": 5,
      "createdAt": "2026-09-20T09:00:00.000Z"
    }
  ]
}
```

## `portal_sessions_create`

**Generate a customer portal link.** Operation [`portal.sessions.create`](https://ownrecord.io/docs/reference/portal-sessions-create.md) · Brand level · permission `customers:write` · risk tier `security` · since M9

Makes a one-time sign-in link to the brand's customer portal for one customer, in the request's mode: send the
customer there (for example from your own app's billing page). The link works once, for 15 minutes; the
customer's session then lasts 7 days.

Generating a link doesn't sign you in: whoever opens it is signed in as the customer, so give it only to them.
Generating it is recorded in the audit log with who asked. Only a hash of the link's token is stored. Needs
`customers:write`.

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: false`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customerId`      | ID (`cus_…`) | yes      | The customer the link signs in (`cus_…`).                                                                                                                                                                |
| `entity_id`       | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): `customerId` isn't a customer ID (`cus_…`).
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): This brand has no such customer in this mode (customers belong to one brand and one mode).
- [`customer_deleted`](https://ownrecord.io/docs/reference/errors.md#customer_deleted): The customer was deleted.
- [`scope_required`](https://ownrecord.io/docs/reference/errors.md#scope_required): The request names no brand (an org or entity key without `Ownrecord-Brand`).
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): You have no live-mode access, so you can't act on live customers.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): You can't change this brand's customers (`customers:write`: owners, admins and support).

### Example

**A sign-in link for a customer**

Arguments:

```json
{
  "customerId": "cus_01m4tx5nj8emwbyf7y9vcf48b1",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "object": "portal_session_link",
  "id": "psess_01m4tx5nj8emwbyf7y9vcf48b2",
  "customerId": "cus_01m4tx5nj8emwbyf7y9vcf48b1",
  "livemode": false,
  "url": "https://ownrecord.io/portal/genno/login/verify?session=psess_01m4tx5nj8emwbyf7y9vcf48b2&token=Q2hhbmdlTWVJbkV4YW1wbGVzT25seS4uLi4uLi4uLi4",
  "expiresAt": "2026-10-20T09:15:00.000Z"
}
```

## `portal_sessions_redeem`

**Sign in to the customer portal with a link.** Operation [`portal.sessions.redeem`](https://ownrecord.io/docs/reference/portal-sessions-redeem.md) · Brand level · permission `portal:write` · risk tier `security` · since M9

Redeems a one-time sign-in link (from the portal's email, or an operator's `portal.sessions.create`): the link's
portal session becomes an active session for 7 days, and the response carries its `sessionToken` once. The
portal keeps it in an HttpOnly, Secure, SameSite=Lax cookie on its own host; only its hash is stored.

A link works once and for 15 minutes. A used, expired or signed-out link, or a wrong token, is
`portal_link_invalid`, all alike. The customer's `portalLastLoginAt` is set.

Public: the session's own brand and mode apply (a test-mode link signs in to the test-mode portal).

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: false`

### Arguments

| Argument          | Type           | Required | Description                                                                                                                                                                                              |
| ----------------- | -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `portalSessionId` | ID (`psess_…`) | yes      | The portal session's ID (`psess_…`).                                                                                                                                                                     |
| `token`           | string         | yes      | The `token` of the sign-in link.                                                                                                                                                                         |
| `entity_id`       | ID (`ent_…`)   | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`)   | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string         | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): `portalSessionId` or `token` is malformed.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): No portal session has this ID.
- [`portal_link_invalid`](https://ownrecord.io/docs/reference/errors.md#portal_link_invalid): The link was already used, expired (15 minutes), was signed out of, or the token is wrong.
- [`customer_deleted`](https://ownrecord.io/docs/reference/errors.md#customer_deleted): The customer was deleted after the link was sent.
- [`rate_limited`](https://ownrecord.io/docs/reference/errors.md#rate_limited): Too many requests from this address or client IP; wait `retry_after_ms`.

### Example

**Redeem the link from the email**

Arguments:

```json
{
  "portalSessionId": "psess_01m4tx5nj8emwbyf7y9vcf48b2",
  "token": "Q2hhbmdlTWVJbkV4YW1wbGVzT25seS4uLi4uLi4uLi4",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "object": "portal_session",
  "id": "psess_01m4tx5nj8emwbyf7y9vcf48b2",
  "customerId": "cus_01m4tx5nj8emwbyf7y9vcf48b1",
  "brandId": "brd_01m4tx5nj8emwbyf7y9vcf48b0",
  "brandSlug": "genno",
  "livemode": false,
  "sessionToken": "U2Vzc2lvblRva2VuRm9yRG9jc09ubHkuLi4uLi4uLi4",
  "expiresAt": "2026-10-27T09:01:00.000Z"
}
```

## `portal_sessions_revoke`

**Sign out of the customer portal.** Operation [`portal.sessions.revoke`](https://ownrecord.io/docs/reference/portal-sessions-revoke.md) · Brand level · permission `portal:write` · risk tier `write` · since M9

Ends the signed-in customer's portal session at once; the portal also clears its cookie. Other sessions of the
customer (other browsers) keep working until they expire.

Customer portal only: the caller is the customer signed in to the brand's portal (`portal:write`).

Annotations: `readOnlyHint: false` · `destructiveHint: true` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `entity_id`       | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**Sign out**

Arguments:

```json
{
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "object": "portal_session",
  "id": "psess_01m4tx5nj8emwbyf7y9vcf48b2",
  "signedOut": true
}
```

## `portal_subscriptions_apply_promotion_code`

**Apply a promotion code.** Operation [`portal.subscriptions.apply_promotion_code`](https://ownrecord.io/docs/reference/portal-subscriptions-apply-promotion-code.md) · Brand level · permission `portal:write` · risk tier `financial` · since M9

Applies a promotion code the brand published to the signed-in customer's subscription: its coupon becomes a
discount of the subscription from now, for the coupon's duration. The code's own rules apply: it must be active
and unexpired with redemptions left, and meet its restrictions (this customer only, first-time customers only, a
minimum amount). Not while a cancellation is scheduled. Customer portal only (`portal:write`).

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: false`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `subscriptionId`  | ID (`sub_…`) | yes      | One of your subscriptions (`sub_…`).                                                                                                                                                                     |
| `promotionCode`   | string       | yes      | A promotion code as the customer typed it (case doesn't matter). (1–50 characters)                                                                                                                       |
| `entity_id`       | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |
| `dry_run`         | boolean      | no       | true: check permissions and rules and compute the result, then roll everything back. Nothing is saved. (default `false`)                                                                                 |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** supported. `dry_run: true` checks permissions and rules and computes the result, then rolls everything back.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): The code is empty or too long.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such subscription (another customer's reads the same), or the brand has no such code (`param: promotionCode`).
- [`portal_action_not_allowed`](https://ownrecord.io/docs/reference/errors.md#portal_action_not_allowed): The brand's portal settings or the subscription's state don't allow promotion codes now (a cancellation is scheduled, or the subscription can't change) (`details.action`).
- [`promotion_code_not_redeemable`](https://ownrecord.io/docs/reference/errors.md#promotion_code_not_redeemable): The code is archived, has expired or is used up, is for another customer or first-time customers only, or the plan doesn't reach its minimum (`details.reason`).
- [`discount_already_applied`](https://ownrecord.io/docs/reference/errors.md#discount_already_applied): This code's coupon already applies to the subscription.
- [`subscription_status_invalid`](https://ownrecord.io/docs/reference/errors.md#subscription_status_invalid): The subscription can't change in its status.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).

### Example

**20% off for three months**

Arguments:

```json
{
  "subscriptionId": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "promotionCode": "AUTUMN20",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "id": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "object": "portal_subscription",
  "status": "active",
  "currency": "EUR",
  "items": [
    {
      "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c2",
      "productId": "prod_01m4tx5nj8emwbyf7y9vcf48c4",
      "productName": "Genno Plus",
      "priceName": "Plus, monthly",
      "interval": {
        "interval": "month",
        "intervalCount": 1
      },
      "usageType": "licensed",
      "unitAmount": {
        "amount": "9000000",
        "currency": "EUR"
      },
      "taxBehavior": "inclusive",
      "id": "si_01m4tx5nj8emwbyf7y9vcf48c1",
      "quantity": 1,
      "amount": {
        "amount": "9000000",
        "currency": "EUR"
      }
    }
  ],
  "collectionMethod": "charge_automatically",
  "paymentMethodId": null,
  "currentPeriodStart": "2026-10-20T09:00:00.000Z",
  "currentPeriodEnd": "2026-11-20T10:00:00.000Z",
  "trialEnd": null,
  "cancelAtPeriodEnd": false,
  "cancelAt": null,
  "canceledAt": null,
  "endedAt": null,
  "cancellation": null,
  "discounts": [
    {
      "id": "disc_01m4tx5nj8emwbyf7y9vcf48c6",
      "name": "Autumn 20%",
      "percentOffPpm": "200000",
      "amountOff": null,
      "start": "2026-11-05T09:30:00.000Z",
      "end": "2027-02-20T10:00:00.000Z"
    }
  ],
  "scheduledChange": null,
  "actions": {
    "switch_plan": true,
    "change_quantity": false,
    "apply_promotion_code": true,
    "cancel": true,
    "reactivate": false,
    "change_payment_method": true
  },
  "cancelBehavior": "period_end",
  "collectCancelReason": true,
  "version": 5,
  "createdAt": "2026-09-20T09:00:00.000Z"
}
```

## `portal_subscriptions_cancel`

**Cancel your subscription.** Operation [`portal.subscriptions.cancel`](https://ownrecord.io/docs/reference/portal-subscriptions-cancel.md) · Brand level · permission `portal:write` · risk tier `financial` · since M9

Cancels the signed-in customer's subscription as the brand's portal settings say: at the end of the paid period
(`cancelBehavior: period_end`, the default; it can be reactivated until then), or at once (`immediate`: usage
and pending items so far are invoiced, the unused time isn't credited). When the brand asks why
(`collectCancelReason`), the customer may give a reason (`feedback`) and a comment; they're kept on the
subscription for the brand. Recorded as canceled by the customer. Emits `subscription.cancel_scheduled` or
`subscription.canceled`. Customer portal only (`portal:write`).

Annotations: `readOnlyHint: false` · `destructiveHint: true` · `idempotentHint: true` · `openWorldHint: true`

### Arguments

| Argument          | Type                                                                                                                                | Required | Description                                                                                                                                                                                              |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `subscriptionId`  | ID (`sub_…`)                                                                                                                        | yes      | One of your subscriptions (`sub_…`).                                                                                                                                                                     |
| `expectedVersion` | integer                                                                                                                             | no       | The subscription's `version` you saw (the preview's); if it changed meanwhile, `version_conflict`. (1–9007199254740991)                                                                                  |
| `feedback`        | one of `too_expensive`, `missing_features`, `switched_service`, `unused`, `customer_service`, `too_complex`, `low_quality`, `other` | no       | Why you're leaving, when the brand asks (`collectCancelReason`).                                                                                                                                         |
| `comment`         | string                                                                                                                              | no       | Anything you'd like to tell the brand, when it asks. (1–500 characters)                                                                                                                                  |
| `entity_id`       | ID (`ent_…`)                                                                                                                        | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`)                                                                                                                        | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string                                                                                                                              | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |
| `dry_run`         | boolean                                                                                                                             | no       | true: check permissions and rules and compute the result, then roll everything back. Nothing is saved. (default `false`)                                                                                 |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** supported. `dry_run: true` checks permissions and rules and computes the result, then rolls everything back.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field is malformed, or a reason was sent to a brand that doesn't ask for one.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such subscription (another customer's reads the same).
- [`portal_action_not_allowed`](https://ownrecord.io/docs/reference/errors.md#portal_action_not_allowed): The brand's portal settings or the subscription's state don't allow cancelling: cancelling is off, a cancellation is already scheduled, or the status can't be canceled (`details.action`).
- [`subscription_status_invalid`](https://ownrecord.io/docs/reference/errors.md#subscription_status_invalid): The subscription can't be canceled in its status.
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): The subscription changed since you saw it (`expectedVersion`); look again.
- [`jurisdiction_blocked`](https://ownrecord.io/docs/reference/errors.md#jurisdiction_blocked): The brand can't sell in your country now (an invoice due now can't be issued).
- [`registration_required`](https://ownrecord.io/docs/reference/errors.md#registration_required): The brand can't invoice your country yet (an invoice due now can't be issued).
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).

### Example

**Cancel at the end of the period, too expensive**

Arguments:

```json
{
  "subscriptionId": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "expectedVersion": 4,
  "feedback": "too_expensive",
  "comment": "Back when my project starts again.",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "id": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "object": "portal_subscription",
  "status": "active",
  "currency": "EUR",
  "items": [
    {
      "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c2",
      "productId": "prod_01m4tx5nj8emwbyf7y9vcf48c4",
      "productName": "Genno Plus",
      "priceName": "Plus, monthly",
      "interval": {
        "interval": "month",
        "intervalCount": 1
      },
      "usageType": "licensed",
      "unitAmount": {
        "amount": "9000000",
        "currency": "EUR"
      },
      "taxBehavior": "inclusive",
      "id": "si_01m4tx5nj8emwbyf7y9vcf48c1",
      "quantity": 1,
      "amount": {
        "amount": "9000000",
        "currency": "EUR"
      }
    }
  ],
  "collectionMethod": "charge_automatically",
  "paymentMethodId": null,
  "currentPeriodStart": "2026-10-20T09:00:00.000Z",
  "currentPeriodEnd": "2026-11-20T10:00:00.000Z",
  "trialEnd": null,
  "cancelAtPeriodEnd": true,
  "cancelAt": "2026-11-20T10:00:00.000Z",
  "canceledAt": null,
  "endedAt": null,
  "cancellation": {
    "by": "customer",
    "feedback": "too_expensive",
    "comment": "Back when my project starts again."
  },
  "discounts": [],
  "scheduledChange": null,
  "actions": {
    "switch_plan": false,
    "change_quantity": false,
    "apply_promotion_code": false,
    "cancel": false,
    "reactivate": true,
    "change_payment_method": true
  },
  "cancelBehavior": "period_end",
  "collectCancelReason": true,
  "version": 5,
  "createdAt": "2026-09-20T09:00:00.000Z"
}
```

## `portal_subscriptions_get`

**Get one of your subscriptions.** Operation [`portal.subscriptions.get`](https://ownrecord.io/docs/reference/portal-subscriptions-get.md) · Brand level · permission `portal:read` · risk tier `read` · since M9

Returns one of the signed-in customer's subscriptions as `portal.subscriptions.list` shows it, plus the plans
each item may switch to under the brand's plan-change matrix (`planOptions`) and the upcoming invoice as it
stands now (`upcomingInvoice`: the next renewal's lines, tax and amount due; null when nothing is billed next).
Customer portal only (`portal:read`).

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument         | Type         | Required | Description                                                                                                                              |
| ---------------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `subscriptionId` | ID (`sub_…`) | yes      | One of your subscriptions (`sub_…`).                                                                                                     |
| `entity_id`      | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                 |
| `brand_id`       | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand. |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): The subscription's ID or a pagination value is malformed.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such subscription (another customer's reads the same).
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).

### Example

**A monthly plan that can move up to Pro**

Arguments:

```json
{
  "subscriptionId": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz"
}
```

Result `output`:

```json
{
  "id": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "object": "portal_subscription",
  "status": "active",
  "currency": "EUR",
  "items": [
    {
      "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c2",
      "productId": "prod_01m4tx5nj8emwbyf7y9vcf48c4",
      "productName": "Genno Plus",
      "priceName": "Plus, monthly",
      "interval": {
        "interval": "month",
        "intervalCount": 1
      },
      "usageType": "licensed",
      "unitAmount": {
        "amount": "9000000",
        "currency": "EUR"
      },
      "taxBehavior": "inclusive",
      "id": "si_01m4tx5nj8emwbyf7y9vcf48c1",
      "quantity": 1,
      "amount": {
        "amount": "9000000",
        "currency": "EUR"
      }
    }
  ],
  "collectionMethod": "charge_automatically",
  "paymentMethodId": null,
  "currentPeriodStart": "2026-10-20T09:00:00.000Z",
  "currentPeriodEnd": "2026-11-20T10:00:00.000Z",
  "trialEnd": null,
  "cancelAtPeriodEnd": false,
  "cancelAt": null,
  "canceledAt": null,
  "endedAt": null,
  "cancellation": null,
  "discounts": [],
  "scheduledChange": null,
  "actions": {
    "switch_plan": true,
    "change_quantity": false,
    "apply_promotion_code": true,
    "cancel": true,
    "reactivate": false,
    "change_payment_method": true
  },
  "cancelBehavior": "period_end",
  "collectCancelReason": true,
  "version": 4,
  "createdAt": "2026-09-20T09:00:00.000Z",
  "planOptions": [
    {
      "itemId": "si_01m4tx5nj8emwbyf7y9vcf48c1",
      "targets": [
        {
          "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c3",
          "productId": "prod_01m4tx5nj8emwbyf7y9vcf48c5",
          "productName": "Genno Pro",
          "priceName": "Pro, monthly",
          "interval": {
            "interval": "month",
            "intervalCount": 1
          },
          "usageType": "licensed",
          "unitAmount": {
            "amount": "29000000",
            "currency": "EUR"
          },
          "taxBehavior": "inclusive"
        }
      ]
    }
  ],
  "upcomingInvoice": {
    "currency": "EUR",
    "lines": [
      {
        "description": "Genno Plus (Plus, monthly)",
        "quantity": 1,
        "amount": {
          "amount": "9000000",
          "currency": "EUR"
        },
        "proration": false,
        "periodStart": "2026-11-20T10:00:00.000Z",
        "periodEnd": "2026-12-20T10:00:00.000Z"
      }
    ],
    "subtotal": {
      "amount": "7377049",
      "currency": "EUR"
    },
    "tax": {
      "amount": "1622951",
      "currency": "EUR"
    },
    "total": {
      "amount": "9000000",
      "currency": "EUR"
    },
    "creditApplied": null,
    "amountDue": {
      "amount": "9000000",
      "currency": "EUR"
    }
  }
}
```

## `portal_subscriptions_list`

**List your subscriptions.** Operation [`portal.subscriptions.list`](https://ownrecord.io/docs/reference/portal-subscriptions-list.md) · Brand level · permission `portal:read` · risk tier `read` · since M9

Lists the signed-in customer's subscriptions with this brand, newest first: status, plan and quantity, the
current period, discounts, a scheduled change or cancellation, and what the brand's portal settings let the
customer do with each (`actions`). Customer portal only (`portal:read`).

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument         | Type         | Required | Description                                                                                                                              |
| ---------------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `limit`          | integer      | no       | Page size, 1 to 100 (default 20). (1–100)                                                                                                |
| `starting_after` | string       | no       | The `next_cursor` of the previous page. (1–64 characters)                                                                                |
| `entity_id`      | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                 |
| `brand_id`       | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand. |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A pagination value is malformed.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).

### Example

**One active plan**

Arguments:

```json
{
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz"
}
```

Result `output`:

```json
{
  "object": "list",
  "data": [
    {
      "id": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
      "object": "portal_subscription",
      "status": "active",
      "currency": "EUR",
      "items": [
        {
          "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c2",
          "productId": "prod_01m4tx5nj8emwbyf7y9vcf48c4",
          "productName": "Genno Plus",
          "priceName": "Plus, monthly",
          "interval": {
            "interval": "month",
            "intervalCount": 1
          },
          "usageType": "licensed",
          "unitAmount": {
            "amount": "9000000",
            "currency": "EUR"
          },
          "taxBehavior": "inclusive",
          "id": "si_01m4tx5nj8emwbyf7y9vcf48c1",
          "quantity": 1,
          "amount": {
            "amount": "9000000",
            "currency": "EUR"
          }
        }
      ],
      "collectionMethod": "charge_automatically",
      "paymentMethodId": null,
      "currentPeriodStart": "2026-10-20T09:00:00.000Z",
      "currentPeriodEnd": "2026-11-20T10:00:00.000Z",
      "trialEnd": null,
      "cancelAtPeriodEnd": false,
      "cancelAt": null,
      "canceledAt": null,
      "endedAt": null,
      "cancellation": null,
      "discounts": [],
      "scheduledChange": null,
      "actions": {
        "switch_plan": true,
        "change_quantity": false,
        "apply_promotion_code": true,
        "cancel": true,
        "reactivate": false,
        "change_payment_method": true
      },
      "cancelBehavior": "period_end",
      "collectCancelReason": true,
      "version": 4,
      "createdAt": "2026-09-20T09:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

## `portal_subscriptions_preview_change`

**Preview a plan or quantity change.** Operation [`portal.subscriptions.preview_change`](https://ownrecord.io/docs/reference/portal-subscriptions-preview-change.md) · Brand level · permission `portal:read` · risk tier `read` · since M9

Shows exactly what switching one item of the signed-in customer's subscription to another plan, or changing its
quantity, would do, without changing anything: an upgrade applies now and its invoice is issued and charged now
(`invoice`: the credit for the unused time, the charge for the time left, tax and the amount due); a downgrade
applies at the end of the period with nothing to pay now. The brand's portal settings decide what may change:
a plan switch only to a plan its plan-change matrix lists for the item's plan, a quantity change only when the
brand allows it. Confirm with `portal.subscriptions.update` and the preview's `version`.

Proration is exact to the second, so the real change may differ by the time that passed. Customer portal only
(`portal:read`).

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: true`

### Arguments

| Argument         | Type           | Required | Description                                                                                                                              |
| ---------------- | -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `subscriptionId` | ID (`sub_…`)   | yes      | One of your subscriptions (`sub_…`).                                                                                                     |
| `itemId`         | ID (`si_…`)    | yes      | The subscription item to change (`si_…`).                                                                                                |
| `priceId`        | ID (`price_…`) | no       | Switch the item to this price: one the brand's plan-change matrix allows from its price.                                                 |
| `quantity`       | integer        | no       | The item's new quantity (seats), when the brand lets customers change it. (1–1000000)                                                    |
| `entity_id`      | ID (`ent_…`)   | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                 |
| `brand_id`       | ID (`brd_…`)   | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand. |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field is malformed, or neither `priceId` nor `quantity` was sent.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such subscription (another customer's reads the same).
- [`subscription_item_not_found`](https://ownrecord.io/docs/reference/errors.md#subscription_item_not_found): `itemId` isn't one of the subscription's items.
- [`portal_action_not_allowed`](https://ownrecord.io/docs/reference/errors.md#portal_action_not_allowed): The brand's portal settings or the subscription's state don't allow this change: plan switches need the brand's plan-change matrix to list the target, quantity changes need the brand's permission and a licensed item (`details.action`).
- [`subscription_change_invalid`](https://ownrecord.io/docs/reference/errors.md#subscription_change_invalid): The change can't be made now (for example a change already waits for its invoice to be paid).
- [`subscription_status_invalid`](https://ownrecord.io/docs/reference/errors.md#subscription_status_invalid): The subscription can't change in its status.
- [`subscription_price_invalid`](https://ownrecord.io/docs/reference/errors.md#subscription_price_invalid): The new plan's price is archived.
- [`jurisdiction_blocked`](https://ownrecord.io/docs/reference/errors.md#jurisdiction_blocked): The brand can't sell in your country now (an invoice due now can't be issued).
- [`registration_required`](https://ownrecord.io/docs/reference/errors.md#registration_required): The brand can't invoice your country yet (an invoice due now can't be issued).
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).

### Example

**Upgrading from Plus to Pro mid-period**

Arguments:

```json
{
  "subscriptionId": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "itemId": "si_01m4tx5nj8emwbyf7y9vcf48c1",
  "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c3",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz"
}
```

Result `output`:

```json
{
  "object": "portal_subscription_change_preview",
  "subscriptionId": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "direction": "upgrade",
  "effective": "now",
  "effectiveAt": "2026-11-05T09:30:00.000Z",
  "settlement": "invoice_now",
  "invoice": {
    "currency": "EUR",
    "lines": [
      {
        "description": "Unused time on Genno Plus (Plus, monthly)",
        "quantity": 1,
        "amount": {
          "amount": "-4500000",
          "currency": "EUR"
        },
        "proration": true,
        "periodStart": "2026-11-05T09:30:00.000Z",
        "periodEnd": "2026-11-20T10:00:00.000Z"
      },
      {
        "description": "Remaining time on Genno Pro (Pro, monthly)",
        "quantity": 1,
        "amount": {
          "amount": "14500000",
          "currency": "EUR"
        },
        "proration": true,
        "periodStart": "2026-11-05T09:30:00.000Z",
        "periodEnd": "2026-11-20T10:00:00.000Z"
      }
    ],
    "subtotal": {
      "amount": "8196721",
      "currency": "EUR"
    },
    "tax": {
      "amount": "1803279",
      "currency": "EUR"
    },
    "total": {
      "amount": "10000000",
      "currency": "EUR"
    },
    "creditApplied": null,
    "amountDue": {
      "amount": "10000000",
      "currency": "EUR"
    }
  },
  "pendingItems": [],
  "credit": null,
  "items": [
    {
      "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c3",
      "productId": "prod_01m4tx5nj8emwbyf7y9vcf48c5",
      "productName": "Genno Pro",
      "priceName": "Pro, monthly",
      "interval": {
        "interval": "month",
        "intervalCount": 1
      },
      "usageType": "licensed",
      "unitAmount": {
        "amount": "29000000",
        "currency": "EUR"
      },
      "taxBehavior": "inclusive",
      "id": "si_01m4tx5nj8emwbyf7y9vcf48c1",
      "quantity": 1,
      "amount": {
        "amount": "29000000",
        "currency": "EUR"
      }
    }
  ],
  "periodAmount": {
    "amount": "29000000",
    "currency": "EUR"
  },
  "version": 4
}
```

## `portal_subscriptions_reactivate`

**Keep your subscription.** Operation [`portal.subscriptions.reactivate`](https://ownrecord.io/docs/reference/portal-subscriptions-reactivate.md) · Brand level · permission `portal:write` · risk tier `write` · since M9

Withdraws a cancellation the signed-in customer scheduled for the end of the period, before it ends: the
subscription renews as before. A cancellation the brand scheduled can't be withdrawn here. Emits
`subscription.updated`. Customer portal only (`portal:write`).

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `subscriptionId`  | ID (`sub_…`) | yes      | One of your subscriptions (`sub_…`).                                                                                                                                                                     |
| `expectedVersion` | integer      | no       | The subscription's `version` you saw (the preview's); if it changed meanwhile, `version_conflict`. (1–9007199254740991)                                                                                  |
| `entity_id`       | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): The subscription's ID is malformed.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such subscription (another customer's reads the same).
- [`portal_action_not_allowed`](https://ownrecord.io/docs/reference/errors.md#portal_action_not_allowed): The brand's portal settings or the subscription's state don't allow reactivating: no cancellation is scheduled, the brand scheduled it, or the period has already ended (`details.action`).
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): The subscription changed since you saw it (`expectedVersion`); look again.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).

### Example

**Changed your mind**

Arguments:

```json
{
  "subscriptionId": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "expectedVersion": 5,
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "id": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "object": "portal_subscription",
  "status": "active",
  "currency": "EUR",
  "items": [
    {
      "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c2",
      "productId": "prod_01m4tx5nj8emwbyf7y9vcf48c4",
      "productName": "Genno Plus",
      "priceName": "Plus, monthly",
      "interval": {
        "interval": "month",
        "intervalCount": 1
      },
      "usageType": "licensed",
      "unitAmount": {
        "amount": "9000000",
        "currency": "EUR"
      },
      "taxBehavior": "inclusive",
      "id": "si_01m4tx5nj8emwbyf7y9vcf48c1",
      "quantity": 1,
      "amount": {
        "amount": "9000000",
        "currency": "EUR"
      }
    }
  ],
  "collectionMethod": "charge_automatically",
  "paymentMethodId": null,
  "currentPeriodStart": "2026-10-20T09:00:00.000Z",
  "currentPeriodEnd": "2026-11-20T10:00:00.000Z",
  "trialEnd": null,
  "cancelAtPeriodEnd": false,
  "cancelAt": null,
  "canceledAt": null,
  "endedAt": null,
  "cancellation": null,
  "discounts": [],
  "scheduledChange": null,
  "actions": {
    "switch_plan": true,
    "change_quantity": false,
    "apply_promotion_code": true,
    "cancel": true,
    "reactivate": false,
    "change_payment_method": true
  },
  "cancelBehavior": "period_end",
  "collectCancelReason": true,
  "version": 6,
  "createdAt": "2026-09-20T09:00:00.000Z"
}
```

## `portal_subscriptions_set_payment_method`

**Choose the card a subscription charges.** Operation [`portal.subscriptions.set_payment_method`](https://ownrecord.io/docs/reference/portal-subscriptions-set-payment-method.md) · Brand level · permission `portal:write` · risk tier `write` · since M9

Sets which of the signed-in customer's saved cards a card-charged subscription charges (`null`: the customer's
default card). A past-due invoice of the subscription is retried with it at once. Add cards with
`portal.payment_methods.setup`. Customer portal only (`portal:write`).

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: true`

### Arguments

| Argument          | Type                | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `subscriptionId`  | ID (`sub_…`)        | yes      | One of your subscriptions (`sub_…`).                                                                                                                                                                     |
| `paymentMethodId` | ID (`pm_…`) or null | yes      | One of your saved payment methods (`pm_…`), or null to charge your default one.                                                                                                                          |
| `entity_id`       | ID (`ent_…`)        | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`)        | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string              | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): An ID is malformed.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such subscription (another customer's reads the same).
- [`portal_action_not_allowed`](https://ownrecord.io/docs/reference/errors.md#portal_action_not_allowed): The brand's portal settings or the subscription's state don't allow choosing a card: the subscription is paid by bank transfer or has ended (`details.action`).
- [`subscription_payment_method_invalid`](https://ownrecord.io/docs/reference/errors.md#subscription_payment_method_invalid): The payment method isn't one of your saved, usable cards.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).

### Example

**Charge the new card**

Arguments:

```json
{
  "subscriptionId": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "paymentMethodId": "pm_01m4tx5nj8emwbyf7y9vcf48cb",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "id": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "object": "portal_subscription",
  "status": "active",
  "currency": "EUR",
  "items": [
    {
      "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c2",
      "productId": "prod_01m4tx5nj8emwbyf7y9vcf48c4",
      "productName": "Genno Plus",
      "priceName": "Plus, monthly",
      "interval": {
        "interval": "month",
        "intervalCount": 1
      },
      "usageType": "licensed",
      "unitAmount": {
        "amount": "9000000",
        "currency": "EUR"
      },
      "taxBehavior": "inclusive",
      "id": "si_01m4tx5nj8emwbyf7y9vcf48c1",
      "quantity": 1,
      "amount": {
        "amount": "9000000",
        "currency": "EUR"
      }
    }
  ],
  "collectionMethod": "charge_automatically",
  "paymentMethodId": "pm_01m4tx5nj8emwbyf7y9vcf48cb",
  "currentPeriodStart": "2026-10-20T09:00:00.000Z",
  "currentPeriodEnd": "2026-11-20T10:00:00.000Z",
  "trialEnd": null,
  "cancelAtPeriodEnd": false,
  "cancelAt": null,
  "canceledAt": null,
  "endedAt": null,
  "cancellation": null,
  "discounts": [],
  "scheduledChange": null,
  "actions": {
    "switch_plan": true,
    "change_quantity": false,
    "apply_promotion_code": true,
    "cancel": true,
    "reactivate": false,
    "change_payment_method": true
  },
  "cancelBehavior": "period_end",
  "collectCancelReason": true,
  "version": 5,
  "createdAt": "2026-09-20T09:00:00.000Z"
}
```

## `portal_subscriptions_update`

**Switch plan or change the quantity.** Operation [`portal.subscriptions.update`](https://ownrecord.io/docs/reference/portal-subscriptions-update.md) · Brand level · permission `portal:write` · risk tier `financial` · since M9

Switches one item of the signed-in customer's subscription to another plan, or changes its quantity, as
`portal.subscriptions.preview_change` showed: an upgrade applies now, its prorated invoice is issued and charged
to the subscription's card (or sent, for a subscription paid by bank transfer); a downgrade is scheduled for the
end of the period (`scheduledChange`). The brand's portal settings decide what may change, and its billing
settings how a failed payment is handled. Send the preview's `version` as `expectedVersion`. Emits
`subscription.updated`. Customer portal only (`portal:write`).

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: true`

### Arguments

| Argument          | Type           | Required | Description                                                                                                                                                                                              |
| ----------------- | -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `subscriptionId`  | ID (`sub_…`)   | yes      | One of your subscriptions (`sub_…`).                                                                                                                                                                     |
| `itemId`          | ID (`si_…`)    | yes      | The subscription item to change (`si_…`).                                                                                                                                                                |
| `priceId`         | ID (`price_…`) | no       | Switch the item to this price: one the brand's plan-change matrix allows from its price.                                                                                                                 |
| `quantity`        | integer        | no       | The item's new quantity (seats), when the brand lets customers change it. (1–1000000)                                                                                                                    |
| `expectedVersion` | integer        | no       | The subscription's `version` you saw (the preview's); if it changed meanwhile, `version_conflict`. (1–9007199254740991)                                                                                  |
| `entity_id`       | ID (`ent_…`)   | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`)   | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string         | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |
| `dry_run`         | boolean        | no       | true: check permissions and rules and compute the result, then roll everything back. Nothing is saved. (default `false`)                                                                                 |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** supported. `dry_run: true` checks permissions and rules and computes the result, then rolls everything back.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field is malformed, or neither `priceId` nor `quantity` was sent.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such subscription (another customer's reads the same).
- [`subscription_item_not_found`](https://ownrecord.io/docs/reference/errors.md#subscription_item_not_found): `itemId` isn't one of the subscription's items.
- [`portal_action_not_allowed`](https://ownrecord.io/docs/reference/errors.md#portal_action_not_allowed): The brand's portal settings or the subscription's state don't allow this change: plan switches need the brand's plan-change matrix to list the target, quantity changes need the brand's permission and a licensed item (`details.action`).
- [`subscription_change_invalid`](https://ownrecord.io/docs/reference/errors.md#subscription_change_invalid): The change can't be made now (for example a change already waits for its invoice to be paid).
- [`subscription_status_invalid`](https://ownrecord.io/docs/reference/errors.md#subscription_status_invalid): The subscription can't change in its status.
- [`subscription_price_invalid`](https://ownrecord.io/docs/reference/errors.md#subscription_price_invalid): The new plan's price is archived.
- [`jurisdiction_blocked`](https://ownrecord.io/docs/reference/errors.md#jurisdiction_blocked): The brand can't sell in your country now (an invoice due now can't be issued).
- [`registration_required`](https://ownrecord.io/docs/reference/errors.md#registration_required): The brand can't invoice your country yet (an invoice due now can't be issued).
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): The subscription changed since you saw it (`expectedVersion`); look again.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).

### Example

**Move up to Pro now**

Arguments:

```json
{
  "subscriptionId": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "itemId": "si_01m4tx5nj8emwbyf7y9vcf48c1",
  "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c3",
  "expectedVersion": 4,
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "id": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "object": "portal_subscription",
  "status": "active",
  "currency": "EUR",
  "items": [
    {
      "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c3",
      "productId": "prod_01m4tx5nj8emwbyf7y9vcf48c5",
      "productName": "Genno Pro",
      "priceName": "Pro, monthly",
      "interval": {
        "interval": "month",
        "intervalCount": 1
      },
      "usageType": "licensed",
      "unitAmount": {
        "amount": "29000000",
        "currency": "EUR"
      },
      "taxBehavior": "inclusive",
      "id": "si_01m4tx5nj8emwbyf7y9vcf48c1",
      "quantity": 1,
      "amount": {
        "amount": "29000000",
        "currency": "EUR"
      }
    }
  ],
  "collectionMethod": "charge_automatically",
  "paymentMethodId": null,
  "currentPeriodStart": "2026-10-20T09:00:00.000Z",
  "currentPeriodEnd": "2026-11-20T10:00:00.000Z",
  "trialEnd": null,
  "cancelAtPeriodEnd": false,
  "cancelAt": null,
  "canceledAt": null,
  "endedAt": null,
  "cancellation": null,
  "discounts": [],
  "scheduledChange": null,
  "actions": {
    "switch_plan": true,
    "change_quantity": false,
    "apply_promotion_code": true,
    "cancel": true,
    "reactivate": false,
    "change_payment_method": true
  },
  "cancelBehavior": "period_end",
  "collectCancelReason": true,
  "version": 5,
  "createdAt": "2026-09-20T09:00:00.000Z"
}
```

## `portal_tax_details_update`

**Update your billing details.** Operation [`portal.tax_details.update`](https://ownrecord.io/docs/reference/portal-tax-details-update.md) · Brand level · permission `portal:write` · risk tier `write` · since M9

Changes the signed-in customer's name, business name or billing address, as invoices show them. Send only the
fields to change; `null` removes an optional one, and `billingAddress` is replaced as a whole. The new address
is used for the next purchases and invoices; past sales stay taxed where they were.

Emits `customer.updated` to the brand's webhooks. Customer portal only (`portal:write`).

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument          | Type           | Required | Description                                                                                                                                                                                              |
| ----------------- | -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`            | string or null | no       | Your name; null removes it.                                                                                                                                                                              |
| `businessName`    | string or null | no       | Your business's legal name, for invoices; null removes it.                                                                                                                                               |
| `billingAddress`  | object or null | no       | Replaces the billing address; null removes it.                                                                                                                                                           |
| `entity_id`       | ID (`ent_…`)   | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`)   | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string         | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): Nothing to change, a field is malformed, or a business would be left without a business name or a tax ID.
- [`customer_deleted`](https://ownrecord.io/docs/reference/errors.md#customer_deleted): Your account was deleted by the brand.
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): Your details changed at the same moment; retry.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**Set the billing address**

Arguments:

```json
{
  "billingAddress": {
    "line1": "Ring 1",
    "city": "Wien",
    "postalCode": "1010",
    "country": "AT"
  },
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "object": "portal_account",
  "customerId": "cus_01m4tx5nj8emwbyf7y9vcf48b1",
  "livemode": false,
  "email": "ana@example.com",
  "name": "Ana Novak",
  "businessName": "Alpen Apps GmbH",
  "type": "business",
  "locale": null,
  "billingAddress": {
    "line1": "Ring 1",
    "line2": null,
    "city": "Wien",
    "postalCode": "1010",
    "region": null,
    "country": "AT"
  },
  "taxIds": [
    {
      "id": "ATU00000000",
      "type": "eu_vat",
      "value": "ATU00000000",
      "country": "AT",
      "status": "valid",
      "checkedAt": "2026-10-20T09:00:00.000Z"
    }
  ],
  "portalLastLoginAt": "2026-10-20T09:00:00.000Z"
}
```

## `portal_tax_ids_add`

**Add a tax ID to your account.** Operation [`portal.tax_ids.add`](https://ownrecord.io/docs/reference/portal-tax-ids-add.md) · Brand level · permission `portal:write` · risk tier `write` · since M9

Adds a tax ID (for example an EU VAT number) to the signed-in customer's account after checking it with its
register (VIES for EU VAT IDs, HMRC for UK VAT numbers, the Swiss UID register; others by format). The answer is
stored with it: a number the register doesn't know is kept as `invalid` and doesn't make purchases
business-to-business. A malformed number is refused (`tax_id_invalid`). At most 5 per customer.

Emits `customer.updated`. Customer portal only (`portal:write`).

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: true`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`            | string       | yes      | The tax ID's type: `eu_vat`, `gb_vat`, `ch_uid`/`ch_vat`, `no_org`/`no_vat` or `us_ein`. Other types are kept but can't be checked (`unverified`).                                                       |
| `value`           | string       | yes      | The number as the customer gave it, with or without the country prefix and separators. (1–50 characters)                                                                                                 |
| `country`         | string       | yes      | The country that issued the tax ID (ISO 3166-1 alpha-2; `EL` = Greece).                                                                                                                                  |
| `entity_id`       | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field is missing or malformed.
- [`tax_id_invalid`](https://ownrecord.io/docs/reference/errors.md#tax_id_invalid): The number isn't valid for its type and country (format or checksum); `details.reason` says which. Nothing is stored.
- [`customer_deleted`](https://ownrecord.io/docs/reference/errors.md#customer_deleted): Your account was deleted by the brand.
- [`customer_tax_id_exists`](https://ownrecord.io/docs/reference/errors.md#customer_tax_id_exists): You already have this tax ID.
- [`customer_tax_id_limit`](https://ownrecord.io/docs/reference/errors.md#customer_tax_id_limit): You already have 5 tax IDs.
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): Your details changed at the same moment; retry.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**Add an Austrian VAT ID**

Arguments:

```json
{
  "type": "eu_vat",
  "value": "ATU 0000 0000",
  "country": "AT",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "object": "portal_account",
  "customerId": "cus_01m4tx5nj8emwbyf7y9vcf48b1",
  "livemode": false,
  "email": "ana@example.com",
  "name": "Ana Novak",
  "businessName": "Alpen Apps GmbH",
  "type": "business",
  "locale": null,
  "billingAddress": {
    "line1": "Ring 1",
    "line2": null,
    "city": "Wien",
    "postalCode": "1010",
    "region": null,
    "country": "AT"
  },
  "taxIds": [
    {
      "id": "ATU00000000",
      "type": "eu_vat",
      "value": "ATU00000000",
      "country": "AT",
      "status": "valid",
      "checkedAt": "2026-10-20T09:00:00.000Z"
    }
  ],
  "portalLastLoginAt": "2026-10-20T09:00:00.000Z"
}
```

## `portal_tax_ids_delete`

**Remove a tax ID from your account.** Operation [`portal.tax_ids.delete`](https://ownrecord.io/docs/reference/portal-tax-ids-delete.md) · Brand level · permission `portal:write` · risk tier `write` · since M9

Removes one of the signed-in customer's tax IDs (by its `id`, the canonical number). Later purchases are taxed
without it; issued invoices don't change. Emits `customer.updated`. Customer portal only (`portal:write`).

Annotations: `readOnlyHint: false` · `destructiveHint: true` · `idempotentHint: false` · `openWorldHint: false`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `taxIdId`         | string       | yes      | The tax ID's `id` on the customer: its canonical number, e.g. `ATU12345678`.                                                                                                                             |
| `entity_id`       | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): The tax ID's ID or another field is malformed.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such tax ID (another customer's reads the same).
- [`customer_deleted`](https://ownrecord.io/docs/reference/errors.md#customer_deleted): Your account was deleted by the brand.
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): Your details changed at the same moment; retry.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the customer operations).

### Example

**Remove a VAT ID**

Arguments:

```json
{
  "taxIdId": "ATU00000000",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "object": "portal_account",
  "customerId": "cus_01m4tx5nj8emwbyf7y9vcf48b1",
  "livemode": false,
  "email": "ana@example.com",
  "name": "Ana Novak",
  "businessName": null,
  "type": "individual",
  "locale": null,
  "billingAddress": {
    "line1": "Ring 1",
    "line2": null,
    "city": "Wien",
    "postalCode": "1010",
    "region": null,
    "country": "AT"
  },
  "taxIds": [],
  "portalLastLoginAt": "2026-10-20T09:00:00.000Z"
}
```

## `portal_wallets_auto_reload_update`

**Set up auto-reload.** Operation [`portal.wallets.auto_reload.update`](https://ownrecord.io/docs/reference/portal-wallets-auto-reload-update.md) · Brand level · permission `portal:write` · risk tier `write` · since M9

Turns automatic reloading of one of the signed-in customer's wallets on or off. On: when the balance falls below
`below`, `amount` is bought and charged to the card (`paymentMethodId`, default: the customer's default card)
without the customer present. The amount must be one the brand sells as credits (a pack of that price, or within
a custom-amount pack's range) and the threshold at most the amount, so one reload restores the balance. Off keeps
the settings for next time. Auto-reload turns itself off after 2 declined reloads. Customer portal only
(`portal:write`).

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `walletId`        | ID (`wal_…`) | yes      | One of your wallets (`wal_…`).                                                                                                                                                                           |
| `enabled`         | boolean      | yes      | Turn auto-reload on or off.                                                                                                                                                                              |
| `below`           | object       | no       | Reload when the balance falls below this (at most the reload amount).                                                                                                                                    |
| `amount`          | object       | no       | How much each reload buys: an amount the brand sells as credits.                                                                                                                                         |
| `paymentMethodId` | ID (`pm_…`)  | no       | The card to charge (`pm_…`); default: your default payment method.                                                                                                                                       |
| `entity_id`       | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field is malformed, the threshold or amount is missing to turn it on, the amount isn't one the brand sells as credits, or the threshold is zero or above the amount (`details.reason`).
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such wallet (another customer's reads the same).
- [`wallet_currency_mismatch`](https://ownrecord.io/docs/reference/errors.md#wallet_currency_mismatch): An amount isn't in the wallet's currency.
- [`wallet_payment_method_invalid`](https://ownrecord.io/docs/reference/errors.md#wallet_payment_method_invalid): You have no saved card to charge, or the card isn't yours or was removed.
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): The wallet changed at the same moment; retry.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).

### Example

**Reload €25 below €5**

Arguments:

```json
{
  "walletId": "wal_01m4tx5nj8emwbyf7y9vcf48c7",
  "enabled": true,
  "below": {
    "amount": "5000000",
    "currency": "EUR"
  },
  "amount": {
    "amount": "25000000",
    "currency": "EUR"
  },
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "id": "wal_01m4tx5nj8emwbyf7y9vcf48c7",
  "object": "portal_wallet",
  "currency": "EUR",
  "status": "active",
  "balance": {
    "amount": "32500000",
    "currency": "EUR"
  },
  "available": {
    "amount": "25000000",
    "currency": "EUR"
  },
  "promo": {
    "amount": "5000000",
    "currency": "EUR"
  },
  "reserved": {
    "amount": "2500000",
    "currency": "EUR"
  },
  "upcomingExpiries": [
    {
      "amount": {
        "amount": "5000000",
        "currency": "EUR"
      },
      "expiresAt": "2026-12-31T23:00:00.000Z"
    }
  ],
  "autoReload": {
    "enabled": true,
    "below": {
      "amount": "5000000",
      "currency": "EUR"
    },
    "amount": {
      "amount": "25000000",
      "currency": "EUR"
    },
    "paymentMethodId": "pm_01m4tx5nj8emwbyf7y9vcf48cb"
  },
  "createdAt": "2026-09-20T09:00:00.000Z"
}
```

## `portal_wallets_list`

**List your wallets.** Operation [`portal.wallets.list`](https://ownrecord.io/docs/reference/portal-wallets-list.md) · Brand level · permission `portal:read` · risk tier `read` · since M9

Lists the signed-in customer's prepaid credit wallets with this brand, one per currency, newest first: the
balance, what is available, promotional and held for usage in progress, the next credits to expire, and the
auto-reload settings. Amounts are integer micros. Customer portal only (`portal:read`).

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument         | Type         | Required | Description                                                                                                                              |
| ---------------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `limit`          | integer      | no       | Page size, 1 to 100 (default 20). (1–100)                                                                                                |
| `starting_after` | string       | no       | The `next_cursor` of the previous page. (1–64 characters)                                                                                |
| `entity_id`      | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                 |
| `brand_id`       | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand. |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).

### Example

**A euro wallet with promotional credits**

Arguments:

```json
{
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz"
}
```

Result `output`:

```json
{
  "object": "list",
  "data": [
    {
      "id": "wal_01m4tx5nj8emwbyf7y9vcf48c7",
      "object": "portal_wallet",
      "currency": "EUR",
      "status": "active",
      "balance": {
        "amount": "32500000",
        "currency": "EUR"
      },
      "available": {
        "amount": "25000000",
        "currency": "EUR"
      },
      "promo": {
        "amount": "5000000",
        "currency": "EUR"
      },
      "reserved": {
        "amount": "2500000",
        "currency": "EUR"
      },
      "upcomingExpiries": [
        {
          "amount": {
            "amount": "5000000",
            "currency": "EUR"
          },
          "expiresAt": "2026-12-31T23:00:00.000Z"
        }
      ],
      "autoReload": null,
      "createdAt": "2026-09-20T09:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

## `portal_wallets_top_up`

**Top up your credits.** Operation [`portal.wallets.top_up`](https://ownrecord.io/docs/reference/portal-wallets-top-up.md) · Brand level · permission `portal:write` · risk tier `write` · since M9

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`).

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: false`

### Arguments

| Argument          | Type           | Required | Description                                                                                                                                                                                              |
| ----------------- | -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `priceId`         | ID (`price_…`) | 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.                                                                                                                         |
| `entity_id`       | ID (`ent_…`)   | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                                                                                 |
| `brand_id`        | ID (`brd_…`)   | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand.                                                                 |
| `idempotency_key` | string         | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** not supported. `dry_run: true` fails with `dry_run_not_supported`.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field is malformed.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): The brand sells no such credit pack (or it was archived).
- [`checkout_amount_invalid`](https://ownrecord.io/docs/reference/errors.md#checkout_amount_invalid): A custom amount is missing, outside the pack's minimum and maximum, or given for a fixed pack.
- [`checkout_currency_mismatch`](https://ownrecord.io/docs/reference/errors.md#checkout_currency_mismatch): The custom amount isn't in the pack's currency.
- [`customer_deleted`](https://ownrecord.io/docs/reference/errors.md#customer_deleted): Your account was deleted by the brand.
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).

### Example

**Buy the €25 pack**

Arguments:

```json
{
  "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c9",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

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

## `portal_wallets_transactions_list`

**List your wallet's history.** Operation [`portal.wallets.transactions.list`](https://ownrecord.io/docs/reference/portal-wallets-transactions-list.md) · Brand level · permission `portal:read` · risk tier `read` · since M9

Lists what moved one of the signed-in customer's wallets, newest first, from the ledger: top-ups, included and
promotional credits, usage, holds and their release, expiries and refunds. Each entry has its category and the
signed change of the balance and of each part. Operators' notes and internal references aren't shown.
Paginate with `limit` and `starting_after`. Customer portal only (`portal:read`).

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument         | Type         | Required | Description                                                                                                                              |
| ---------------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `limit`          | integer      | no       | Page size, 1 to 100 (default 20). (1–100)                                                                                                |
| `starting_after` | string       | no       | The `next_cursor` of the previous page. (1–64 characters)                                                                                |
| `walletId`       | ID (`wal_…`) | yes      | One of your wallets (`wal_…`).                                                                                                           |
| `entity_id`      | ID (`ent_…`) | no       | Optional: the brand's legal entity (ent\_…); `brand_id` alone is enough.                                                                 |
| `brand_id`       | ID (`brd_…`) | no       | The brand to act on (brd\_…), inside your API key's reach. Needed with an organization or entity key; a brand key acts on its own brand. |

- **Brand:** pass `brand_id` with an organization or entity key; a brand key acts on its own brand.
- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): The wallet's ID or a pagination value is malformed.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): You have no such wallet (another customer's reads the same).
- [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed): The portal session is in the other mode than the request.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).

### Example

**A top-up**

Arguments:

```json
{
  "walletId": "wal_01m4tx5nj8emwbyf7y9vcf48c7",
  "brand_id": "brd_01jbx4m3v8e9t0s6k2c5n7q1wz"
}
```

Result `output`:

```json
{
  "object": "list",
  "data": [
    {
      "id": "txn_01m4tx5nj8emwbyf7y9vcf48c8",
      "object": "portal_wallet_entry",
      "walletId": "wal_01m4tx5nj8emwbyf7y9vcf48c7",
      "type": "top_up",
      "balance": {
        "amount": "25000000",
        "currency": "EUR"
      },
      "available": {
        "amount": "25000000",
        "currency": "EUR"
      },
      "promo": {
        "amount": "0",
        "currency": "EUR"
      },
      "reserved": {
        "amount": "0",
        "currency": "EUR"
      },
      "effectiveAt": "2026-11-05T09:30:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```
