Billing Portal
Generate a hosted billing portal URL for a logged-in customer.
Endpoint
POST /api/portal/token/
Headers
| Header | Value |
|---|---|
X-Portal-Signature | HMAC-SHA512 hex digest of the raw request body |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
cws_id | string | Yes | Application identifier |
ref_id | string | Yes | Logged-in customer's Reference ID |
timestamp | integer | Yes | Current unix timestamp (seconds) |
Body must be serialised with keys in this exact order: cws_id, then ref_id, then timestamp.
Signature
Compute HMAC-SHA512 of the raw request body bytes using your PORTAL_SECRET. Hex-encode the result and send it in the X-Portal-Signature header.
Serialise the body once, sign that string, then send that exact string — do not re-serialise or allow an HTTP library to reformat it. Key order affects the signature.
// Bun
import { createHmac } from 'crypto';
const timestamp = Math.floor(Date.now() / 1000);
const body = `cws_id=${CWS_ID}&ref_id=${uid}×tamp=${timestamp}`;
const sig = createHmac('sha512', PORTAL_SECRET).update(body).digest('hex');
await fetch('https://pay.with.pink/api/portal/token/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Portal-Signature': sig,
},
body,
});
# Python
import hmac, hashlib, time, requests
timestamp = int(time.time())
body = f"cws_id={CWS_ID}&ref_id={uid}×tamp={timestamp}"
sig = hmac.new(PORTAL_SECRET.encode(), body.encode(), hashlib.sha512).hexdigest()
resp = requests.post(
'https://pay.with.pink/api/portal/token/',
data=body, # pass raw string — do not use a dict (re-serialisation breaks the signature)
headers={
'Content-Type': 'application/x-www-form-urlencoded',
'X-Portal-Signature': sig,
},
)
Response
{
"url": "https://pay.with.pink/portal/Mj3k9x/"
}
| Field | Type | Description |
|---|---|---|
url | string | Redirect the customer to this URL |
Errors
Always check the HTTP status code before parsing the response body. On error the body is {"error": "<message>"}.
| Status | Error message | Cause |
|---|---|---|
| 400 | cws_id required | cws_id missing from body |
| 400 | ref_id required | ref_id missing from body |
| 401 | Invalid signature | X-Portal-Signature does not match — check key order and raw-body HMAC |
| 401 | Request expired | timestamp missing, zero, or outside ±5 min window |
| 404 | Account was not found. | ref_id has no PWP account |
| 500 | Portal not configured | PWP-side configuration issue — contact PWP |
Token Lifetime
| Property | Value |
|---|---|
| Valid for | 1 hour from generation |
| After expiry | Portal page shows: "This link has expired. Return to Autoview to access your billing portal." |
| Re-issue | Call this endpoint again — a new token is created with a fresh 1-hour window |
Generate the token on demand (when the user clicks the billing button) rather than pre-generating.