Create a payment from your server
POST a product id and your account reference from your server and get a payment address to send the user to: request, response, warnings, error codes and the full flow with the webhook.
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.
POST /api/v1/checkouts creates an order and returns the address to send the user to. The product is named by its id from the cabinet catalog; your account reference for the user is required, since without it the payment cannot be credited to anyone. From there everything works as for a widget purchase: the order.paid webhook with that reference, the result page, the return to your site.
Request
POST https://app.norelio.eu/api/v1/checkouts
Authorization: Bearer {api key}
Content-Type: application/json| Field | Description |
|---|---|
productId | Required. The product id from the cabinet catalog. |
clientReference | Required. Your account reference for the user, in the client reference format: up to 128 characters, Latin letters, digits and . _ : -, not starting with "-" or ".". A malformed value is a 400 error, not an empty field: here there is no buyer who has already pressed "Pay". |
customerEmail | Optional. The user's email, if you know it: it is pre-filled in the payment provider's form so the buyer does not type it. The order's email is still set by the payment, from the provider's data. |
returnUrl | Optional. Where to bring the buyer after payment, by the rules of the return URL section: an https address on one of the return sites allowed for your account, up to 2000 characters. An address that fails the rules does not stop the purchase: the order is created without a return address and the response carries a warning. |
buyer | Optional. An object with ip (the buyer's IPv4 or IPv6 address) and userAgent (the buyer's browser): what your server knows about the user's request. Both go into the consent record; your server's own address never does. An invalid ip is a 400 error. |
packageId | Top-up products only: the package number from the product card (1 to 6, in the order the partner listed them). Exactly one of packageId and amount for a top-up; neither for any other product — otherwise 400 invalid_request. The server takes the price and the credits from the package itself. |
amount | Top-up products only: the amount to pay in the smallest currency unit of the product's currency (1250 means 12.50), within the range the partner set for free amounts. The server checks the range and computes the credits by the product's rate. Exactly one of packageId and amount for a top-up; neither for any other product. |
{
"productId": "prod_1a2b3c",
"clientReference": "acc_10294",
"customerEmail": "buyer@example.com",
"returnUrl": "https://pawclaw.com/thank-you",
"buyer": {
"ip": "203.0.113.7",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64)"
}
}curl -s -X POST "https://app.norelio.eu/api/v1/checkouts" \
-H "Authorization: Bearer $NORELIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"productId":"prod_1a2b3c","clientReference":"acc_10294","customerEmail":"buyer@example.com","returnUrl":"https://pawclaw.com/thank-you","buyer":{"ip":"203.0.113.7","userAgent":"Mozilla/5.0 (X11; Linux x86_64)"}}'Response
201 Created with these fields:
| Field | Description |
|---|---|
id | The order id — the same paymentId that arrives in the webhook. |
url | The address to send the user to. It is on Norelio's payment domain: the page issues the buyer's receipt key, stores it in a cookie and immediately forwards them to the payment provider. The address is single-use: it can be opened once. |
expiresAt | Until when the address still accepts the buyer (ISO 8601): the payment provider's session lifetime. After it, create a new payment. |
clientReference | The account reference stored on the order — the one you passed. |
warnings | Warnings, empty when everything was accepted. Each is an object with code and message. |
credits | Top-ups: the number of credits this payment buys, computed by Norelio from the package or the amount; null for any other product. The same number arrives in the webhook. |
creditsUnit | Top-ups: the unit name from the product card; null for any other product. |
test | true for a payment created with the test API key: no money and no payment provider, url leads to the test payment page; false for a live payment. |
{
"id": "ord_7k3m2p",
"url": "https://pay.lis-system.eu/api/public/checkout/continue?order=ord_7k3m2p&key=k4Xw1QmR9zL2nT8vB0cY7pH5sJ3eF6gD",
"expiresAt": "2026-03-14T10:26:53.000Z",
"clientReference": "acc_10294",
"warnings": []
}Open the url from the response in the user's browser as a normal navigation (a link or a redirect), not as a request from your server: the page at that address issues the buyer's receipt key, stores it in a cookie — as the checkout panel and direct checkout do — and forwards them to the payment provider. The address is SINGLE-USE: it carries a separate handoff key that is spent on the first open; a second open is refused. The receipt key never travels in the address — it is issued in the buyer's browser. Show the address to that user only and do not write it to logs: whoever opens it first receives the receipt key. The address expires at expiresAt. The result page and the return to your site after payment work as with direct checkout; on the Stripe page a line under the pay button links to the purchase terms.
Warnings
| Code | When |
|---|---|
return_url_rejected | the returnUrl you passed failed the check; the order was created without a return address |
{
"id": "ord_7k3m2p",
"url": "https://pay.lis-system.eu/api/public/checkout/continue?order=ord_7k3m2p&key=k4Xw1QmR9zL2nT8vB0cY7pH5sJ3eF6gD",
"expiresAt": "2026-03-14T10:26:53.000Z",
"clientReference": "acc_10294",
"warnings": [
{
"code": "return_url_rejected",
"message": "returnUrl was ignored: it is not an https address on one of the return sites allowed for your account. The payment was created without a return address."
}
]
}Repeated calls and limits
A repeated call with the same data creates a NEW payment rather than returning the previous one: there is no idempotency. Orders the user never paid are removed by Norelio's nightly cleanup; they never enter payouts or the customer state.
On top of the per-address and per-key limits, the payment-attempt limit per buyer address — the same one the panel and the button use — applies when you pass buyer.ip. Without buyer.ip no per-buyer limit is counted: the request's address is your server's.
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 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 |
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 |
{
"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."
}
}The whole flow
How it fits together for an application with a subscription:
- The user presses "Pay" in your application. Your server calls the create-payment endpoint with productId, your account reference for the user and, when known, the user's email, IP address and browser.
- Send the user to the url from the response as a normal browser navigation. They pay on the provider's page, land on the Norelio result page and, with the return enabled, come back to you.
- Your server receives the order.paid webhook with paymentId (the id from the response) and clientReference — your account reference. Verify the signature and credit that account.
- When the user comes back, ask the customer state by clientReference, or the access check by product: both already account for renewals, refunds, grace and closed access.
The webhook is described in Webhooks, the state in Customer state, the access check in Access check.
