> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shoppex.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Portal Sessions

> Sign a customer into your store's account page with a one-time link.

A portal session is a one-time link that signs one customer into the account page of your
storefront. Use it when your own app, support desk or bot already knows who the buyer is, and the
buyer should not type their email and a code again.

```bash theme={"system"}
curl -X POST https://api.shoppex.io/dev/v1/customers/7c9e6679-7425-40de-944b-e07fc1f90ae7/portal-sessions \
  -H "Authorization: Bearer $SHOPPEX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: support-ticket-4812" \
  -d '{"section": "orders", "return_url": "https://demo.myshoppex.io/members"}'
```

```json theme={"system"}
{
  "data": {
    "url": "https://demo.myshoppex.io/api/customer/auth/portal-session#ps_Q2hlY2sgdGhlIGRvY3MsIG5vdCB0aGlzIHRva2VuLgA",
    "expires_at": "2026-10-26T18:15:00.000Z",
    "single_use": true
  }
}
```

Send the buyer to `url`. The buyer lands on the account page of your store, signed in, on the tab
you named in `section`.

## The scope

The endpoint needs the `customers.portal_sessions` scope.

* **Full Access (`*`) does not include it.** A key gets it only when you tick
  **Log buyers into the portal** in **Settings → Developers → API Keys** (on a Custom key, or in
  **Edit Access** next to Full Access).
* **Only the store owner** (or a team member with the Owner role) can give a key this scope.
* An OAuth app can ask for it. Its token works on this endpoint only while the member who approved
  the app is an owner.

> [!WARNING]
>
> Anyone who holds a key with this scope can open the account of any of your customers. Keep the
> key on your server, never in a browser or a mobile app.

## What the link does

| Property | Value |
| - | - |
| Works | Once |
| Expires | 15 minutes after it was made |
| Works on | The storefront of the shop that made it. Another shop's storefront refuses it. |
| Signs in | The customer the link was made for, by their customer ID. A customer deleted after the link was made is not signed in. |
| Limit | 10 links per customer per hour. The 11th answers `429` with `Retry-After`. |
| Session | Lasts 24 hours from the sign-in. Using it does not make it last longer. |

The token is the part after `#`. Browsers do not send that part to any server, so the token does
not appear in server logs, in a `Referer` header or in a link preview. The storefront page reads
it, removes it from the address bar and redeems it with a form post from the same site. A mail
scanner or chat preview that opens the link without running scripts does not use it up.

When the buyer is already signed in on the store as somebody else, the link signs them in as the
customer it names, and the earlier session on this store ends.

The email address is not marked as verified: you vouched for the buyer, the buyer did not prove
the address. The buyer verifies it the next time they sign in with a code.

## What the session cannot do

A session a link opened can read the account: orders, downloads, subscriptions, tickets and the
other tabs. It cannot move money, change how the buyer signs in, or see or end the buyer's
sign-ins. These actions answer `403`
with **Sign in with your email code to do this**:

* Pay with the store balance (Instant Buy), place a reseller order, top up the wallet or the
  reseller balance, redeem loyalty points
* Request an affiliate payout, convert affiliate earnings to store balance, change the commission
  share of a link
* Open the card update page of a subscription, or confirm a plan change
* Create or revoke reseller API keys, or read, rotate or revoke the reseller delivery URL
* Change the password, link or unlink Discord
* List the devices the buyer is signed in on, sign one out, or sign out all others

The reseller catalog shows no delivery URLs to this session. The session works only on your
storefront: the hosted customer portal and direct API calls do not accept it.

The buyer signs in with their email code to do these things.

## Sections

`section` is optional. Leave it out to open the overview.

| `section` | Opens |
| - | - |
| `overview` | The account overview |
| `orders` | Orders |
| `subscriptions` | Subscriptions |
| `downloads` | Downloads |
| `favorites` | Favorites |
| `rewards` | Rewards |
| `reseller` | The reseller program |
| `support` | Support tickets |
| `referrals` | Referrals |
| `settings` | Account settings |

## Return URL

`return_url` is optional. When you send it, the account page shows **Back to** and your store name
at the top, and the link goes to this URL. Use it to send the buyer back to the page of your app
that opened the account.

The URL must use `https` and must be on one of your store's own addresses:

* your `myshoppex.io` address, while it is on
* a custom domain you added and verified in **Store → Domains**

Anything else answers `422`: `http`, another port, a domain you did not verify, another store's
address, or any other site. This keeps the link from sending a signed-in buyer to a page you do
not control. Without `return_url` the account page shows no such link.

## When the link does not work

A used, expired or unknown link, or a link opened on another store, never signs anybody in. The
buyer sees **This link has expired** on the account page, with a button to sign in with their
email instead. A session the browser already had stays as it was.

## Errors

| Status | Code | Why |
| - | - | - |
| `403` | `FORBIDDEN` | The key does not have `customers.portal_sessions`. |
| `404` | `CUSTOMER_NOT_FOUND` | No customer with this ID in your shop. |
| `409` | `CONFLICT` | Your shop has no storefront address to sign the customer in on, or the `Idempotency-Key` was already used (subcode `idempotency_key_reused`). |
| `422` | `VALIDATION_ERROR` | The ID is not a UUID, `section` is not in the list above, or `return_url` is not an `https` URL on one of your store's addresses (`details[].field` is `return_url`). |
| `429` | `RATE_LIMITED` | This customer already got 10 links in the last hour. |

## Retries

Send an `Idempotency-Key` header. The link is not stored with the key, so a retry with the same
key does not get it again: it answers `409` with the code `CONFLICT` and the subcode
`idempotency_key_reused`, and makes no second link. Use a new key for a new link. A key still
runs the request only once, so a retry after a timeout does not make two links.

## Revoking access

* A link that was not used expires after 15 minutes.
* A session a link opened ends 24 hours after the sign-in.
* Revoke the API key, or remove `customers.portal_sessions` from it, and its unused links stop
  working and the sessions they opened end at their next request. The same is true when the key
  expires after a rotation, and for an OAuth access token that is revoked.
* The buyer signs out on the account page to end a session a link opened.
* A customer you delete loses access to the account page, also through a session a link opened.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.