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

# Portal Sessions

> Send your customer a one-time link to their own billing portal

# Portal Sessions

## What is a portal session?

A **portal session** is a one-time link you send to your customer so they can see their own bill, invoices, subscription, and usage history on a MarginFront-hosted page.

Think of it like a Google Doc share link, but for billing. You generate the link on your backend with your secret API key. Your customer clicks it once. They see only their own data. The link works for one hour, then expires.

## Why use it?

* Let your customers see their invoices and pay (or follow up) without bothering your support team.
* Show them their usage and what they're being billed for, so there are no surprises.
* Keep your API key safe. Your customer never sees it.

***

## How it works

1. You call `POST /v1/portal-sessions` from your backend.
2. MarginFront returns a URL like `https://portal.marginfront.com/r/mgl_abc123...`.
3. You send that URL to your customer (email, SMS, "View my billing" button in your app, whatever fits).
4. They click it. The link is used up.
5. They see a MarginFront page branded with your logo and colors showing their invoices, subscription, usage, and profile.
6. After one hour, they're logged out.
7. If anyone tries the same link a second time, they get a "link already used" error.

***

## The endpoints

All four endpoints require a **secret** API key (`mf_sk_...`). Publishable keys (`mf_pk_...`) are rejected with `401 Unauthorized`. Always call these from your backend, never from the browser.

### Create a portal session

```
POST /v1/portal-sessions
```

**Request body:**

```json theme={null}
{
  "customerExternalId": "acme-001",
  "returnUrl": "https://your-app.com/billing"
}
```

| Field                | Type      | Required         | Notes                                                                                                                                                                                            |
| -------------------- | --------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `customerExternalId` | string    | One of these two | Your system's customer ID (the same one you use everywhere else with MarginFront).                                                                                                               |
| `customerId`         | string    | One of these two | MarginFront's internal customer UUID. Use this if you already have it.                                                                                                                           |
| `returnUrl`          | string    | No               | Stored on the session for your records. The portal does NOT auto-redirect; this is informational.                                                                                                |
| `features`           | string\[] | No               | Which sections to enable. Valid values: `invoices`, `subscriptions`, `usage`, `profile`. The v1 portal shows all four sections regardless of what you send, so this field is informational only. |

**Response (`201 Created`):**

```json theme={null}
{
  "id": "ps_a1b2c3d4...",
  "object": "portal_session",
  "url": "https://portal.marginfront.com/r/mgl_a1b2c3d4...",
  "token": "mgl_a1b2c3d4...",
  "customerId": "550e8400-e29b-41d4-a716-446655440000",
  "customerName": "Acme Corp",
  "customerEmail": "billing@acme.com",
  "expiresAt": "2026-05-13T17:30:00.000Z",
  "createdAt": "2026-05-13T16:30:00.000Z"
}
```

Give the `url` to your customer. Save the `id` if you want to look up or revoke the session later. That's the only field you'll need.

**curl example:**

```bash theme={null}
curl -X POST https://api.marginfront.com/v1/portal-sessions \
  -H "x-api-key: mf_sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "customerExternalId": "acme-001",
    "returnUrl": "https://your-app.com/billing"
  }'
```

**Common errors:**

* `400 Bad Request`: neither `customerId` nor `customerExternalId` was provided.
* `401 Unauthorized`: missing API key, or you sent a publishable (`mf_pk_*`) key.
* `404 Not Found`: no customer in your org matches the ID you sent.

***

### List portal sessions

```
GET /v1/portal-sessions
```

Lists portal sessions your organization has created. Useful for audit or support. For example: "did Acme already open the link I sent yesterday?"

**Query parameters (all optional):**

| Param            | Type    | Default | Notes                                                       |
| ---------------- | ------- | ------- | ----------------------------------------------------------- |
| `customerId`     | string  | none    | Show only sessions for one customer (internal UUID).        |
| `limit`          | number  | 10      | Max 100.                                                    |
| `includeExpired` | boolean | false   | Set to `true` to also see expired or already-used sessions. |

The `token` and `url` fields are NOT returned by this endpoint. Those are only returned at creation. If you lost the URL, mint a new session.

**Response (`200 OK`):**

```json theme={null}
[
  {
    "id": "ps_a1b2c3d4...",
    "object": "portal_session",
    "customerId": "550e8400-e29b-41d4-a716-446655440000",
    "customerName": "Acme Corp",
    "customerEmail": "billing@acme.com",
    "expiresAt": "2026-05-13T17:30:00.000Z",
    "isExpired": false,
    "isUsed": false,
    "createdAt": "2026-05-13T16:30:00.000Z",
    "features": ["invoices", "subscriptions", "usage", "profile"]
  }
]
```

The response is a flat JSON array. There is no wrapper object and no `hasMore` field.

***

### Get one portal session

```
GET /v1/portal-sessions/{sessionId}
```

Returns one session's details: customer, expiry, whether it's been used yet. The `token` and `url` are NOT included. You only get those at creation time.

Returns `404 Not Found` if the session doesn't exist or belongs to another organization.

**Response (`200 OK`):**

```json theme={null}
{
  "id": "ps_a1b2c3d4...",
  "object": "portal_session",
  "customerId": "550e8400-e29b-41d4-a716-446655440000",
  "customerName": "Acme Corp",
  "customerEmail": "billing@acme.com",
  "expiresAt": "2026-05-13T17:30:00.000Z",
  "isExpired": false,
  "isUsed": true,
  "usedAt": "2026-05-13T16:45:12.000Z",
  "createdAt": "2026-05-13T16:30:00.000Z",
  "features": ["invoices", "subscriptions", "usage", "profile"]
}
```

***

### Revoke a portal session

```
DELETE /v1/portal-sessions/{sessionId}
```

Immediately invalidates the link. Use this if you sent a link to the wrong customer or your support team needs to cut access early.

Returns `204 No Content` on success, `404 Not Found` if the session doesn't exist.

***

## Security notes

* **Always mint links on your backend.** Portal sessions require a secret key. Treat that key like a database password.
* **One link, one click.** The link is consumed on first open. Anyone (including the original customer) who tries it a second time gets an error.
* **Tokens are only shown once.** The `token` and `url` fields are returned only by `create`. Lost URLs cannot be retrieved; revoke and mint a new session.
* **Need to cut access early?** Use the `revoke` endpoint. It works immediately.
