Portal sessions

A portal session is a short-lived signed URL that drops your customer into Plugipay's hosted customer billing portal. From the portal they can view receipts, update their saved payment method, cancel a subscription, download invoices — the whole self-serve surface, without you building any of it. This page covers the plugipay.portalSessions namespace; for the underlying HTTP surface see API: Portal sessions, and for the portal feature set see Portal → Customers.

Namespace

plugipay.portalSessions — every method on this namespace:

plugipay.portalSessions.create(input)

Portal sessions are mint-and-redirect. There's no list / get / cancel — each session is single-use, short-lived (5 minutes by default), and bound to the customer it was minted for.

Methods

portalSessions.create

Signature. plugipay.portalSessions.create(input): Promise<PortalSession>

Creates a portal session for a specific customer. Returns a PortalSession with a url you immediately redirect the customer to. The SDK auto-attaches an Idempotency-Key.

import { PlugipayClient } from '@forjio/plugipay-node';

const plugipay = new PlugipayClient({
  keyId: process.env.PLUGIPAY_KEY_ID!,
  secret: process.env.PLUGIPAY_SECRET!,
});

const session = await plugipay.portalSessions.create({
  customerId: 'cus_01HX...',
  returnUrl: 'https://yourapp.com/account/billing',
});

console.log(session.url);
// → 'https://plugipay.com/p/...' — redirect the customer here

The returnUrl is where Plugipay sends the customer when they click "Done" or close the portal — usually a page in your app that says "thanks, your billing settings are updated."

Mint per-redirect, don't cache. Portal sessions are short-lived (around 5 minutes) and single-use. Don't cache the URL on the server, don't embed it in an email link, don't store it in the customer record. Mint a fresh one each time the customer clicks "manage billing."

Types

interface PortalSession {
  id: string;                  // 'pls_...'
  customerId: string;
  url: string;                 // the signed redirect URL
  returnUrl: string;
  expiresAt: string;           // ISO-8601; usually 5 minutes from createdAt
}

For the full reference (including the portal feature toggles, branding inheritance from Checkout settings, and the URL signature scheme), see API: Portal sessions.

Common patterns

"Manage billing" button

Server-side handler — mint the session, redirect, done:

// Express:
app.get('/account/manage-billing', requireAuth, async (req, res) => {
  const user = req.user!;
  const session = await plugipay.portalSessions.create({
    customerId: user.plugipayCustomerId,
    returnUrl: `${process.env.APP_URL}/account/billing`,
  });
  res.redirect(303, session.url);
});

The client-side button is then just a regular link — no fetch / no JSON / no window.location.replace plumbing required:

<a href="/account/manage-billing">Manage billing</a>

Embed deep-link via query parameter

The portal supports deep-linking to specific tabs via query parameters on the returnUrl you pass through. Plugipay echoes selected actions back via webhooks (e.g. subscription cancellation), so your returnUrl doesn't need to carry state — just take the customer back to the right page in your app.

const session = await plugipay.portalSessions.create({
  customerId: 'cus_01HX...',
  returnUrl: `https://yourapp.com/account/billing?from=portal`,
});

Generate a session for support to share

When a support agent is helping a customer over chat, they often want a one-click link to drop the customer into their billing portal. Same call, different returnUrl:

async function supportPortalLink(customerId: string) {
  const session = await plugipay.portalSessions.create({
    customerId,
    returnUrl: 'https://yourapp.com/support/billing-thanks',
  });
  return session.url;
}

Paste the resulting URL into your support tool. The customer clicks, lands in the portal, makes the change, gets dropped back to your "thanks for updating" page. Don't put this URL in an email — by the time they click it, it'll have expired.

Per-merchant portal sessions (platform admin only)

If you're a platform admin acting on behalf of a merchant, pass onBehalfOf either on the client or on the call:

const platform = new PlugipayClient({
  keyId: process.env.PLUGIPAY_PLATFORM_KEY_ID!,
  secret: process.env.PLUGIPAY_PLATFORM_SECRET!,
});

// Option 1: scoped sub-client
const merchant = platform.forMerchant('acc_01HX...');
const session = await merchant.portalSessions.create({
  customerId: 'cus_01HX...',
  returnUrl: 'https://merchant-site.com/done',
});

// Option 2: low-level escape hatch with one-off override
const session2 = await platform.request<PortalSession>({
  method: 'POST',
  path: '/api/v1/portal-sessions',
  body: { customerId: 'cus_01HX...', returnUrl: 'https://merchant-site.com/done' },
  onBehalfOf: 'acc_01HX...',
});

Handle the redirect-back

Plugipay redirects back to your returnUrl when the customer is done. Treat this page as informational only — don't trust query parameters on it for any state mutation. The source of truth for "they cancelled / they updated their card" is the webhook stream:

// app/account/billing/page.tsx (Next.js)
export default function BillingPage() {
  return (
    <div>
      <h1>Billing settings updated</h1>
      <p>Your changes are being processed. Check your email for confirmation.</p>
    </div>
  );
}

// app/webhooks/plugipay.ts
import { verifyWebhook } from '@forjio/plugipay-node';

export async function POST(req: Request) {
  const event = verifyWebhook({
    body: await req.text(),
    signature: req.headers.get('Plugipay-Signature')!,
    secret: process.env.PLUGIPAY_WEBHOOK_SECRET!,
  });

  if (event.type === 'plugipay.subscription.canceled.v1') {
    await markSubCanceledInYourDb(event.data.object);
  }
  return new Response('ok');
}

Errors

Code Status Cause
validation_error 400 Bad returnUrl (not HTTPS, malformed), missing customerId.
not_found 404 Customer ID doesn't exist or is in another workspace.
forbidden 403 Key lacks plugipay:portal:create scope.

Next

Plugipay — Payments that don't tax your success