Portal sessions

A portal session is a short-lived signed URL into Plugipay's hosted customer portal — the page where a buyer can manage their payment methods, view invoices, download receipts, and cancel subscriptions. You create the session server-side; you redirect the buyer to the returned URL; Plugipay handles authentication via the session token. The Python SDK exposes a single method behind plugipay.portal_sessions. For wire format and what the portal supports, see API → Portal sessions.

Namespace

plug.portal_sessions    # _PortalSessions

Methods

create

plug.portal_sessions.create(
    *,
    customer_id: str,
    return_url: str,
) -> PortalSession

Creates a portal session for customer_id and returns a hosted URL. return_url is where the buyer is sent when they click "back to site" inside the portal. SDK auto-generates an idempotency key.

session = plug.portal_sessions.create(
    customer_id="cus_01HX...",
    return_url="https://app.example.com/account/billing",
)
print(session["url"])           # one-time signed portal URL
print(session["expiresAt"])     # ISO 8601 UTC; typically minutes

The URL is single-use. Once the buyer hits it, they get a server-side session cookie inside the portal domain — the URL itself can't be reused, replayed, or shared (the portal cookie is httpOnly and tied to that browser).

Don't email portal URLs. They're short-lived and one-shot. Render them from a server-side handler in response to a logged-in buyer click — "Manage billing" button → POST to your backend → call this method → redirect.

Types

from plugipay import PortalSession

PortalSession is a Resource subclass. Likely-read fields:

  • session["url"] — hosted portal URL; redirect the buyer here.
  • session["expiresAt"] — ISO 8601 UTC; typically 5–15 minutes from creation.
  • session["customerId"] — echoes the buyer.
  • session.get("returnUrl") — your return_url.

There is no get or list — sessions are write-only ephemera. Once you've handed the URL to the buyer, the SDK has no further visibility.

Common patterns

Server-rendered "Manage billing" handler. The canonical integration:

from flask import Flask, redirect, session as user_session

app = Flask(__name__)

@app.post("/account/billing/portal")
def open_portal():
    customer_id = user_session["plugipay_customer_id"]
    portal = plug.portal_sessions.create(
        customer_id=customer_id,
        return_url="https://app.example.com/account/billing",
    )
    return redirect(portal["url"], code=303)

The 303 redirect avoids the browser caching the one-shot URL.

FastAPI + auth dependency.

from fastapi import FastAPI, Depends
from fastapi.responses import RedirectResponse

app = FastAPI()

@app.post("/account/billing/portal")
async def open_portal(user = Depends(require_auth)):
    portal = plug.portal_sessions.create(
        customer_id=user.plugipay_customer_id,
        return_url="https://app.example.com/account/billing",
    )
    return RedirectResponse(url=portal["url"], status_code=303)

Don't pre-mint at page load. Each create call is one network round-trip and the resulting URL expires fast. Mint on click, not on render:

# BAD — every page render burns a portal session
@app.get("/account/billing")
def billing_page(user):
    portal = plug.portal_sessions.create(customer_id=user.cid, return_url="...")
    return render_template("billing.html", portal_url=portal["url"])

# GOOD — POST handler mints on click
@app.get("/account/billing")
def billing_page(user):
    return render_template("billing.html")    # has a form POSTing to /portal

Handle the rare "customer not found" case. The customer must exist in your workspace; cross-workspace ids return 404:

from plugipay import PlugipayError

try:
    portal = plug.portal_sessions.create(
        customer_id=customer_id,
        return_url=return_url,
    )
except PlugipayError as err:
    if err.status == 404:
        # Customer was deleted/migrated — fall back to manual flow
        return redirect("/account/billing/manual")
    raise

Embed-then-redirect for marketplaces. If you run a platform with on_behalf_of, mint portal sessions against the merchant's workspace:

merchant_client = plug.for_merchant("acc_merchant123")
portal = merchant_client.portal_sessions.create(
    customer_id="cus_buyer_in_merchant_workspace",
    return_url="https://platform.example.com/back",
)

The return_url should usually point back to your platform shell, not the merchant's site — the buyer started on your platform and expects to land back there.

Pre-warm-then-redirect with timing budget. The hosted portal has its own latency budget; if you're inside a slow page-load already, do the mint async and stream-redirect via a short HTML page rather than holding the buyer's request:

@app.post("/account/billing/portal")
def open_portal_async():
    customer_id = user_session["plugipay_customer_id"]
    # Don't block on the network round-trip in the request thread —
    # render a "redirecting…" page that polls a JSON endpoint
    return render_template("portal_redirect.html", task_id=enqueue_portal_mint(customer_id))

For most apps, the synchronous redirect is fine — Plugipay's portal-session endpoint typically returns under 200ms.

Log the expiresAt for debugging. If a buyer complains that the portal URL didn't work, it's almost always because they sat on a tab for too long. Pair the redirect with a structured log line:

log.info(
    "portal_session_minted",
    customer_id=customer_id,
    expires_at=portal["expiresAt"],
    return_url=portal.get("returnUrl"),
)

When a complaint lands, correlate by customer_id and check whether the click came after expires_at.

Errors

err.status err.code Cause
400 validation_error Missing customer_id / return_url, return_url not https (in live mode).
404 not_found Customer doesn't exist or is in another workspace.
409 idempotency_key_reused Same Idempotency-Key against a different body — note the SDK auto-generates these, so this should only happen if you reach for plug.request with your own key.
403 insufficient_scope Key lacks plugipay:portal:write.

All raise PlugipayError. See Errors.

Next

Plugipay — Payments that don't tax your success