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")— yourreturn_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
- API → Portal sessions — what the portal lets buyers do.
- Customers — the resource portal sessions are scoped against.
- Subscriptions — the portal can cancel/pause these.
- Checkout settings — branding that also applies to the portal.