{
  "openapi": "3.1.0",
  "info": {
    "title": "Norelio partner API",
    "version": "1",
    "summary": "The server API, the access check, the machine-readable schema and the partner webhook.",
    "description": "Norelio sells digital products and subscriptions on behalf of its partners. This document describes what a partner's server calls (create a payment, customer state, the access check) and what Norelio calls on the partner's server (the webhook). Human-readable documentation: https://docs.norelio.eu. The version number is the notification payload version: Payload fields are only ever ADDED: we never rename or remove an existing field and never change its meaning. An incompatible change is rare and always increments version. Your code can safely ignore fields it does not know: a new field never requires changing a check you have already written, and a version you have already seen keeps arriving in the same shape.",
    "contact": {
      "name": "Norelio Developers",
      "url": "https://docs.norelio.eu"
    }
  },
  "externalDocs": {
    "description": "Norelio Developers",
    "url": "https://docs.norelio.eu"
  },
  "servers": [
    {
      "url": "https://app.norelio.eu",
      "description": "Norelio"
    }
  ],
  "tags": [
    {
      "name": "Server API",
      "description": "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."
    },
    {
      "name": "Access check",
      "description": "Asked from the partner's server with the partner's API key."
    },
    {
      "name": "Schema",
      "description": "The same contract as JSON Schema, for code generators."
    },
    {
      "name": "Webhooks",
      "description": "Sent by Norelio to the endpoint the partner saved in the cabinet."
    }
  ],
  "paths": {
    "/api/v1/checkouts": {
      "post": {
        "operationId": "createCheckout",
        "tags": [
          "Server API"
        ],
        "summary": "Create a payment from your server",
        "description": "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. The endpoint works only after the platform has reviewed your purchase terms and allowed direct checkout for your account (see the direct checkout section): the buyer never sees a Norelio page before paying, so the consent to the purchase terms — and, for a subscription, to the recurring charges — is collected by your application before the call. What your terms must contain is described there. Without the permission the endpoint answers 403 direct_checkout_not_enabled and creates no order. For every order Norelio stores that the consent was collected in your application, the version and text of the terms, the date and author of the permission and, if you passed them, the buyer's IP address and browser: this record answers a payment dispute, so pass the buyer field.\n\nOpen 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.\n\nA 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.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutRequest"
              },
              "examples": {
                "full": {
                  "summary": "Every field",
                  "value": {
                    "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)"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The order is created; send the user to url.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutResponse"
                },
                "examples": {
                  "created": {
                    "summary": "Created",
                    "value": {
                      "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": []
                    }
                  },
                  "warning": {
                    "summary": "Created, the return address was rejected",
                    "value": {
                      "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."
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`: the body or the query is malformed; message says what exactly",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "invalid_request": {
                    "value": {
                      "error": {
                        "code": "invalid_request",
                        "message": "The request is malformed."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`invalid_api_key`: the key is missing or does not match",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "invalid_api_key": {
                    "value": {
                      "error": {
                        "code": "invalid_api_key",
                        "message": "Invalid or missing API key."
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`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)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "direct_checkout_not_enabled": {
                    "value": {
                      "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."
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "`product_not_found`: no product with this id belongs to your account, or it cannot be bought right now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "product_not_found": {
                    "value": {
                      "error": {
                        "code": "product_not_found",
                        "message": "No product with this id can be bought from your account right now."
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many requests in a row, from your address or with your key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "rate_limited": {
                    "value": {
                      "error": {
                        "code": "rate_limited",
                        "message": "Too many requests. Please wait a couple of minutes."
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "In how many seconds the request may be repeated.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "502": {
            "description": "`checkout_failed`: the payment provider could not start the payment; retry later",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "checkout_failed": {
                    "value": {
                      "error": {
                        "code": "checkout_failed",
                        "message": "The payment provider could not start the payment. Please try again later."
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/customers/state": {
      "get": {
        "operationId": "getCustomerState",
        "tags": [
          "Server API"
        ],
        "summary": "One buyer's subscriptions and purchases",
        "description": "GET /api/v1/customers/state returns the subscriptions and purchases of one of your buyers: what the partner sees in the cabinet's subscribers and order lists, for one person and machine-readable. It answers 200 always; for an unknown (or foreign) buyer both lists are empty. Exactly one query parameter: clientReference (your account reference, same format) or email (the buyer's email, lower-cased as in the cabinet). Both or neither is a 400 error. By account reference, the orders carrying it are found together with the subscriptions those orders started, including their renewals.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "clientReference",
            "in": "query",
            "required": false,
            "description": "Your account reference for the buyer. Exactly one of clientReference and email.",
            "schema": {
              "type": "string",
              "maxLength": 128,
              "pattern": "^[A-Za-z0-9_:][A-Za-z0-9._:-]{0,127}$"
            }
          },
          {
            "name": "email",
            "in": "query",
            "required": false,
            "description": "The buyer's email. Exactly one of clientReference and email.",
            "schema": {
              "type": "string",
              "format": "email"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The buyer's subscriptions and purchases; both lists are empty for an unknown buyer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerState"
                },
                "examples": {
                  "known": {
                    "summary": "A buyer with a subscription and two purchases",
                    "value": {
                      "subscriptions": [
                        {
                          "id": "sub_4d5e6f",
                          "productId": "prod_1a2b3c",
                          "status": "active",
                          "validUntil": "2026-04-14T09:26:53.000Z",
                          "currentPeriodEnd": "2026-04-14T09:26:53.000Z",
                          "cancelAtPeriodEnd": false,
                          "clientReference": "acc_10294"
                        }
                      ],
                      "purchases": [
                        {
                          "orderId": "ord_7k3m2p",
                          "productId": "prod_1a2b3c",
                          "paidAt": "2026-03-14T09:26:53.000Z",
                          "amount": 1990,
                          "currency": "EUR",
                          "status": "paid",
                          "clientReference": "acc_10294",
                          "refunded": false
                        },
                        {
                          "orderId": "ord_9z8y7x",
                          "productId": "prod_4d5e6f",
                          "paidAt": "2026-02-01T18:03:11.000Z",
                          "amount": 990,
                          "currency": "EUR",
                          "status": "refunded",
                          "clientReference": "acc_10294",
                          "refunded": true
                        }
                      ]
                    }
                  },
                  "unknown": {
                    "summary": "Unknown buyer",
                    "value": {
                      "subscriptions": [],
                      "purchases": []
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`: the body or the query is malformed; message says what exactly",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "invalid_request": {
                    "value": {
                      "error": {
                        "code": "invalid_request",
                        "message": "The request is malformed."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`invalid_api_key`: the key is missing or does not match",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "invalid_api_key": {
                    "value": {
                      "error": {
                        "code": "invalid_api_key",
                        "message": "Invalid or missing API key."
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many requests in a row, from your address or with your key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "rate_limited": {
                    "value": {
                      "error": {
                        "code": "rate_limited",
                        "message": "Too many requests. Please wait a couple of minutes."
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "In how many seconds the request may be repeated.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          }
        }
      }
    },
    "/api/public/access-check": {
      "get": {
        "operationId": "checkAccess",
        "tags": [
          "Access check"
        ],
        "summary": "Is subscription access for this email or account reference and product paid, and until when",
        "description": "GET /api/public/access-check: query parameters and the required Authorization header. The partner is identified by the key: there is no partner parameter. The response is never cached and must not be cached by the caller. Instead of the email you can pass your own account reference for the buyer: the clientReference query parameter, the same value you passed at checkout (see the client reference section). One of the two is required; with both, either may match: when the email has no access, the emails of this product's orders carrying that reference are checked. The response has a third field, clientReference: the reference of this email's latest order for this product (for a subscription renewal, of the order the subscription started with), or null. Existing calls with an email alone keep working unchanged. A recurring subscription charge arrives within about an hour after the end of the paid period. So that the buyer does not lose access for that time, the access check keeps a grace of 2 hours: hasAccess stays true for 2 hours after validUntil, while validUntil remains the date until which access is paid, without grace. If the renewal arrives, validUntil moves to the new period; if it does not, 2 hours after validUntil the answer becomes hasAccess: false.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "required": false,
            "description": "The buyer's email to check. Either it or clientReference is required.",
            "schema": {
              "type": "string",
              "format": "email"
            }
          },
          {
            "name": "clientReference",
            "in": "query",
            "required": false,
            "description": "Your account reference for the buyer, passed at checkout (the client reference). Either it or email is required; with both, either may match.",
            "schema": {
              "type": "string",
              "maxLength": 128,
              "pattern": "^[A-Za-z0-9_:][A-Za-z0-9._:-]{0,127}$"
            }
          },
          {
            "name": "productId",
            "in": "query",
            "required": true,
            "description": "Your product id.",
            "schema": {
              "type": "string",
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The answer, for a paid access and for everything else alike.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessCheckResponse"
                },
                "examples": {
                  "paid": {
                    "summary": "Access is paid",
                    "value": {
                      "hasAccess": true,
                      "validUntil": "2026-04-14T09:26:53.000Z",
                      "clientReference": "acc_10294"
                    }
                  },
                  "none": {
                    "summary": "No access",
                    "value": {
                      "hasAccess": false,
                      "validUntil": null,
                      "clientReference": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "the productId query parameter is missing, or neither email nor clientReference is given",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "the key is missing or does not match",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "too many requests in a row; the Retry-After header says in how many seconds to retry",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "In how many seconds the request may be repeated.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          }
        }
      }
    },
    "/api/public/webhook-schema": {
      "get": {
        "operationId": "getWebhookSchema",
        "tags": [
          "Schema"
        ],
        "summary": "JSON Schema of the notification payload and the access check",
        "description": "The notification payload, the access-check request and response, the error codes and the retry policy as one JSON Schema (draft 2020-12) document, with descriptions in Russian, like the cabinet. The same payload schema with English descriptions is under components.schemas here. Public, cacheable, no authentication.",
        "responses": {
          "200": {
            "description": "The schema document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "$schema",
                    "version",
                    "compatibility",
                    "notification",
                    "accessCheck",
                    "retryPolicy"
                  ],
                  "properties": {
                    "$schema": {
                      "type": "string"
                    },
                    "version": {
                      "type": "integer",
                      "const": 1
                    },
                    "compatibility": {
                      "type": "string"
                    },
                    "notification": {
                      "type": "object"
                    },
                    "accessCheck": {
                      "type": "object"
                    },
                    "retryPolicy": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "order.paid": {
      "post": {
        "operationId": "webhook_order_paid",
        "tags": [
          "Webhooks"
        ],
        "summary": "Payment received",
        "description": "The buyer paid the order. Grant access. The order counts as delivered when this notification is DELIVERED, not when it is sent.\n\nDelivery is at-least-once, not exactly-once: a cut between our request and the record of its outcome cannot be ruled out, so the same event occasionally arrives twice. Deduplicate on eventId — the same on every delivery of one event, different for every other event — and treat a repeat as an event you have already processed, not as a new one. Do not use paymentId and event for this: two partial refunds of one order share both. Do not use the x-norelio-webhook-id header either: it names one delivery attempt and is part of its signature.",
        "parameters": [
          {
            "name": "x-norelio-webhook-id",
            "in": "header",
            "required": true,
            "description": "Id of this delivery attempt. A retry carries a new id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "x-norelio-webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Send time in seconds since the Unix epoch. Reject a request older than 300 seconds.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "x-norelio-webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64>`: HMAC-SHA256 of `{id}.{timestamp}.{raw body}` with the partner's webhook secret, base64-encoded. Compare in constant time.",
            "schema": {
              "type": "string",
              "pattern": "^v1,[A-Za-z0-9+/]+=*$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerWebhookNotification"
              },
              "examples": {
                "payment": {
                  "summary": "A real payment",
                  "value": {
                    "version": 1,
                    "event": "order.paid",
                    "test": false,
                    "productId": "prod_1a2b3c",
                    "customerEmail": "buyer@example.com",
                    "paymentId": "ord_9z8y7x",
                    "amount": 1990,
                    "currency": "EUR",
                    "taxAmount": 418,
                    "occurredAt": "2026-03-14T09:26:53.000Z",
                    "accessValidUntil": null,
                    "clientReference": "acc_10294",
                    "credits": null,
                    "creditsUnit": null,
                    "refundedAmount": null,
                    "eventId": "cm8f3k2q90001ab7d9x2y4z6w"
                  }
                },
                "test": {
                  "summary": "A test notification sent from the cabinet",
                  "value": {
                    "version": 1,
                    "event": "order.paid",
                    "test": true,
                    "productId": "test_product",
                    "customerEmail": "buyer@example.com",
                    "paymentId": "test_1773480413000",
                    "amount": 1990,
                    "currency": "EUR",
                    "taxAmount": 0,
                    "occurredAt": "2026-03-14T09:26:53.000Z",
                    "accessValidUntil": null,
                    "clientReference": "acc_10294",
                    "credits": null,
                    "creditsUnit": null,
                    "refundedAmount": null,
                    "eventId": "test_cm8f3k2q90005gh2j4k6m8n0p"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Delivered. Any 2xx status counts as success; the response body is not read."
          },
          "default": {
            "description": "Failure: any non-2xx status, a network error or no response within 5 seconds. The delivery is retried, see x-norelio-retry-policy."
          }
        }
      }
    },
    "order.refunded_partial": {
      "post": {
        "operationId": "webhook_order_refunded_partial",
        "tags": [
          "Webhooks"
        ],
        "summary": "Partial refund",
        "description": "Part of the order amount was refunded to the buyer. Access usually stays open: this is not a full refund. For a subscription the access period is shortened in proportion to the refunded amount; the notification does not carry the new date, the access check does. refundedAmount says how much this refund returned; every partial refund is its own event with its own eventId.\n\nDelivery is at-least-once, not exactly-once: a cut between our request and the record of its outcome cannot be ruled out, so the same event occasionally arrives twice. Deduplicate on eventId — the same on every delivery of one event, different for every other event — and treat a repeat as an event you have already processed, not as a new one. Do not use paymentId and event for this: two partial refunds of one order share both. Do not use the x-norelio-webhook-id header either: it names one delivery attempt and is part of its signature.",
        "parameters": [
          {
            "name": "x-norelio-webhook-id",
            "in": "header",
            "required": true,
            "description": "Id of this delivery attempt. A retry carries a new id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "x-norelio-webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Send time in seconds since the Unix epoch. Reject a request older than 300 seconds.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "x-norelio-webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64>`: HMAC-SHA256 of `{id}.{timestamp}.{raw body}` with the partner's webhook secret, base64-encoded. Compare in constant time.",
            "schema": {
              "type": "string",
              "pattern": "^v1,[A-Za-z0-9+/]+=*$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerWebhookNotification"
              },
              "examples": {
                "example": {
                  "summary": "A partial refund notification",
                  "value": {
                    "version": 1,
                    "event": "order.refunded_partial",
                    "test": false,
                    "productId": "prod_1a2b3c",
                    "customerEmail": "buyer@example.com",
                    "paymentId": "ord_9z8y7x",
                    "amount": 1990,
                    "currency": "EUR",
                    "taxAmount": 418,
                    "occurredAt": "2026-03-14T09:26:53.000Z",
                    "accessValidUntil": null,
                    "clientReference": "acc_10294",
                    "credits": null,
                    "creditsUnit": null,
                    "refundedAmount": 995,
                    "eventId": "cm8f3k2q90001ab7d9x2y4z6b"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Delivered. Any 2xx status counts as success; the response body is not read."
          },
          "default": {
            "description": "Failure: any non-2xx status, a network error or no response within 5 seconds. The delivery is retried, see x-norelio-retry-policy."
          }
        }
      }
    },
    "order.refunded_full": {
      "post": {
        "operationId": "webhook_order_refunded_full",
        "tags": [
          "Webhooks"
        ],
        "summary": "Full refund",
        "description": "The whole order amount was refunded to the buyer. Close the access paid by this order. For a subscription the access is shortened by the full period of this payment, counted from the current end date: if the new date has already passed, open access closes at once; if it is still ahead, access stays open until that date plus 2 hours. The access check gives the exact answer. refundedAmount carries the amount of this refund — the remainder, if partial refunds came before.\n\nDelivery is at-least-once, not exactly-once: a cut between our request and the record of its outcome cannot be ruled out, so the same event occasionally arrives twice. Deduplicate on eventId — the same on every delivery of one event, different for every other event — and treat a repeat as an event you have already processed, not as a new one. Do not use paymentId and event for this: two partial refunds of one order share both. Do not use the x-norelio-webhook-id header either: it names one delivery attempt and is part of its signature.",
        "parameters": [
          {
            "name": "x-norelio-webhook-id",
            "in": "header",
            "required": true,
            "description": "Id of this delivery attempt. A retry carries a new id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "x-norelio-webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Send time in seconds since the Unix epoch. Reject a request older than 300 seconds.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "x-norelio-webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64>`: HMAC-SHA256 of `{id}.{timestamp}.{raw body}` with the partner's webhook secret, base64-encoded. Compare in constant time.",
            "schema": {
              "type": "string",
              "pattern": "^v1,[A-Za-z0-9+/]+=*$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerWebhookNotification"
              },
              "examples": {
                "example": {
                  "summary": "A full refund notification",
                  "value": {
                    "version": 1,
                    "event": "order.refunded_full",
                    "test": false,
                    "productId": "prod_1a2b3c",
                    "customerEmail": "buyer@example.com",
                    "paymentId": "ord_9z8y7x",
                    "amount": 1990,
                    "currency": "EUR",
                    "taxAmount": 418,
                    "occurredAt": "2026-03-14T09:26:53.000Z",
                    "accessValidUntil": null,
                    "clientReference": "acc_10294",
                    "credits": null,
                    "creditsUnit": null,
                    "refundedAmount": 1990,
                    "eventId": "cm8f3k2q90001ab7d9x2y4z6c"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Delivered. Any 2xx status counts as success; the response body is not read."
          },
          "default": {
            "description": "Failure: any non-2xx status, a network error or no response within 5 seconds. The delivery is retried, see x-norelio-retry-policy."
          }
        }
      }
    },
    "order.dispute_opened": {
      "post": {
        "operationId": "webhook_order_dispute_opened",
        "tags": [
          "Webhooks"
        ],
        "summary": "Dispute opened",
        "description": "The buyer disputed the payment with their bank or payment service. Until the dispute is resolved you may withhold access.\n\nDelivery is at-least-once, not exactly-once: a cut between our request and the record of its outcome cannot be ruled out, so the same event occasionally arrives twice. Deduplicate on eventId — the same on every delivery of one event, different for every other event — and treat a repeat as an event you have already processed, not as a new one. Do not use paymentId and event for this: two partial refunds of one order share both. Do not use the x-norelio-webhook-id header either: it names one delivery attempt and is part of its signature.",
        "parameters": [
          {
            "name": "x-norelio-webhook-id",
            "in": "header",
            "required": true,
            "description": "Id of this delivery attempt. A retry carries a new id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "x-norelio-webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Send time in seconds since the Unix epoch. Reject a request older than 300 seconds.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "x-norelio-webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64>`: HMAC-SHA256 of `{id}.{timestamp}.{raw body}` with the partner's webhook secret, base64-encoded. Compare in constant time.",
            "schema": {
              "type": "string",
              "pattern": "^v1,[A-Za-z0-9+/]+=*$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerWebhookNotification"
              },
              "examples": {
                "example": {
                  "summary": "A dispute opened notification",
                  "value": {
                    "version": 1,
                    "event": "order.dispute_opened",
                    "test": false,
                    "productId": "prod_1a2b3c",
                    "customerEmail": "buyer@example.com",
                    "paymentId": "ord_9z8y7x",
                    "amount": 1990,
                    "currency": "EUR",
                    "taxAmount": 418,
                    "occurredAt": "2026-03-14T09:26:53.000Z",
                    "accessValidUntil": null,
                    "clientReference": "acc_10294",
                    "credits": null,
                    "creditsUnit": null,
                    "refundedAmount": null,
                    "eventId": "cm8f3k2q90001ab7d9x2y4z6d"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Delivered. Any 2xx status counts as success; the response body is not read."
          },
          "default": {
            "description": "Failure: any non-2xx status, a network error or no response within 5 seconds. The delivery is retried, see x-norelio-retry-policy."
          }
        }
      }
    },
    "order.dispute_resolved": {
      "post": {
        "operationId": "webhook_order_dispute_resolved",
        "tags": [
          "Webhooks"
        ],
        "summary": "Dispute resolved",
        "description": "The dispute was closed, in your favour or in the buyer's. Sent once, when the outcome is known; intermediate stages of a dispute produce no event. The notification does not say who won. A lost dispute on a subscription affects access like a refund of the same amount: ask the access check.\n\nDelivery is at-least-once, not exactly-once: a cut between our request and the record of its outcome cannot be ruled out, so the same event occasionally arrives twice. Deduplicate on eventId — the same on every delivery of one event, different for every other event — and treat a repeat as an event you have already processed, not as a new one. Do not use paymentId and event for this: two partial refunds of one order share both. Do not use the x-norelio-webhook-id header either: it names one delivery attempt and is part of its signature.",
        "parameters": [
          {
            "name": "x-norelio-webhook-id",
            "in": "header",
            "required": true,
            "description": "Id of this delivery attempt. A retry carries a new id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "x-norelio-webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Send time in seconds since the Unix epoch. Reject a request older than 300 seconds.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "x-norelio-webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64>`: HMAC-SHA256 of `{id}.{timestamp}.{raw body}` with the partner's webhook secret, base64-encoded. Compare in constant time.",
            "schema": {
              "type": "string",
              "pattern": "^v1,[A-Za-z0-9+/]+=*$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerWebhookNotification"
              },
              "examples": {
                "example": {
                  "summary": "A dispute resolved notification",
                  "value": {
                    "version": 1,
                    "event": "order.dispute_resolved",
                    "test": false,
                    "productId": "prod_1a2b3c",
                    "customerEmail": "buyer@example.com",
                    "paymentId": "ord_9z8y7x",
                    "amount": 1990,
                    "currency": "EUR",
                    "taxAmount": 418,
                    "occurredAt": "2026-03-14T09:26:53.000Z",
                    "accessValidUntil": null,
                    "clientReference": "acc_10294",
                    "credits": null,
                    "creditsUnit": null,
                    "refundedAmount": null,
                    "eventId": "cm8f3k2q90001ab7d9x2y4z6e"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Delivered. Any 2xx status counts as success; the response body is not read."
          },
          "default": {
            "description": "Failure: any non-2xx status, a network error or no response within 5 seconds. The delivery is retried, see x-norelio-retry-policy."
          }
        }
      }
    },
    "subscription.renewed": {
      "post": {
        "operationId": "webhook_subscription_renewed",
        "tags": [
          "Webhooks"
        ],
        "summary": "Subscription renewed",
        "description": "A recurring charge succeeded and created a NEW order. Extend access for the new period. Usually arrives within an hour after the end of the paid period; for that time the access check keeps a grace of 2 hours after accessValidUntil. If you close access yourself by that date, keep the same grace. A renewal also reopens access closed by the seller, a full refund or a lost dispute.\n\nDelivery is at-least-once, not exactly-once: a cut between our request and the record of its outcome cannot be ruled out, so the same event occasionally arrives twice. Deduplicate on eventId — the same on every delivery of one event, different for every other event — and treat a repeat as an event you have already processed, not as a new one. Do not use paymentId and event for this: two partial refunds of one order share both. Do not use the x-norelio-webhook-id header either: it names one delivery attempt and is part of its signature.",
        "parameters": [
          {
            "name": "x-norelio-webhook-id",
            "in": "header",
            "required": true,
            "description": "Id of this delivery attempt. A retry carries a new id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "x-norelio-webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Send time in seconds since the Unix epoch. Reject a request older than 300 seconds.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "x-norelio-webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64>`: HMAC-SHA256 of `{id}.{timestamp}.{raw body}` with the partner's webhook secret, base64-encoded. Compare in constant time.",
            "schema": {
              "type": "string",
              "pattern": "^v1,[A-Za-z0-9+/]+=*$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerWebhookNotification"
              },
              "examples": {
                "example": {
                  "summary": "A subscription renewed notification",
                  "value": {
                    "version": 1,
                    "event": "subscription.renewed",
                    "test": false,
                    "productId": "prod_1a2b3c",
                    "customerEmail": "buyer@example.com",
                    "paymentId": "ord_9z8y7x",
                    "amount": 1990,
                    "currency": "EUR",
                    "taxAmount": 418,
                    "occurredAt": "2026-03-14T09:26:53.000Z",
                    "accessValidUntil": null,
                    "clientReference": "acc_10294",
                    "credits": null,
                    "creditsUnit": null,
                    "refundedAmount": null,
                    "eventId": "cm8f3k2q90001ab7d9x2y4z6f"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Delivered. Any 2xx status counts as success; the response body is not read."
          },
          "default": {
            "description": "Failure: any non-2xx status, a network error or no response within 5 seconds. The delivery is retried, see x-norelio-retry-policy."
          }
        }
      }
    },
    "subscription.ended": {
      "post": {
        "operationId": "webhook_subscription_ended",
        "tags": [
          "Webhooks"
        ],
        "summary": "Subscription ended",
        "description": "The subscription is over: the charge failed after all of the provider's retries, or the buyer, the partner or an administrator cancelled an OVERDUE subscription with no paid period left (such a cancellation ends the subscription at once). Close access. A normal cancellation during a paid period does not produce this event: subscription.canceled reports it, and subscription.ended follows when the period runs out.\n\nDelivery is at-least-once, not exactly-once: a cut between our request and the record of its outcome cannot be ruled out, so the same event occasionally arrives twice. Deduplicate on eventId — the same on every delivery of one event, different for every other event — and treat a repeat as an event you have already processed, not as a new one. Do not use paymentId and event for this: two partial refunds of one order share both. Do not use the x-norelio-webhook-id header either: it names one delivery attempt and is part of its signature.",
        "parameters": [
          {
            "name": "x-norelio-webhook-id",
            "in": "header",
            "required": true,
            "description": "Id of this delivery attempt. A retry carries a new id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "x-norelio-webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Send time in seconds since the Unix epoch. Reject a request older than 300 seconds.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "x-norelio-webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64>`: HMAC-SHA256 of `{id}.{timestamp}.{raw body}` with the partner's webhook secret, base64-encoded. Compare in constant time.",
            "schema": {
              "type": "string",
              "pattern": "^v1,[A-Za-z0-9+/]+=*$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerWebhookNotification"
              },
              "examples": {
                "example": {
                  "summary": "A subscription ended notification",
                  "value": {
                    "version": 1,
                    "event": "subscription.ended",
                    "test": false,
                    "productId": "prod_1a2b3c",
                    "customerEmail": "buyer@example.com",
                    "paymentId": "ord_9z8y7x",
                    "amount": 1990,
                    "currency": "EUR",
                    "taxAmount": 418,
                    "occurredAt": "2026-03-14T09:26:53.000Z",
                    "accessValidUntil": null,
                    "clientReference": "acc_10294",
                    "credits": null,
                    "creditsUnit": null,
                    "refundedAmount": null,
                    "eventId": "cm8f3k2q90001ab7d9x2y4z6g"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Delivered. Any 2xx status counts as success; the response body is not read."
          },
          "default": {
            "description": "Failure: any non-2xx status, a network error or no response within 5 seconds. The delivery is retried, see x-norelio-retry-policy."
          }
        }
      }
    },
    "subscription.payment_failed": {
      "post": {
        "operationId": "webhook_subscription_payment_failed",
        "tags": [
          "Webhooks"
        ],
        "summary": "Charge failed",
        "description": "A recurring charge failed and the provider will retry. Access is not extended until a charge succeeds: it is paid only until accessValidUntil, and by the time of this event that date has usually passed; the access check closes access 2 hours after it. If access was already closed by the seller, a full refund or a lost dispute, accessValidUntil is the moment of closing and there is no grace after it. Use this event to warn the buyer: a successful retry sends subscription.renewed, exhausted retries send subscription.ended.\n\nDelivery is at-least-once, not exactly-once: a cut between our request and the record of its outcome cannot be ruled out, so the same event occasionally arrives twice. Deduplicate on eventId — the same on every delivery of one event, different for every other event — and treat a repeat as an event you have already processed, not as a new one. Do not use paymentId and event for this: two partial refunds of one order share both. Do not use the x-norelio-webhook-id header either: it names one delivery attempt and is part of its signature.",
        "parameters": [
          {
            "name": "x-norelio-webhook-id",
            "in": "header",
            "required": true,
            "description": "Id of this delivery attempt. A retry carries a new id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "x-norelio-webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Send time in seconds since the Unix epoch. Reject a request older than 300 seconds.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "x-norelio-webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64>`: HMAC-SHA256 of `{id}.{timestamp}.{raw body}` with the partner's webhook secret, base64-encoded. Compare in constant time.",
            "schema": {
              "type": "string",
              "pattern": "^v1,[A-Za-z0-9+/]+=*$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerWebhookNotification"
              },
              "examples": {
                "example": {
                  "summary": "A charge failed notification",
                  "value": {
                    "version": 1,
                    "event": "subscription.payment_failed",
                    "test": false,
                    "productId": "prod_1a2b3c",
                    "customerEmail": "buyer@example.com",
                    "paymentId": "ord_9z8y7x",
                    "amount": 1990,
                    "currency": "EUR",
                    "taxAmount": 418,
                    "occurredAt": "2026-03-14T09:26:53.000Z",
                    "accessValidUntil": null,
                    "clientReference": "acc_10294",
                    "credits": null,
                    "creditsUnit": null,
                    "refundedAmount": null,
                    "eventId": "cm8f3k2q90001ab7d9x2y4z6h"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Delivered. Any 2xx status counts as success; the response body is not read."
          },
          "default": {
            "description": "Failure: any non-2xx status, a network error or no response within 5 seconds. The delivery is retried, see x-norelio-retry-policy."
          }
        }
      }
    },
    "subscription.canceled": {
      "post": {
        "operationId": "webhook_subscription_canceled",
        "tags": [
          "Webhooks"
        ],
        "summary": "Cancelled by the buyer",
        "description": "The buyer stopped the recurring charges. There will be no new charges, but access stays valid until the end of the paid period (see accessValidUntil; the access check closes it 2 hours after that date). Do not close access on this event: subscription.ended arrives separately when the period runs out. If access was already closed by the seller, a full refund or a lost dispute, accessValidUntil is the moment of closing and this event does not reopen it.\n\nDelivery is at-least-once, not exactly-once: a cut between our request and the record of its outcome cannot be ruled out, so the same event occasionally arrives twice. Deduplicate on eventId — the same on every delivery of one event, different for every other event — and treat a repeat as an event you have already processed, not as a new one. Do not use paymentId and event for this: two partial refunds of one order share both. Do not use the x-norelio-webhook-id header either: it names one delivery attempt and is part of its signature.",
        "parameters": [
          {
            "name": "x-norelio-webhook-id",
            "in": "header",
            "required": true,
            "description": "Id of this delivery attempt. A retry carries a new id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "x-norelio-webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Send time in seconds since the Unix epoch. Reject a request older than 300 seconds.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "x-norelio-webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64>`: HMAC-SHA256 of `{id}.{timestamp}.{raw body}` with the partner's webhook secret, base64-encoded. Compare in constant time.",
            "schema": {
              "type": "string",
              "pattern": "^v1,[A-Za-z0-9+/]+=*$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerWebhookNotification"
              },
              "examples": {
                "example": {
                  "summary": "A cancelled by the buyer notification",
                  "value": {
                    "version": 1,
                    "event": "subscription.canceled",
                    "test": false,
                    "productId": "prod_1a2b3c",
                    "customerEmail": "buyer@example.com",
                    "paymentId": "ord_9z8y7x",
                    "amount": 1990,
                    "currency": "EUR",
                    "taxAmount": 418,
                    "occurredAt": "2026-03-14T09:26:53.000Z",
                    "accessValidUntil": null,
                    "clientReference": "acc_10294",
                    "credits": null,
                    "creditsUnit": null,
                    "refundedAmount": null,
                    "eventId": "cm8f3k2q90001ab7d9x2y4z6i"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Delivered. Any 2xx status counts as success; the response body is not read."
          },
          "default": {
            "description": "Failure: any non-2xx status, a network error or no response within 5 seconds. The delivery is retried, see x-norelio-retry-policy."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "The access-check key issued in the cabinet (API and webhooks, API key tab), sent from your server."
      }
    },
    "schemas": {
      "PartnerWebhookNotification": {
        "title": "Norelio partner webhook notification",
        "type": "object",
        "required": [
          "version",
          "event",
          "test",
          "productId",
          "customerEmail",
          "paymentId",
          "amount",
          "currency",
          "taxAmount",
          "occurredAt",
          "accessValidUntil",
          "clientReference",
          "credits",
          "creditsUnit",
          "refundedAmount",
          "eventId"
        ],
        "additionalProperties": true,
        "properties": {
          "version": {
            "description": "Payload version. Changes only on an incompatible change; see the compatibility commitment.",
            "type": "integer",
            "const": 1
          },
          "event": {
            "description": "Which event happened: one of the values in the event catalog.",
            "type": "string",
            "enum": [
              "order.paid",
              "order.refunded_partial",
              "order.refunded_full",
              "order.dispute_opened",
              "order.dispute_resolved",
              "subscription.renewed",
              "subscription.ended",
              "subscription.payment_failed",
              "subscription.canceled"
            ]
          },
          "test": {
            "description": "true for a test notification sent from the cabinet. Do not grant access on it; the event is real (order.paid), so parse it with your normal code.",
            "type": "boolean"
          },
          "productId": {
            "description": "Your product id, the same as in the cabinet and in the payment page address.",
            "type": "string",
            "minLength": 1
          },
          "customerEmail": {
            "description": "The buyer's email. null when the payment arrived without one.",
            "type": [
              "string",
              "null"
            ]
          },
          "paymentId": {
            "description": "Our order id. Every event of the order shares it — the payment, its refunds, two partial refunds — so recognise a repeated delivery by eventId.",
            "type": "string",
            "minLength": 1
          },
          "amount": {
            "description": "The product price in the smallest currency unit (cents), without VAT: 1000 means 10.00.",
            "type": "integer",
            "minimum": 0
          },
          "currency": {
            "description": "Three-letter currency code, as the buyer saw it at checkout.",
            "type": "string",
            "pattern": "^[A-Z]{3}$"
          },
          "taxAmount": {
            "description": "VAT charged to an EU buyer on top of the price, in the same unit and currency as amount; the buyer paid amount + taxAmount. 0 when no VAT applied: a buyer outside the EU, a business with a VAT id (reverse charge), a PayPal payment, or an order from before this field existed. Added without a version change.",
            "type": "integer",
            "minimum": 0
          },
          "occurredAt": {
            "description": "When the event happened, ISO 8601, UTC.",
            "type": "string",
            "format": "date-time"
          },
          "accessValidUntil": {
            "description": "Date and time until which access is paid, ISO 8601, UTC: the end of the paid period, without grace (the access check closes access 2 hours after it). For access closed by the seller, a full refund or a lost dispute, this is the moment of closing, with no grace after it; the notification does not distinguish the two, the access check does (hasAccess: false while the 2 hours have not passed yet). Present on subscription events (renewed, ended, payment_failed, canceled); null on order events.",
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "clientReference": {
            "description": "Your reference for the buyer's account, passed at checkout: the widget's data-client-reference attribute or the client_reference query parameter of the payment link. Present on every order event (payment, refunds, disputes); on subscription events it is the value of the order the subscription started with. null when nothing was passed or the value failed the format check (up to 128 characters: Latin letters, digits and ._:-, not starting with - or .). Set by the buyer's browser: treat it as a crediting address, not as proof of identity.",
            "type": [
              "string",
              "null"
            ],
            "maxLength": 128,
            "pattern": "^[A-Za-z0-9_:][A-Za-z0-9._:-]{0,127}$"
          },
          "credits": {
            "description": "The number of credits bought on a top-up order: an integer that Norelio took from the package or computed from the product's rate (a number sent by anyone is never accepted). null on every other order and on subscription events. Credit the account on order.paid; on every refund event (order.refunded_partial, order.refunded_full) remove the share this refund returned: floor(credits × refundedAmount ÷ amount). Added without a version change.",
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "creditsUnit": {
            "description": "The name of the unit as the partner set it on the product (\"credits\", \"tokens\"); null wherever credits is null.",
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 24
          },
          "refundedAmount": {
            "description": "The amount this refund returned to the buyer, without VAT, in the same unit and currency as amount. Present on order.refunded_partial and order.refunded_full; null on every other event. Every refund is its own event with its own amount: two partial refunds of one order are two notifications, and the refund after which everything has been returned arrives as order.refunded_full with the remainder. Added without a version change.",
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "eventId": {
            "description": "Our id of this event. The same on every delivery of the event — automatic retries and a resend from the cabinet's delivery log alike — and different for every other event: two partial refunds of one order have different ids. Deduplicate on it. Treat it as an opaque string; do not parse it. A test notification has its own id, starting with test_. The x-norelio-webhook-id header is not for this: it names one delivery attempt and is part of its signature. Added without a version change.",
            "type": "string",
            "minLength": 1
          }
        }
      },
      "AccessCheckResponse": {
        "title": "Norelio partner access-check response",
        "type": "object",
        "required": [
          "hasAccess",
          "validUntil",
          "clientReference"
        ],
        "additionalProperties": false,
        "properties": {
          "hasAccess": {
            "type": "boolean",
            "description": "Whether access is open for this email/product pair right now: the paid period is still running, or less than 120 minutes have passed since its end (validUntil) — the grace for a renewal, which arrives within about an hour after the period ends. Access closed by the seller, a full refund or a lost dispute is closed at once, without grace."
          },
          "validUntil": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "End of the paid period, without grace; for access closed by the seller, a full refund or a lost dispute, the moment of closing; null for a non-subscription or non-existent access."
          },
          "clientReference": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 128,
            "pattern": "^[A-Za-z0-9_:][A-Za-z0-9._:-]{0,127}$",
            "description": "The account reference of the access found: the reference of this email's latest order for this product (for a subscription renewal, of the order the subscription started with); null when there is none or there is no access."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A short English explanation."
          }
        }
      },
      "ApiError": {
        "type": "object",
        "description": "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.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "rate_limited",
                  "invalid_api_key",
                  "invalid_request",
                  "direct_checkout_not_enabled",
                  "product_not_found",
                  "checkout_failed",
                  "not_found",
                  "method_not_allowed"
                ],
                "description": "`rate_limited` (429): too many requests in a row, from your address or with your key; `invalid_api_key` (401): the key is missing or does not match; `invalid_request` (400): the body or the query is malformed; message says what exactly; `direct_checkout_not_enabled` (403): 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); `product_not_found` (404): no product with this id belongs to your account, or it cannot be bought right now; `checkout_failed` (502): the payment provider could not start the payment; retry later; `not_found` (404): no such endpoint; `method_not_allowed` (405): the endpoint does not accept that method"
              },
              "message": {
                "type": "string",
                "description": "A short English explanation."
              }
            }
          }
        }
      },
      "CheckoutRequest": {
        "type": "object",
        "required": [
          "productId",
          "clientReference"
        ],
        "additionalProperties": true,
        "properties": {
          "productId": {
            "type": "string",
            "minLength": 1,
            "description": "Required. The product id from the cabinet catalog."
          },
          "clientReference": {
            "type": "string",
            "maxLength": 128,
            "pattern": "^[A-Za-z0-9_:][A-Za-z0-9._:-]{0,127}$",
            "description": "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": {
            "type": [
              "string",
              "null"
            ],
            "format": "email",
            "description": "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": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "maxLength": 2000,
            "description": "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": {
            "type": [
              "object",
              "null"
            ],
            "description": "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.",
            "properties": {
              "ip": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The buyer's IPv4 or IPv6 address."
              },
              "userAgent": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The buyer's browser (User-Agent)."
              }
            }
          },
          "packageId": {
            "type": "integer",
            "minimum": 1,
            "maximum": 6,
            "description": "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": {
            "type": "integer",
            "minimum": 50,
            "maximum": 100000,
            "description": "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."
          }
        }
      },
      "CheckoutResponse": {
        "type": "object",
        "required": [
          "id",
          "url",
          "expiresAt",
          "clientReference",
          "warnings",
          "credits",
          "creditsUnit"
        ],
        "additionalProperties": true,
        "properties": {
          "id": {
            "type": "string",
            "description": "The order id — the same paymentId that arrives in the webhook."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "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": {
            "type": "string",
            "format": "date-time",
            "description": "Until when the address still accepts the buyer (ISO 8601): the payment provider's session lifetime. After it, create a new payment."
          },
          "clientReference": {
            "type": "string",
            "maxLength": 128,
            "pattern": "^[A-Za-z0-9_:][A-Za-z0-9._:-]{0,127}$",
            "description": "The account reference stored on the order — the one you passed."
          },
          "warnings": {
            "type": "array",
            "description": "Warnings, empty when everything was accepted. Each is an object with code and message.",
            "items": {
              "type": "object",
              "required": [
                "code",
                "message"
              ],
              "properties": {
                "code": {
                  "type": "string",
                  "enum": [
                    "return_url_rejected"
                  ],
                  "description": "`return_url_rejected`: the returnUrl you passed failed the check; the order was created without a return address"
                },
                "message": {
                  "type": "string",
                  "description": "A short English explanation."
                }
              }
            }
          },
          "credits": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "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": {
            "type": [
              "string",
              "null"
            ],
            "description": "Top-ups: the unit name from the product card; null for any other product."
          }
        }
      },
      "CustomerState": {
        "type": "object",
        "required": [
          "subscriptions",
          "purchases"
        ],
        "additionalProperties": true,
        "description": "Each list holds up to 50 rows, newest first. Purchases include only orders on which money actually moved: paid, refunded and disputed ones; unpaid and cancelled attempts are not shown.",
        "properties": {
          "subscriptions": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "type": "object",
              "required": [
                "id",
                "productId",
                "status",
                "validUntil",
                "currentPeriodEnd",
                "cancelAtPeriodEnd",
                "clientReference"
              ],
              "additionalProperties": true,
              "properties": {
                "id": {
                  "type": "string",
                  "description": "The subscription id at Norelio."
                },
                "productId": {
                  "type": "string",
                  "description": "The product id."
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "active",
                    "past_due",
                    "canceled",
                    "ended"
                  ],
                  "description": "The subscription state as in the buyer's account: active — charges are running; past_due — the last charge failed and the provider is retrying; canceled — charges stopped, the paid period runs out; ended — the subscription is over."
                },
                "validUntil": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time",
                  "description": "Until when access is paid (ISO 8601): the same date the access check returns, without grace; null when there is no access. It is the access date, not the next charge: a refund or a closed access shortens it."
                },
                "currentPeriodEnd": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time",
                  "description": "End of the current paid period as the provider reports it, i.e. the next charge; null when unknown."
                },
                "cancelAtPeriodEnd": {
                  "type": "boolean",
                  "description": "true when charges have been stopped and the subscription runs out its period."
                },
                "clientReference": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "maxLength": 128,
                  "pattern": "^[A-Za-z0-9_:][A-Za-z0-9._:-]{0,127}$",
                  "description": "The account reference of the order the subscription started with, or null."
                }
              }
            }
          },
          "purchases": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "type": "object",
              "required": [
                "orderId",
                "productId",
                "paidAt",
                "amount",
                "currency",
                "status",
                "clientReference",
                "refunded",
                "credits",
                "creditsUnit"
              ],
              "additionalProperties": true,
              "properties": {
                "orderId": {
                  "type": "string",
                  "description": "The order id — the same paymentId as in the webhooks."
                },
                "productId": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The product id; null for a payment without a product."
                },
                "paidAt": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time",
                  "description": "When the payment was confirmed (ISO 8601); null while it is not."
                },
                "amount": {
                  "type": "integer",
                  "minimum": 0,
                  "description": "The price shown to the buyer, in the smallest currency unit (cents), without VAT — like amount in the webhook."
                },
                "currency": {
                  "type": "string",
                  "pattern": "^[A-Z]{3}$",
                  "description": "The currency of that price, three-letter code."
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "paid",
                    "refunded",
                    "disputed"
                  ],
                  "description": "paid, refunded (in full) or disputed."
                },
                "clientReference": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "maxLength": 128,
                  "pattern": "^[A-Za-z0-9_:][A-Za-z0-9._:-]{0,127}$",
                  "description": "The account reference by the client reference rule: for a subscription renewal, the one of the order the subscription started with; null when none was passed."
                },
                "refunded": {
                  "type": "boolean",
                  "description": "true when money went back to the buyer, in full or in part."
                },
                "credits": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "minimum": 0,
                  "description": "For a top-up purchase, the number of credits bought (from the order's snapshot); null for any other purchase."
                },
                "creditsUnit": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "For a top-up purchase, the unit name; null for any other purchase."
                }
              }
            }
          }
        }
      }
    }
  },
  "x-norelio-retry-policy": {
    "successStatusRange": "2xx",
    "maxAttempts": 6,
    "retryDelaysMinutes": [
      2,
      10,
      60,
      240,
      720
    ],
    "timeoutSeconds": 5,
    "orderingGuaranteed": false,
    "deliveryGuarantee": "at-least-once"
  }
}