Sign in to the customer portal with a link
The portal.sessions.redeem operation: POST /api/v1/public/portal_sessions/{portalSessionId}/redeem in the REST API and the portal_sessions_redeem tool in the MCP server.
/api/v1/public/portal_sessions/{portalSessionId}/redeemRedeems 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).
At a glance
- REST:
POST /api/v1/public/portal_sessions/{portalSessionId}/redeem - MCP tool:
portal_sessions_redeem - Operation ID:
portal.sessions.redeem - Scope: Brand
- Permission:
portal:write - Risk tier:
security - Dry run: Not supported
- Idempotency:
Idempotency-Keyrequired - Modes: Test and live
- Authentication: None (a public endpoint, rate-limited per IP address)
- Since: M9
Request
Path parameters
| Field | Type | Description |
|---|---|---|
portalSessionId | psess_… ID | The portal session's ID (psess_…). |
Body
A JSON object (Content-Type: application/json):
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The token of the sign-in link. |
Response
200 OK with a JSON object:
| Field | Type | Description |
|---|---|---|
object | "portal_session" | |
id | psess_… ID | |
customerId | cus_… ID | |
brandId | brd_… ID | |
brandSlug | string | |
livemode | boolean | |
sessionToken | string | The session's secret for the portal's HttpOnly cookie. Returned once; only its hash is stored. |
expiresAt | date-time | The session ends then (7 days), or at sign-out. |
Examples
Redeem the link from the email
POST /api/v1/public/portal_sessions/psess_01m4tx5nj8emwbyf7y9vcf48b2/redeem
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json
{
"token": "Q2hhbmdlTWVJbkV4YW1wbGVzT25seS4uLi4uLi4uLi4"
}
Response (200):
{
"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"
}
Errors
| Code | HTTP status | When | What to do |
|---|---|---|---|
validation_failed | 400 | portalSessionId or token is malformed. | Fix the fields listed in details.issues and send the request again. |
resource_not_found | 404 | No portal session has this ID. | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode. |
portal_link_invalid | 401 | The link was already used, expired (15 minutes), was signed out of, or the token is wrong. | Ask for a new sign-in link on the portal's sign-in page. Links work once, for 15 minutes; an operator can also generate a new one. |
customer_deleted | 400 | The customer was deleted after the link was sent. | Create a new customer for this buyer. Deleted customers stay readable for the records they belong to, but can't be changed. |
rate_limited | 429 | Too many requests from this address or client IP; wait retry_after_ms. | Wait for retry_after_ms and retry with backoff. |
It can also return these common errors: api_version_unsupported, invalid_json, unsupported_media_type, scope_required, idempotency_key_required, idempotency_key_reused, idempotency_in_progress, dry_run_not_supported, internal_error.