---
title: "Mint a customer portal session"
description: "Self-serve payment-method management."
canonical_url: "https://twominutebilling.com/help/developers/api-reference/payment-methods/mint-a-customer-portal-session"
---
# Mint a customer portal session

> Self-serve payment-method management.

<api-operation method="POST" path="/v1/businesses/{biz}/customers/{cus}/portal_sessions">
<template v-slot:docs="">

Returns a SINGLE-USE entry credential (returned exactly once) and the /portal/s/<token> link the buyer opens to manage their payment methods. The org API key never reaches the buyer. The link is valid for ONE HOUR (`access_expires_at`) and is spent the first time it is opened — opening it exchanges the credential for a session held in an HttpOnly cookie, so do not persist or re-send the URL. `expires_at` is the ceiling on that session, not on the link.

## Path parameters

<table>
<thead>
  <tr>
    <th>
      Parameter
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Required
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        biz
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <strong>
        yes
      </strong>
    </td>
    
    <td>
      Business id (biz_…)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        cus
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <strong>
        yes
      </strong>
    </td>
    
    <td>
      Customer id (cus_…)
    </td>
  </tr>
</tbody>
</table>

## Headers

<table>
<thead>
  <tr>
    <th>
      Parameter
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Required
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Idempotency-Key
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      no
    </td>
    
    <td>
      Safely retry writes — repeats return the original response. If a write FAILED after it had already taken effect, that failure is what repeats: the key is spent, and a new attempt needs a new key.
    </td>
  </tr>
</tbody>
</table>

## Body

The request body is optional.

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Required
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        link_kind
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      no
    </td>
    
    <td>
      How long the LINK stays openable. 'api' (default) lasts one hour and suits minting a link you immediately redirect an already-authenticated user to. 'email' lasts five days — use it when the link goes into a message a human reads later, because an hour expires before most people open their mail. Neither makes the link multi-use: it is spent on first open. Read <code>
        access_expires_at
      </code>
      
       for the actual deadline. One of <code>
        api
      </code>
      
      , <code>
        email
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        redirect_url
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      no
    </td>
    
    <td>
      http(s). Where the buyer is sent after signing out of the portal. Omit to show the portal's own signed-out page.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        forward_url
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      no
    </td>
    
    <td>
      The portal page the link opens on. Omit to open <code>
        /portal
      </code>
      
       and let the portal pick its section. One of <code>
        /portal
      </code>
      
      , <code>
        /portal?tab=invoices
      </code>
      
      , <code>
        /portal?tab=plan
      </code>
      
      , <code>
        /portal?tab=methods
      </code>
      
      , <code>
        /portal?tab=details
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        action
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      no
    </td>
    
    <td>
      Open the portal on one task. If the business turned the task off, the link opens the portal home. Do not send with forward_url. One of <code>
        pay_invoice
      </code>
      
      , <code>
        update_card
      </code>
      
      , <code>
        change_plan
      </code>
      
      , <code>
        cancel_subscription
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        object
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      no
    </td>
    
    <td>
      The task's invoice (pay_invoice) or subscription (change_plan, cancel_subscription). It must belong to this customer.
    </td>
  </tr>
</tbody>
</table>

## Response

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        object
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      The object type; always <code>
        portal_session
      </code>
      
      . One of <code>
        portal_session
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        id
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      The portal session id.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        status
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Always <code>
        created
      </code>
      
       on a new session.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        token
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      The bearer token for the portal link. We never store it, so save it now.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        url
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      The portal link. Present only when the portal base URL is set.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        access_expires_at
      </code>
    </td>
    
    <td>
      <code>
        string · ISO-8601
      </code>
    </td>
    
    <td>
      When the link stops working for a first sign-in.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        expires_at
      </code>
    </td>
    
    <td>
      <code>
        string · ISO-8601
      </code>
    </td>
    
    <td>
      The latest time the session can last.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        redirect_url
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Where the portal sends the customer when they leave.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        forward_url
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      The portal page the link opens on.
    </td>
  </tr>
</tbody>
</table>

## Status codes

<table>
<thead>
  <tr>
    <th>
      Status
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        201
      </code>
    </td>
    
    <td>
      Portal session (id, status, token, url?, access_expires_at, expires_at)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        400
      </code>
    </td>
    
    <td>
      Invalid link_kind, redirect_url or forward_url
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        404
      </code>
    </td>
    
    <td>
      No such customer
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        409
      </code>
    </td>
    
    <td>
      Idempotency-Key conflict — the key is already in flight, or was reused with a different request payload.
    </td>
  </tr>
</tbody>
</table>
</template>

<template v-slot:request="">

```bash [Request]
curl -X POST "$TMBILLING_URL/v1/businesses/biz_XXXXXXXXXXXX01/customers/cus_XXXXXXXXXXXX01/portal_sessions" \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "link_kind": "api",
    "redirect_url": "https://example.com/webhooks/billing"
  }'
```

</template>

<template v-slot:response="">
<code-group>

```json [201 Created]
{
  "object": "portal_session",
  "id": "psess_XXXXXXXXXXXX01",
  "status": "created",
  "token": "pstk_XXXXXXXXXXXXXXXXXXXXXXXX",
  "url": "https://portal.example.com/portal/s/pstk_XXXXXXXXXXXXXXXXXXXXXXXX",
  "access_expires_at": "2026-04-14T09:30:00Z",
  "expires_at": "2026-04-14T09:30:00Z",
  "redirect_url": "https://app.example.com/billing",
  "forward_url": "/portal?tab=methods"
}
```

```json [400 Bad Request]
{
  "error": {
    "type": "invalid_request_error",
    "message": "Invalid link_kind, redirect_url or forward_url"
  }
}
```

```json [404 Not Found]
{
  "error": {
    "type": "invalid_request_error",
    "message": "No such customer"
  }
}
```

```json [409 Conflict]
{
  "error": {
    "type": "idempotency_error",
    "message": "Idempotency-Key conflict — the key is already in flight, or was reused with a different request payload."
  }
}
```

</code-group>
</template>
</api-operation>


## Sitemap

See the full [sitemap](https://twominutebilling.com/sitemap.md) for all pages.
