Manage a subscription
A one-hour link to the page where the buyer manages one subscription, with a button back to your app: request, response, return URL rules, the flow and test subscriptions.
POST /api/v1/subscriptions/{id}/manage-link returns a link to the page where the buyer manages ONE of your subscriptions: the same page the renewal and failed-charge emails lead to — the period, the next charge, cancelling at the end of the period. Put a "Manage subscription" button in your application and open the link when the user presses it. The subscription is named by its id from the customer state (subscriptions[].id); the body is optional.
The server API is for a partner whose application knows its own user: a payment is created from your server, with no button or panel on a page, and the user's state (subscriptions and purchases) is read by your own account reference. The key is the same as for the access check (API key tab in the cabinet), sent as Authorization: Bearer; the partner is identified by the key, there is no partner parameter. Rate limits are the same as for the access check: per your server's address and per key. Responses are JSON only and never cached. You can ask only about your own products and buyers: a foreign product or buyer and a non-existent one answer alike.
Request
POST https://app.norelio.eu/api/v1/subscriptions/{id}/manage-link
Authorization: Bearer {api key}
Content-Type: application/json| Field | Description |
|---|---|
returnUrl | Optional. Where the "Return to the app" button on the page leads: an https address on one of the return sites allowed for your account, by the same rule as the return URL after payment, up to 2000 characters. Without it the page has no such button. |
curl -s -X POST "https://app.norelio.eu/api/v1/subscriptions/sub_4d5e6f/manage-link" \
-H "Authorization: Bearer $NORELIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"returnUrl":"https://pawclaw.com/account"}'The return address follows the rules of Return URL. A returnUrl that fails the check does not stop the link: it is created without the return button, and warnings carries return_url_rejected ("returnUrl was ignored: it is not an https address on one of the return sites allowed for your account. The link was created without the button that returns the buyer to your app.").
Response
201 Created with these fields:
| Field | Description |
|---|---|
subscriptionId | The subscription id you asked about. |
url | The address of the subscription page, on Norelio's payment domain. Open it in the user's browser as a normal navigation. |
expiresAt | Until when the link works (ISO 8601): 60 minutes after it was created. After that the page says the link does not work; create a new link on every press of the button. |
warnings | Warnings, empty when everything was accepted. Each is an object with code and message. |
test | true for a test subscription (asked with the test key): the link leads to the test subscription page; false for a live one. |
{
"subscriptionId": "sub_4d5e6f",
"url": "https://pay.lis-system.eu/subscription/eyJpZCI6InN1Yl80ZDVlNmYiLCJleHAiOjE3NzM0ODQ0MTN9.Qm9ndXNTaWduYXR1cmVFeGFtcGxl",
"expiresAt": "2026-03-14T10:26:53.000Z",
"warnings": [],
"test": false
}What the buyer sees
The page shows the subscription to whoever opens the link, without signing in, and lets them cancel it at the end of the period. Links in the emails Norelio sends the buyer keep working as before and do not expire; a link from the API lives 60 minutes and carries your return button. The return address is checked again every time the page is shown: remove the site from your allowed return sites, and the button disappears from links already issued.
The whole flow
A "Manage subscription" button in your application:
- Your server reads the customer state by the user's account reference and takes the id of the subscription from subscriptions[].
- When the user presses the button, your server calls the manage-link endpoint with that id and, optionally, returnUrl — the page of your application to come back to.
- Send the user to url as a normal browser navigation. They see the subscription on Norelio's page and can cancel it; "Return to the app" brings them back to returnUrl.
- A cancellation by the buyer arrives as subscription.canceled for a product delivered by webhook, and as customer.state_changed for any product when it is on; the access check and the customer state show it at once.
The subscription id comes from Customer state; the cancellation arrives as described in Webhooks.
Test mode
With the test key, pass the id of a test subscription (it starts with test_): the link leads to the test subscription page on Norelio's payment domain (/test-checkout/subscription/…), marked as test mode, with a Cancel subscription button. Cancelling there sends subscription.canceled with test: true to your endpoint, as the cancel action of a test order does. The live key does not find test subscriptions, and the test key does not find live ones. See Test mode.
Errors
An error from any API endpoint is JSON of one shape: an error object with code (one of the codes below) and message (a short English sentence). A 429 or 503 response carries a Retry-After header. The access check keeps its own, older error shape.
| Status | Code | When |
|---|---|---|
429 | rate_limited | too many requests in a row, from your address or with your key |
503 | rate_limiter_busy | the request could not be counted against the rate limits, so nothing was done; send the same request again after the number of seconds in the Retry-After header |
401 | invalid_api_key | the key is missing or does not match |
400 | invalid_request | the body or the query is malformed; message says what exactly |
403 | direct_checkout_not_enabled | creating payments from your server is not yet enabled for your account: the platform first reviews your purchase terms (the same review as for direct checkout) |
404 | product_not_found | no product with this id belongs to your account, or it cannot be bought right now |
502 | checkout_failed | the payment provider could not start the payment; retry later |
404 | not_found | no such endpoint |
405 | method_not_allowed | the endpoint does not accept that method |
403 | test_key_required | the endpoint works only with the test API key (it starts with nrl_test_), and the request came with the live key |
404 | test_order_not_found | no test order with this id belongs to your account, or it has already been deleted: test orders live for 30 days |
409 | test_action_not_allowed | the action is not possible in the test order's current state, such as a refund of an order not yet paid or a renewal of a canceled subscription; message names the reason |
409 | insufficient_balance | the balance is lower than the amount to debit; nothing was debited, and the response carries the current balance in the balance field next to error |
422 | idempotency_conflict | a request with this idempotency key was already processed with a different body; use a new key for a new operation. A balance debit or grant answers 422 (idempotencyKey in the body), a payment created with the Idempotency-Key header answers 409 |
409 | idempotency_in_progress | a payment request with this Idempotency-Key is still being processed; send the same request again after the number of seconds in Retry-After and you get its response |
404 | subscription_not_found | no subscription with this id belongs to your account in this mode: a live key sees only live subscriptions, a test key only test ones |
{
"error": {
"code": "direct_checkout_not_enabled",
"message": "Creating payments from your server is enabled after the platform has reviewed your purchase terms. Ask support from the cabinet's Messages section."
}
}