Self-service
Customer portal
Mint a session for a customer, then let them cancel, pause, change plan, or pull invoices from buttons in your own app. The token is the only credential, so customers never log into MeridaPay.
How it works
Mint a portal session for a specific customer with your API key. You get back a single-use token, scoped to that customer and good for 24 hours. Pass the token to the portal REST endpoints to cancel, pause, change a plan, or pull invoices on the customer's behalf, with no MeridaPay login required.
Keep the token on your server and drive the endpoints from buttons in your own app. The customer manages their subscription without ever leaving your product. The Add a cancel button to your app section below walks through the full flow.
Mint a session
Create a session for the customer you want to give access to. Authenticate
with your secret API key. Look the customer up by their MeridaPay id, your own
external_customer_id, or their email.
curl -X POST https://api.meridapay.com/portal/sessions \
-H "Authorization: Bearer mrd_live_..." \
-H "Content-Type: application/json" \
-d '{ "customer_email": "[email protected]" }'
customer_iduuidThe MeridaPay customer UUID. Provide exactly one of the three lookup keys.
external_customer_idstringYour own customer id, the same value you passed on the original checkout.
customer_emailstringThe customer's email. Use it when that's all you have, for example a buyer
who paid through a hosted buy link. Resolves only when exactly one customer
matches. If several share the email you get a 409, so resolve the exact
one with GET /customers and pass customer_id instead.
{
"id": "ptk_8f5f7c7e...",
"expires_at": "2026-06-05T12:00:00Z"
}
id is the session token. Pass it in the path of the endpoints
below. Sessions expire after 24 hours, so mint a fresh one per visit.
REST endpoints
Every endpoint takes the token in the URL path, accepts JSON, and returns JSON. There's no Bearer header. The token is the auth.
Cancel a subscription
curl -X POST https://api.meridapay.com/portal/<token>/cancel \
-H "Content-Type: application/json" \
-d '{
"subscription_id": "<subscription-uuid>",
"mode": "period_end"
}'
subscription_iduuidrequiredThe subscription to cancel. Must belong to the account this token was minted for.
mode"immediate" | "period_end"Default "period_end". See Subscriptions.
Returns { id, cancellation_effective_at, is_active }.
Fires subscription.canceled webhook.
Pause a subscription
curl -X POST https://api.meridapay.com/portal/<token>/pause \
-H "Content-Type: application/json" \
-d '{
"subscription_id": "<subscription-uuid>",
"resumes_at": "2026-08-01T00:00:00Z",
"keep_invoices": false
}'
subscription_iduuidrequiredThe subscription to pause.
resumes_atISO 8601When the subscription should auto-resume. If omitted, pause is open-ended and only ends with an explicit resume call.
keep_invoicesbooleanWhen true, invoices keep generating on the normal schedule but charges
are deferred. Default false: no invoices while paused.
Resume a paused subscription
curl -X POST https://api.meridapay.com/portal/<token>/resume \
-H "Content-Type: application/json" \
-d '{ "subscription_id": "<subscription-uuid>" }'
Undo a scheduled cancellation
curl -X POST https://api.meridapay.com/portal/<token>/uncancel \
-H "Content-Type: application/json" \
-d '{ "subscription_id": "<subscription-uuid>" }'
Only valid while a period_end cancellation is still pending.
After the period ends, the subscription is gone. Re-subscribe via a fresh
checkout.
Errors
Every endpoint returns 400 with { "error": <message-or-issues> } when the
body fails validation or the token doesn't authorise the operation. There's
no separate 401: an invalid token surfaces as 400 invalid token.
Add a cancel button to your app
You don't have to send customers anywhere. Keep the session token on your server and let them cancel (or pause, or change plan) from a button in your own UI. The customer never sees the token.
1. On your server
Mint a session for the signed-in customer, then cancel the subscription.
Capture the subscription_id when the subscription starts: the
subscription.created webhook carries it as subscriptionId. Store it
against your user. (The signup payment.succeeded carries the same id as
subscriptionId too.)
// POST /account/cancel (your backend, behind your own auth)
const session = await fetch("https://api.meridapay.com/portal/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MERIDA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ external_customer_id: user.id }),
}).then((r) => r.json())
await fetch(`https://api.meridapay.com/portal/${session.id}/cancel`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ subscription_id: subscriptionId, mode: "period_end" }),
})
2. In your UI
The button just calls your own endpoint. The token stays server-side.
<button onClick={() => fetch("/account/cancel", { method: "POST" })}>
Cancel subscription
</button>
The same token works for pause, resume, uncancel, and change-plan, so
the same pattern powers a full set of self-service controls without shipping
a hosted page.
Next up
Invoices →