# Generate a customer portal link

> The portal.sessions.create operation: POST /api/v1/portal/sessions in the REST API and the portal_sessions_create tool in the MCP server.

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

## At a glance

- **REST:** `POST /api/v1/portal/sessions`
- **MCP tool:** `portal_sessions_create`
- **Operation ID:** `portal.sessions.create`
- **Scope:** Brand
- **Permission:** `customers:write`
- **Risk tier:** `security`
- **Dry run:** Not supported
- **Idempotency:** `Idempotency-Key` required
- **Modes:** Test and live
- **Authentication:** API key with the `customers:write` scope
- **Since:** M9

## Request

### Body

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

| Field        | Type       | Required | Description                               |
| ------------ | ---------- | -------- | ----------------------------------------- |
| `customerId` | `cus_…` ID | Yes      | The customer the link signs in (`cus_…`). |

## Response

`201 Created` with a JSON object:

| Field        | Type                    | Description                                                                                             |
| ------------ | ----------------------- | ------------------------------------------------------------------------------------------------------- |
| `object`     | `"portal_session_link"` |                                                                                                         |
| `id`         | `psess_…` ID            |                                                                                                         |
| `customerId` | `cus_…` ID              |                                                                                                         |
| `livemode`   | boolean                 |                                                                                                         |
| `url`        | string                  | The one-time sign-in link to give the customer. It works once, for 15 minutes; only its hash is stored. |
| `expiresAt`  | date-time               | The link stops working then.                                                                            |

## Examples

### A sign-in link for a customer

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

{
  "customerId": "cus_01m4tx5nj8emwbyf7y9vcf48b1"
}
```

Response (`201`):

```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"
}
```

## Errors

| Code                                                                                         | HTTP status | When                                                                                       | What to do                                                                                                                                         |
| -------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed)       | 400         | `customerId` isn't a customer ID (`cus_…`).                                                | Fix the fields listed in `details.issues` and send the request again.                                                                              |
| [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found)     | 404         | This brand has no such customer in this mode (customers belong to one brand and one mode). | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode.                                               |
| [`customer_deleted`](https://ownrecord.io/docs/reference/errors.md#customer_deleted)         | 400         | The customer was deleted.                                                                  | Create a new customer for this buyer. Deleted customers stay readable for the records they belong to, but can't be changed.                        |
| [`scope_required`](https://ownrecord.io/docs/reference/errors.md#scope_required)             | 400         | The request names no brand (an org or entity key without `Ownrecord-Brand`).               | Call the operation with credentials or a target at its level: an organization, a legal entity, a brand or the platform (`details.required_scope`). |
| [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed) | 403         | You have no live-mode access, so you can't act on live customers.                          | Use test mode, or credentials for the target's mode. Developers need live access granted by an organization owner or admin.                        |
| [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied)       | 403         | You can't change this brand's customers (`customers:write`: owners, admins and support).   | Ask an organization owner or admin for a role or scope that allows this action.                                                                    |

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