openapi: 3.1.0
info:
  title: Thestral API
  version: 1.0.0
  description: |-
    Thestral is Nerivia's REST API for external integrations: one HTTPS host, JSON bodies,
    and one signing scheme for every product. Nerivia is non-custodial and never holds your
    funds.

    ## Authentication

    Thestral signs every request instead of using bearer tokens, so a captured request
    can't be replayed or altered. Each API key has two values:

    - `api_key_uuid`: the key's public ID, sent with every request.
    - `api_key_signing_secret`: the secret you sign with. It's shown once, when you create
      the key, and never sent to Nerivia again.

    Send these three headers with every request:

    | Header | Value |
    | --- | --- |
    | `X-Nerivia-Key-Id` | Your `api_key_uuid`. |
    | `X-Nerivia-Timestamp` | Current Unix time, in seconds. |
    | `X-Nerivia-Signature` | Lowercase hex HMAC-SHA256 of the signed message, keyed with your `api_key_signing_secret`. |

    The signed message is the timestamp, a period, and the raw request body, in UTF-8:

    ```text
    <timestamp>.<raw_body>
    ```

    - Sign the exact bytes you send. Any difference, even whitespace or key order, fails
      the check.
    - A request with no body signs `<timestamp>.` alone.
    - The timestamp must be within five minutes of server time, so keep your clock in
      sync.

    A bad signature, a stale timestamp, a revoked or expired key, or a suspended workspace
    returns `401` with a specific error code. Revoke a key or set its expiry in the
    [Nerivia dashboard](https://app.nerivia.io/).

    ## Workspaces

    A workspace owns your settings, API keys, and orders, and nothing crosses between
    workspaces. Every account has two:

    - **Live**: real payments.
    - **Test**: the same flows on test networks, to rehearse your integration.

    Each API key belongs to one workspace, and Nerivia resolves the workspace from that
    key: no URL or body names it. To switch between test and live, sign with the other
    workspace's key.

    ## Errors

    Every error returns the same shape:

    ```json
    {
      "errors": [
        {
          "code": "neripay_order_amount_out_of_range",
          "field": null,
          "message": "The order amount is outside the accepted range."
        }
      ]
    }
    ```

    - `code`: a stable, machine-readable ID. Branch on it, never on the message.
    - `field`: the request field that caused the error, or `null`.
    - `message`: a readable explanation in the `Accept-Language` language, English or
      Spanish.

    The `errors` list holds one item per problem. The status code gives the class of
    failure: `400` for an invalid request, `401` for an authentication failure, `403` for a
    permission failure, and `404` for an object the request references that doesn't exist
    in your workspace.

    ## Webhooks

    Instead of polling, register an endpoint URL in the dashboard and choose the events it
    receives. Each subscription gets its own signing secret.

    Nerivia POSTs a JSON body to your endpoint, signed the same way as a request:
    `X-Nerivia-Timestamp` and `X-Nerivia-Signature`, computed over the exact body
    delivered, with that subscription's secret. Verify the signature before trusting a
    delivery, and reply with any `2xx` status to acknowledge it. A non-`2xx` reply or a
    timeout is retried a limited number of times.

    ## Support

    Need help? Message us on WhatsApp at +593 963 187 317.
servers:
  - url: https://api.nerivia.io/v1/thestral/neripay
security:
  - keyId: []
    timestamp: []
    signature: []
tags:
  - name: Asset Ledger Configs
    x-displayName: Asset ledger configs
    description: |-
      An asset ledger config is one asset on one ledger, or blockchain network, that your
      workspace accepts, such as USDT on Polygon, with its receiving address, commission
      rate, and amount limits. You set configs up in the dashboard, and the API reads
      them.
  - name: Orders
    x-displayName: Orders
    description: |-
      An order asks a customer to pay an amount in one asset on one ledger, to an address
      dedicated to that order. It moves through these statuses on its own:

      | Status | Meaning |
      | --- | --- |
      | `awaiting_payment` | Created, and waiting for the customer's payment until `expires_at`. |
      | `paid` | Payment confirmed on-chain. Fulfill the order at this point. |
      | `settling` | Nerivia is sending the funds to your receiving address. |
      | `completed` | The funds reached your receiving address. |
      | `expired` | No payment arrived before `expires_at`. |
      | `settlement_failed` | Sending the funds to your receiving address failed. |

      Subscribe to the `neripay.order.status.change` webhook event to follow these
      changes, or poll List orders with the order's `uuid` as a filter.
components:
  securitySchemes:
    keyId:
      type: apiKey
      in: header
      name: X-Nerivia-Key-Id
      description: |-
        Your `api_key_uuid`.
    timestamp:
      type: apiKey
      in: header
      name: X-Nerivia-Timestamp
      description: |-
        Current Unix time, in seconds.
    signature:
      type: apiKey
      in: header
      name: X-Nerivia-Signature
      description: |-
        Lowercase hex HMAC-SHA256 of `<timestamp>.<raw_body>`, keyed with your
        `api_key_signing_secret`.
  parameters:
    limit: &limitParam
      name: limit
      in: query
      required: false
      description: |-
        Items per page. Defaults to `10`, which also applies when the value is invalid.
      schema:
        type: integer
        default: 10
    offset: &offsetParam
      name: offset
      in: query
      required: false
      description: |-
        Items to skip before the page starts. Defaults to `0`, which also applies when
        the value is invalid.
      schema:
        type: integer
        default: 0
    asset: &assetParam
      name: asset
      in: query
      required: false
      description: |-
        Only configs for this asset `uuid`, on every ledger it's set up on.
      schema:
        type: string
        format: uuid
    ledger: &ledgerParam
      name: ledger
      in: query
      required: false
      description: |-
        Only configs on this ledger `uuid`, for every asset set up on it.
      schema:
        type: string
        format: uuid
    status: &statusParam
      name: status
      in: query
      required: false
      description: |-
        Only orders in this status.
      schema:
        type: string
        enum:
          - awaiting_payment
          - paid
          - settling
          - completed
          - expired
          - settlement_failed
    uuid: &uuidFilterParam
      name: uuid
      in: query
      required: false
      description: |-
        Only the order with this ID.
      schema:
        type: string
        format: uuid
    created_at__gte: &createdAtGteParam
      name: created_at__gte
      in: query
      required: false
      description: |-
        Only orders created at or after this moment. A value with no offset is read as
        UTC.
      schema:
        type: string
        format: date-time
    created_at__lte: &createdAtLteParam
      name: created_at__lte
      in: query
      required: false
      description: |-
        Only orders created at or before this moment. A value with no offset is read as
        UTC.
      schema:
        type: string
        format: date-time
  responses:
    BadRequest: &BadRequest
      description: |-
        The request is invalid. `errors` lists every problem.
      content:
        application/json:
          schema: &errorEnvelopeSchema
            type: object
            properties:
              errors:
                type: array
                description: |-
                  One item per problem.
                items:
                  type: object
                  properties:
                    code:
                      type: string
                      description: |-
                        A stable, machine-readable error ID. Branch on it, never on
                        `message`.
                    field:
                      type: [string, 'null']
                      description: |-
                        The request field that caused the error, or `null`.
                    message:
                      type: string
                      description: |-
                        A readable explanation in the `Accept-Language` language.
          example:
            errors:
              - code: neripay_order_amount_out_of_range
                field: null
                message: The order amount is outside the accepted range.
    Unauthorized: &Unauthorized
      description: |-
        Authentication failed: a bad signature, a stale timestamp, a revoked or expired
        key, or a suspended workspace.
      content:
        application/json:
          schema: *errorEnvelopeSchema
    Forbidden: &Forbidden
      description: |-
        The key isn't allowed to perform this action.
      content:
        application/json:
          schema: *errorEnvelopeSchema
    NotFound: &NotFound
      description: |-
        An object the request references doesn't exist in your workspace.
      content:
        application/json:
          schema: *errorEnvelopeSchema
paths:
  /workspace-config/asset-ledger-configs/:
    get:
      summary: List asset ledger configs
      description: |-
        Returns your workspace's **active** asset ledger configs, newest first: every
        asset and ledger pair you can create orders in right now. Inactive configs never
        appear, and no filter can include them.

        Filter by asset or ledger with the UUIDs inside an item's `asset_ledger` object,
        never with the config's own `uuid`. When you combine filters, an item must match
        all of them, and a value the API can't parse, such as a malformed UUID, returns an
        empty list, never the unfiltered one.

        A workspace with no active config gets a `200` with an empty `results` list: set
        one up in the dashboard before creating orders.
      tags:
        - Asset Ledger Configs
      parameters:
        - *assetParam
        - *ledgerParam
        - *limitParam
        - *offsetParam
      responses:
        '200':
          description: |-
            Your active asset ledger configs.
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
                    description: |-
                      Items matching your filters, across every page.
                  next:
                    type: [string, 'null']
                    format: uri
                    description: |-
                      URL of the next page, or `null` on the last page.
                  previous:
                    type: [string, 'null']
                    format: uri
                    description: |-
                      URL of the previous page, or `null` on the first page.
                  results:
                    type: array
                    description: |-
                      This page's items, newest first.
                    items:
                      type: object
                      properties:
                        uuid:
                          type: string
                          description: |-
                            The config's ID. Not the value you send when creating an
                            order.
                        is_active:
                          type: boolean
                          description: |-
                            Whether the pair accepts orders. Always `true` here.
                        commission_rate:
                          type: string
                          format: decimal
                          description: |-
                            Nerivia's commission as a percentage: `"2.50"` means 2.50%.
                            Each new order keeps the rate in force when it's created.
                        min_amount:
                          type: string
                          format: decimal
                          description: |-
                            The smallest order amount accepted, in the asset's units.
                        max_amount:
                          type: string
                          format: decimal
                          description: |-
                            The largest order amount accepted, in the asset's units.
                        receiving_address:
                          type: string
                          description: |-
                            Where your share of each settled order is sent. Check it
                            after any dashboard change.
                        asset_ledger:
                          type: object
                          description: |-
                            The asset and ledger this config is for.
                          properties:
                            uuid:
                              type: string
                              description: |-
                                ID of the asset and ledger pair. Send it as
                                `asset_ledger` when you create an order.
                            contract_address:
                              type: [string, 'null']
                              description: |-
                                The token's contract address on this ledger, or `null`
                                for the ledger's native asset. It is not where a customer
                                pays.
                            asset:
                              type: object
                              description: |-
                                The asset, such as Tether USD.
                              properties:
                                uuid:
                                  type: string
                                  description: |-
                                    The asset's ID.
                                name:
                                  type: string
                                  description: |-
                                    The asset's name, such as Tether USD.
                                code:
                                  type: string
                                  description: |-
                                    The asset's code, such as `USDT`.
                                decimals:
                                  type: integer
                                  description: |-
                                    Decimal places the asset supports, which caps an
                                    order amount's precision.
                            ledger:
                              type: object
                              description: |-
                                The ledger, or blockchain network, such as Polygon.
                              properties:
                                uuid:
                                  type: string
                                  description: |-
                                    The ledger's ID.
                                name:
                                  type: string
                                  description: |-
                                    The ledger's name, such as Polygon Amoy.
                                code:
                                  type: string
                                  description: |-
                                    The ledger's code, such as `polygon-amoy`.
                                address_regex:
                                  type: [string, 'null']
                                  description: |-
                                    The pattern an address on this ledger must match, or
                                    `null` when unknown.
              example:
                count: 1
                next: null
                previous: null
                results:
                  - uuid: 59a13b02-a4b5-4d42-9d20-19d42be990e9
                    is_active: true
                    commission_rate: '2.50'
                    min_amount: '1.000000000000000000'
                    max_amount: '10000.000000000000000000'
                    receiving_address: '0xf59fedB5D38CD00995AC1D8256f4CfeF46FD58a3'
                    asset_ledger:
                      uuid: 37caa3f8-79f3-48a3-bde2-840636f50df2
                      contract_address: '0x0502986e4Be5d3e0A98D68A0fD0f7683F9968843'
                      asset:
                        uuid: 46588bb4-600b-4e41-a5c0-95071166f41d
                        name: Tether USD
                        code: USDT
                        decimals: 2
                      ledger:
                        uuid: 85a198fd-4ee6-4f17-bbeb-040ff46896dc
                        name: Polygon Amoy
                        code: polygon-amoy
                        address_regex: ^0x[0-9a-fA-F]{40}$
        '401': *Unauthorized
        '403': *Forbidden
  /orders/:
    get:
      summary: List orders
      description: |-
        Returns your workspace's orders, newest first.

        Filter on `created_at` with the two date parameters, whose bounds are inclusive;
        the field itself isn't returned. When you combine filters, an order must match all
        of them, and a value the API can't parse, such as a malformed UUID, returns an
        empty list, never the unfiltered one.

        List items carry no `checkout_url`: read it from the creation response.
      tags:
        - Orders
      parameters:
        - *statusParam
        - *uuidFilterParam
        - *createdAtGteParam
        - *createdAtLteParam
        - *limitParam
        - *offsetParam
      responses:
        '200':
          description: |-
            Your orders.
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
                    description: |-
                      Items matching your filters, across every page.
                  next:
                    type: [string, 'null']
                    format: uri
                    description: |-
                      URL of the next page, or `null` on the last page.
                  previous:
                    type: [string, 'null']
                    format: uri
                    description: |-
                      URL of the previous page, or `null` on the first page.
                  results:
                    type: array
                    description: |-
                      This page's items, newest first.
                    items:
                      type: object
                      properties:
                        uuid:
                          type: string
                          description: |-
                            The order's ID. Store it to find the order later with the
                            `uuid` filter.
                        amount:
                          type: string
                          format: decimal
                          description: |-
                            What the customer pays, in the asset's units. Nerivia's
                            commission comes out of it, not on top of it.
                        commission_rate:
                          type: string
                          format: decimal
                          description: |-
                            Nerivia's commission as a percentage, fixed when the order
                            was created.
                        status:
                          type: string
                          description: |-
                            The order's current status, such as `awaiting_payment` or
                            `paid`.
                        expires_at:
                          type: string
                          format: date-time
                          description: |-
                            When the order stops accepting payment. Drive your countdown
                            from it rather than assuming a fixed window.
                        contract_address:
                          type: string
                          description: |-
                            The order's own settlement address, where the customer pays.
                            Use it for this order only.
                        asset_ledger:
                          type: object
                          description: |-
                            The asset and ledger the order is priced and paid in.
                          properties:
                            uuid:
                              type: string
                              description: |-
                                ID of the asset and ledger pair.
                            asset:
                              type: object
                              description: |-
                                The asset, such as Tether USD.
                              properties:
                                uuid:
                                  type: string
                                  description: |-
                                    The asset's ID.
                                name:
                                  type: string
                                  description: |-
                                    The asset's name, such as Tether USD.
                                code:
                                  type: string
                                  description: |-
                                    The asset's code, such as `USDT`.
                                decimals:
                                  type: integer
                                  description: |-
                                    Decimal places the asset supports, which caps an
                                    order amount's precision.
                            ledger:
                              type: object
                              description: |-
                                The ledger, or blockchain network, such as Polygon.
                              properties:
                                uuid:
                                  type: string
                                  description: |-
                                    The ledger's ID.
                                name:
                                  type: string
                                  description: |-
                                    The ledger's name, such as Polygon Amoy.
                                code:
                                  type: string
                                  description: |-
                                    The ledger's code, such as `polygon-amoy`.
                                address_regex:
                                  type: [string, 'null']
                                  description: |-
                                    The pattern an address on this ledger must match, or
                                    `null` when unknown.
                            contract_address:
                              type: [string, 'null']
                              description: |-
                                The token's contract address on this ledger, or `null`
                                for the ledger's native asset. It is not where a customer
                                pays.
                        external_reference:
                          type: [string, 'null']
                          description: |-
                            Your own ID for the order, or `null` for an order created in
                            the dashboard.
              example:
                count: 1
                next: null
                previous: null
                results:
                  - uuid: e75f7105-d903-4d69-b1ea-17bbfbd7f777
                    amount: '10.000000000000000000'
                    commission_rate: '2.50'
                    status: settlement_failed
                    expires_at: '2026-09-11T05:44:18.988829Z'
                    contract_address: '0x0146a0AA378044be8a59e2aee97B00Cac073539C'
                    asset_ledger:
                      uuid: 37caa3f8-79f3-48a3-bde2-840636f50df2
                      asset:
                        uuid: 46588bb4-600b-4e41-a5c0-95071166f41d
                        name: Tether USD
                        code: USDT
                        decimals: 2
                      ledger:
                        uuid: 85a198fd-4ee6-4f17-bbeb-040ff46896dc
                        name: Polygon Amoy
                        code: polygon-amoy
                        address_regex: ^0x[0-9a-fA-F]{40}$
                      contract_address: '0x0502986e4Be5d3e0A98D68A0fD0f7683F9968843'
                    external_reference: null
        '401': *Unauthorized
        '403': *Forbidden
    post:
      summary: Create order
      description: |-
        Creates a payment order for an amount in one asset on one ledger. Nerivia gives
        the order its own settlement address, starts watching it, and returns the order
        with a `checkout_url`: a hosted payment page you can send your customer to
        instead of building your own.

        A new order starts in `awaiting_payment`, and its payment window opens as soon as
        this call succeeds.

        #### After creating

        Show your customer `contract_address` and `amount`, or send them to
        `checkout_url`. Nerivia confirms the payment, marks the order `paid`, sends the
        funds to your receiving address, and marks it `completed`, with no further call
        from you.

        Fulfill on `paid`, not `completed`: `paid` means your customer has paid, while
        `completed` only means the funds have reached you.
      tags:
        - Orders
      requestBody:
        description: |-
          The order to create. All three fields are required, and any other field is
          ignored.
        content:
          application/json:
            schema:
              type: object
              required:
                - asset_ledger
                - amount
                - external_reference
              properties:
                asset_ledger:
                  type: string
                  description: |-
                    The `asset_ledger.uuid` of an active asset ledger config in your
                    key's workspace.
                amount:
                  type: string
                  format: decimal
                  description: |-
                    What the customer pays, as a decimal string in the asset's units,
                    such as `"25.50"`. It must fall within the config's `min_amount` and
                    `max_amount`, carry no more decimals than the asset supports, and
                    hold at most 36 digits and 18 decimals.
                external_reference:
                  type: string
                  description: |-
                    Your own ID for the order, such as an invoice number. Up to 36
                    characters, and unique in your workspace, so a retried creation
                    never duplicates a charge.
            example:
              asset_ledger: 37caa3f8-79f3-48a3-bde2-840636f50df2
              amount: '100.00'
              external_reference: AAA-000-000-003
      responses:
        '201':
          description: |-
            The created order.
          content:
            application/json:
              schema:
                type: object
                properties:
                  uuid:
                    type: string
                    description: |-
                      The order's ID. Store it to find the order later with the `uuid`
                      filter.
                  amount:
                    type: string
                    format: decimal
                    description: |-
                      What the customer pays, in the asset's units. Nerivia's commission
                      comes out of it, not on top of it.
                  commission_rate:
                    type: string
                    format: decimal
                    description: |-
                      Nerivia's commission as a percentage, fixed when the order was
                      created.
                  status:
                    type: string
                    description: |-
                      The order's current status, such as `awaiting_payment` or `paid`.
                  expires_at:
                    type: string
                    format: date-time
                    description: |-
                      When the order stops accepting payment. Drive your countdown from
                      it rather than assuming a fixed window.
                  contract_address:
                    type: string
                    description: |-
                      The order's own settlement address, where the customer pays. Use
                      it for this order only.
                  asset_ledger:
                    type: object
                    description: |-
                      The asset and ledger the order is priced and paid in.
                    properties:
                      uuid:
                        type: string
                        description: |-
                          ID of the asset and ledger pair.
                      asset:
                        type: object
                        description: |-
                          The asset, such as Tether USD.
                        properties:
                          uuid:
                            type: string
                            description: |-
                              The asset's ID.
                          name:
                            type: string
                            description: |-
                              The asset's name, such as Tether USD.
                          code:
                            type: string
                            description: |-
                              The asset's code, such as `USDT`.
                          decimals:
                            type: integer
                            description: |-
                              Decimal places the asset supports, which caps an order
                              amount's precision.
                      ledger:
                        type: object
                        description: |-
                          The ledger, or blockchain network, such as Polygon.
                        properties:
                          uuid:
                            type: string
                            description: |-
                              The ledger's ID.
                          name:
                            type: string
                            description: |-
                              The ledger's name, such as Polygon Amoy.
                          code:
                            type: string
                            description: |-
                              The ledger's code, such as `polygon-amoy`.
                          address_regex:
                            type: [string, 'null']
                            description: |-
                              The pattern an address on this ledger must match, or
                              `null` when unknown.
                      contract_address:
                        type: [string, 'null']
                        description: |-
                          The token's contract address on this ledger, or `null` for the
                          ledger's native asset. It is not where a customer pays.
                  external_reference:
                    type: string
                    description: |-
                      Your own ID for the order, as you sent it.
                  checkout_url:
                    type: string
                    format: uri
                    description: |-
                      A Nerivia-hosted payment page for this order alone, safe to share
                      with your customer.
              example:
                uuid: 9ba6ade8-416a-495b-8cf6-6eeac215ab08
                amount: '100.000000000000000000'
                commission_rate: '2.50'
                status: awaiting_payment
                expires_at: '2026-09-11T21:45:03.204571Z'
                contract_address: '0x23BDBDbAD94Bb5CD2D8AD44645523D4A5Bd1aeD9'
                asset_ledger:
                  uuid: 37caa3f8-79f3-48a3-bde2-840636f50df2
                  asset:
                    uuid: 46588bb4-600b-4e41-a5c0-95071166f41d
                    name: Tether USD
                    code: USDT
                    decimals: 2
                  ledger:
                    uuid: 85a198fd-4ee6-4f17-bbeb-040ff46896dc
                    name: Polygon Amoy
                    code: polygon-amoy
                    address_regex: ^0x[0-9a-fA-F]{40}$
                  contract_address: '0x0502986e4Be5d3e0A98D68A0fD0f7683F9968843'
                external_reference: AAA-000-000-001
                checkout_url: https://app.nerivia.io/neripay/checkout/9ba6ade8-416a-495b-8cf6-6eeac215ab08
        '400': *BadRequest
        '401': *Unauthorized
        '403': *Forbidden
        '404': *NotFound
