> ## Documentation Index
> Fetch the complete documentation index at: https://www.bitnob.dev/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Bitnob API requests authenticate with an `X-Auth-Client` API key header against https://api.bitnob.com.
> Always use the sandbox environment for testing. Never put live API keys in sample code.
> Money amounts are integers in the smallest unit of the currency (satoshis for BTC, cents for fiat). Never use floats.
> Every state-changing request takes a client-supplied `reference` and must be treated as idempotent on retry.
> Quotes expire. Re-quote rather than reusing a stale quote id.

# Dry-run policy evaluation

> Runs the policy engine against a synthetic transaction request and returns the decision without any side effects. No approval requests are created, no audit log entries are written, and no transactions are inserted.

Use this endpoint to preview how the active policy would respond to a given request before committing to it — for example, when configuring automation rules or testing policy changes.

All request fields are optional. Omitting a field means 'not applicable' for that condition axis. When no active policy exists the verdict is 'no_match' with Allowed=true.

The response includes a `trace` field containing a per-rule evaluation trace when the evaluator supports it. Each entry records whether the rule matched and, if not, which condition failed first.



## OpenAPI

````yaml /api-collections/swagger/bitnob-platform.openapi.json post /policies/evaluate
openapi: 3.0.3
info:
  description: >-
    # Bitnob Enterprise API


    A high-performance Wallet-as-a-Service API for partners and developers:

    non-custodial wallets, transactions, gas stations, on-chain subscriptions
    and

    platform webhooks across multiple chains.


    ## Authentication


    Authenticate every request with your secret API key in the **X-API-Key**

    request header. Keys are scoped to one organization and carry a fixed set of

    permissions; an endpoint returns **403** when the key lacks the required

    permission. Treat the key as a credential — never expose it in a browser or

    mobile client.


    ## Request tracing


    Every response carries an **X-Request-Id** header. Log it, and quote it when

    contacting support — it identifies the exact request in our systems.


    ## Rate limits


    Wallet/business API and node RPC traffic have **independent rate-limit
    budgets**,

    even when they use the same API key and organization:


    | Traffic | Limit |

    |---|---|

    | Wallet and other non-RPC API requests | A per-key requests-per-minute
    allowance plus a configured burst, and a shared organization per-minute
    allowance |

    | **POST /rpc/{chain_network}**, including the **/v1/rpc/{chain_network}**
    alias | A shared organization requests-per-second limit, counted per
    JSON-RPC call |


    Authenticated RPC requests do not consume your wallet API quota, and wallet

    requests do not consume your RPC limit. The RPC limit is shared across all
    keys

    and chains in your organization; creating another key does not increase it.


    A JSON-RPC batch counts each call in the batch, not as a single request —
    batching

    many calls into one HTTP request consumes the limit per call. On live there
    is

    currently no additional per-day volume cap; sandbox applies a low per-second
    and

    per-day default so it cannot be used as production infrastructure. An RPC
    429

    carries **Retry-After**.


    Rate-limited responses expose the current state:


    - **X-RateLimit-Limit** — requests permitted in the current window.

    - **X-RateLimit-Remaining** — requests still available in this window.

    - **X-RateLimit-Reset** — Unix time (seconds) when the window resets.

    - **X-RateLimit-Scope** — **key** for authenticated non-RPC traffic, or
    **ip**
      for unauthenticated/failed-auth traffic. (RPC uses a separate per-second limit
      and returns only Retry-After on 429.)

    For non-RPC traffic, **X-Org-RateLimit-Limit**,
    **X-Org-RateLimit-Remaining**,

    and **X-Org-RateLimit-Reset** describe your organization-wide allowance
    shared

    across keys; the unprefixed headers report your per-key allowance or the

    organization allowance, whichever has less remaining. RPC proxy traffic is
    limited

    separately and returns only **Retry-After** on 429 (no X-RateLimit headers).


    Read these proactively and slow down before you are throttled. When a limit
    is

    exceeded the API responds **429** with a **Retry-After** header (seconds to

    wait); retry only after that delay.


    These are request-quota boundaries, not dedicated infrastructure: CPU,
    database

    and upstream provider capacity may still be shared. Authentication,
    permissions,

    IP restrictions, signatures and nonce replay protection are unchanged.


    ## Idempotency


    Send an **Idempotency-Key** header (a unique value you generate, e.g. a
    UUID)

    on **POST**, **PATCH** and **DELETE** requests so a network retry never

    applies the same change twice.


    - It is **required** on value-moving endpoints (creating transactions,
      creating addresses, refilling and sponsor-broadcasting gas stations,
      creating wallets); those return **400 idempotency_key_required** if absent.
    - Keys are retained for **24 hours**.

    - Retrying with the same key replays the original response byte-for-byte and
      adds an **Idempotent-Replayed: true** header.
    - Reusing a key with a **different request body** is rejected with
      **409 idempotency_conflict** — generate a fresh key per distinct operation.
    - Only successful (2xx) responses are stored, so a request that failed can
    be
      safely retried with the same key.
    - An Idempotency-Key sent without authentication is rejected with **401**.


    ## Pagination


    List endpoints are cursor-paginated. Parameters:


    - **limit** — page size; bounded by the endpoint maximum.

    - **starting_after** — return the page after this opaque cursor.

    - **ending_before** — return the page before this opaque cursor.


    Supplying both **starting_after** and **ending_before** is rejected with

    **400 ambiguous_cursor**. Every list response is the same envelope:

        {
          "object": "list",
          "data": [ ... ],
          "has_more": true,
          "next_cursor": "b3JnOjE2ODk...",
          "url": "/wallets"
        }

    When **has_more** is true, pass **next_cursor** as **starting_after** to
    fetch

    the following page. Cursors are opaque — do not parse or construct them.


    ## Transaction lifecycle and async outcomes


    Creating a transaction (POST /v1/transactions) returns one of two outcomes:


    - **201 Created** — signed and submitted for broadcast.

    - **202 Accepted** — accepted but not yet executing. The body's **status**
    says why:
      - **pending_authorization** — organization policy requires approval; the
        transaction executes once approvers approve it.
      - **delayed** — a policy time-lock holds the transaction until **execute_at**
        (an ISO-8601 timestamp on the response), when it executes automatically.

      In both cases the transaction is durably persisted and its funds are locked.

    A transaction's **status** moves through these states:


    - **Non-terminal**: pending_signature, pending_authorization, delayed,
    signed,
      awaiting_sponsorship, broadcasting, confirming, pending_screening.
    - **Terminal**: completed, failed, cancelled, rejected.


    Poll **GET /v1/transactions/{id}** (or subscribe to a webhook) until the
    status

    is terminal. An empty **tx_hash** means the transaction has not been
    broadcast

    on-chain yet; once broadcast, **confirmations** climbs until the transaction
    is

    **completed**.


    ## Amounts and units


    All monetary values are JSON **strings**, never numbers — parse them with a

    decimal/big-number type, not a float, to avoid precision loss.


    Every monetary value appears in two forms so you never have to guess
    decimals:


    - **amount** / **fee_amount** — human-readable decimal in the asset's
    display
      unit (e.g. "25.5" USDC).
    - **amount_raw** / **fee_amount_raw** / wallet **balance** — the integer
    value
      in the asset's smallest base unit (e.g. "25500000" for 25.5 USDC at 6
      decimals). **balance** fields are ALWAYS base units.

    **asset_decimals** (and **fee_decimals**) give the conversion:

    display = raw / 10^decimals. The fee is denominated in **fee_asset**, which
    for

    most chains is the native coin (ETH, TRX, …) and differs from the
    transferred

    asset — always read **fee_asset**/**fee_decimals** rather than assuming.
    After a

    transaction confirms, **realized_fee_amount**(_raw) carries the actual fee
    paid.


    Two balance shapes:


    - **GET /v1/wallets/{id}/balances** — one row PER FUNDED ADDRESS (an HD
    wallet
      has many addresses).
    - **GET /v1/wallets/{id}/balances/{asset}** — a single AGGREGATED total for
    that
      asset across the wallet.

    ## Fees


    Control the fee in one of two ways on transaction create and estimate:


    - **fee_level** — "low", "medium", or "high"; the platform picks a rate.

    - **fee_rate** / **max_fee_rate** — Bitcoin only, in sat/vB, for explicit
      control with an optional cap (the transaction will not pay above
      **max_fee_rate**).

    Use **POST /v1/transactions/estimate-fee** to preview low/medium/high quotes
    and

    any gas-funding options before sending.


    ## Errors


    Every error response is the same envelope:

        {
          "error": {
            "type": "invalid_request_error",
            "code": "invalid_cursor",
            "message": "starting_after is not a valid cursor",
            "param": "starting_after",
            "request_id": "req_01HX..."
          }
        }

    **type** is the broad category; **code** is a stable, machine-readable

    identifier — branch on **code**, show **message** to humans, and **param**

    names the offending field when applicable. Some errors also carry a
    **details**

    object with structured, error-specific context (for example the list of

    conflicting fields); treat it as optional and error-specific.


    ### Error code catalogue


    **invalid_request_error** (HTTP 400) — the request was malformed or failed

    validation: invalid_request, invalid_json, invalid_body, empty_body,

    trailing_data, type_mismatch, unknown_field, missing_required_field,

    invalid_id, invalid_cursor, ambiguous_cursor, invalid_expand, invalid_chain,

    invalid_vault_id, wallet_chain_unknown, invalid_transaction_data,

    amount_precision_loss, gas_station_low_balance, body_too_large,

    idempotency_key_required, idempotency_key_invalid, invalid_webhook_id,

    method_not_allowed.


    **authentication_error** (HTTP 401) — the request was not authenticated:

    not_authenticated, auth_required, org_required.


    **permission_error** (HTTP 403) — the request is authenticated but not

    permitted: insufficient_permissions (the API key lacks the required

    permission), ip_not_allowed (the request's source IP is not in the key's IP

    allowlist).


    **not_found_error** (HTTP 404) — the addressed resource does not exist:

    not_found, route_not_found, asset_not_found, event_not_found,

    gas_station_not_found, subscription_not_found, transaction_not_found,

    vault_not_found, wallet_not_found, address_not_found, webhook_not_found.


    **conflict_error** (HTTP 409) — the request conflicts with current state:

    conflict, idempotency_conflict, wallet_archived.


    **precondition_error** (HTTP 412) — a precondition on the request was not
    met,

    for example an ETag mismatch on a conditional request.


    **unprocessable_entity_error** (HTTP 422) — the request was well-formed but
    a

    business rule prevented it from being processed.


    **rate_limit_error** (HTTP 429) — too many requests: rate_limit_exceeded.


    **api_error** (HTTP 500) — an unexpected server error: request_error,

    internal_error.


    **service_unavailable_error** (HTTP 503) — a dependency is unavailable;

    retry after the Retry-After delay: service_unavailable.


    ## Webhook signature verification


    The platform signs every webhook delivery with HMAC-SHA256 using the signing

    secret issued when you create the webhook or subscription (and rotated via
    the

    rotate-secret endpoints — the plaintext secret is shown only once).


    The signature covers the canonical JSON object below, where **data**,

    **event** and **timestamp** are the corresponding fields of the delivered

    payload and **timestamp** is Unix seconds:

        {"data": <data>, "event": "<event>", "timestamp": <unix_seconds>}

    The signed object contains ONLY those three keys — when verifying, build a

    fresh object from the delivery's data, event and timestamp and exclude the

    signature field (and any other body fields) from the HMAC input. Serialize
    the

    keys in lexical order (data, event, timestamp), matching Go's encoding/json.

    The signature value is:

        "sha256=" + hex( HMAC_SHA256(signing_secret, canonical_json) )

    **Platform webhooks** (created via /webhooks) carry this value in the

    **X-Webhook-Signature** response header. **On-chain subscription webhooks**

    (created via /subscriptions) carry it as a **signature** field inside the

    delivered JSON body — there is no signature header.


    To verify a delivery: recompute the signature from your stored secret and

    compare it to the received value with a constant-time comparison. Reject the

    delivery on mismatch, and reject deliveries whose **timestamp** is older
    than

    your tolerance window (a few minutes is typical) to defend against replay.


    ## Webhook event types


    **Platform notification webhooks** (/webhooks) deliver these event types:

    wallet.created, wallet.deleted, wallet.suspended, wallet_address.created,

    transaction.created, transaction.signed, transaction.broadcasting,

    transaction.confirmed, transaction.completed, transaction.received,

    transaction.failed, transaction.cancelled, gas_station.low_balance,

    gas_station.critical_balance, gas_station.auto_refilled, session.created,

    api_key.created, api_key.deleted, policy.violation,

    policy.created, policy.updated, policy.activated, policy.archived,

    policy.deleted, policy.rule.added, policy.rule.updated, policy.rule.deleted,

    kyt.deposit.held, kyt.deposit.flagged, kyt.deposit.approved,
    kyt.deposit.rejected, kyt.deposit.blocked,

    kyt.withdrawal.blocked, kyt.withdrawal.flagged, kyt.sanctions.match,
    kyt.alert.escalated,

    kyt.screening.timeout, kyt.screening.requeued, kyt.screening.completed.


    Use **GET /v1/webhooks/event-types** for the authoritative machine-readable
    list.


    **On-chain subscription webhooks** (/subscriptions) deliver these event

    types: incoming_native_tx, incoming_token_tx, incoming_nft_transfer,

    incoming_internal_tx, outgoing_native_tx, outgoing_token_tx,

    outgoing_nft_transfer, outgoing_internal_tx, tx_failed, tx_pending,

    confirmation_threshold_met, confirmation_finalized, contract_deployed,

    contract_executed, balance_changed.


    ## Versioning and deprecation


    Pin the API version with the **API-Version** request header; omit it to take

    the current default. Additive changes (new endpoints, new optional fields)
    are

    not versioned and may appear at any time — write tolerant clients.


    When an operation or version is scheduled for removal it is marked

    **deprecated** in this specification, and its responses carry a

    **Deprecation** header plus a **Sunset** header (RFC 8594) giving the date

    after which it stops functioning. Monitor those headers and migrate before
    the

    Sunset date.
  title: Bitnob Enterprise API
  version: v1
servers:
  - description: Production
    url: https://api.platform.bitnob.com
security:
  - ApiKeyAuth: []
paths:
  /policies/evaluate:
    post:
      tags:
        - Policies
      summary: Dry-run policy evaluation
      description: >-
        Runs the policy engine against a synthetic transaction request and
        returns the decision without any side effects. No approval requests are
        created, no audit log entries are written, and no transactions are
        inserted.


        Use this endpoint to preview how the active policy would respond to a
        given request before committing to it — for example, when configuring
        automation rules or testing policy changes.


        All request fields are optional. Omitting a field means 'not applicable'
        for that condition axis. When no active policy exists the verdict is
        'no_match' with Allowed=true.


        The response includes a `trace` field containing a per-rule evaluation
        trace when the evaluator supports it. Each entry records whether the
        rule matched and, if not, which condition failed first.
      operationId: postPoliciesEvaluate
      parameters:
        - description: API version (defaults to the latest supported version if omitted).
          in: header
          name: API-Version
          required: false
          schema:
            default: v1
            enum:
              - v1
            type: string
        - description: >-
            Unique key (1-255 printable ASCII chars) used to safely retry write
            operations. Same key + body returns the cached response; same key +
            different body returns 409.
          in: header
          name: Idempotency-Key
          required: false
          schema:
            maxLength: 255
            minLength: 1
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicEvaluateRequest'
        required: true
      responses:
        '200':
          content:
            application/json:
              example:
                allowed: false
                result: matched_rule
                trace:
                  - failed_condition:
                      actual: '500'
                      expected: '10000'
                      field: amount_usd
                      operator: greater_than
                    rule_index: 0
              schema:
                $ref: '#/components/schemas/PublicEvaluateResponse'
          description: Success
          headers:
            Idempotent-Replayed:
              description: >-
                True when this response was replayed from an earlier request
                with the same Idempotency-Key.
              schema:
                type: boolean
            X-RateLimit-Limit:
              description: Requests permitted in the current window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix time (seconds) at which the rate-limit window resets.
              schema:
                type: integer
            X-RateLimit-Scope:
              description: >-
                Scope the rate limit was applied at: key for authenticated API
                traffic, or ip for unauthenticated/failed-auth traffic. RPC
                proxy traffic is limited separately (per second, per
                organization) and is not bucketed here.
              schema:
                type: string
            X-Request-Id:
              description: Unique id for this request; quote it in support requests.
              schema:
                type: string
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Invalid request
        '401':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Authentication required
        '403':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Insufficient permissions
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Conflict
        '429':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Rate limit exceeded
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
        '500':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Internal server error
      security:
        - ApiKeyAuth: []
components:
  schemas:
    PublicEvaluateRequest:
      properties:
        actor_id:
          description: Specific API key ID or user ID of the initiator.
          type: string
        actor_type:
          description: >-
            Initiator class: apikey or session. Empty means rules on actor_type
            won't match.
          type: string
        amount:
          description: Amount as a human-readable decimal in the asset's display unit.
          type: string
        amount_raw:
          description: Amount as an integer in the asset's smallest base unit.
          type: string
        amount_usd:
          description: Approximate USD value of the amount, for USD-threshold rules.
          type: string
        asset_symbol:
          description: Symbol of the asset involved.
          type: string
        automation_rule_id:
          description: >-
            Identifier of the automation rule driving the operation, when
            automated.
          type: string
        chain:
          description: Blockchain the synthetic operation is on.
          type: string
        connection_id:
          description: Identifier of the connection the operation flows through, if any.
          type: string
        destination_type:
          description: Destination classification for the operation.
          type: string
        from_address:
          description: Sender address of the synthetic operation.
          type: string
        is_automated:
          description: Whether the operation is initiated by automation rather than a user.
          type: boolean
        network:
          description: Network the synthetic operation is on.
          type: string
        operation_type:
          description: Operation being evaluated (e.g. the transaction/operation type).
          type: string
        policy_id:
          description: >-
            Evaluate against this specific policy (any state) instead of the
            currently-enforced one. Absent uses the enforced policy.
          type: string
        source_type:
          description: Source classification of the funds/operation.
          type: string
        to_address:
          description: Recipient address of the synthetic operation.
          type: string
        token_address:
          description: Token contract address, for token operations.
          type: string
        tx_type:
          description: Transaction type of the synthetic operation.
          type: string
        user_id:
          description: Initiating user, for rules conditioning on the user.
          type: string
        vault_id:
          description: Vault the synthetic operation targets.
          type: string
        wallet_id:
          description: Wallet the synthetic operation targets.
          type: string
      type: object
    PublicEvaluateResponse:
      properties:
        allowed:
          description: Whether the synthetic request would be allowed.
          example: false
          type: boolean
        delay_minutes:
          description: Delay in minutes applied by a delay-action rule.
          type: integer
        expiry_hours:
          description: Approval expiry window in hours, when approval is required.
          type: integer
        policy_id:
          description: Identifier of the policy that produced the decision.
          type: string
        reason:
          description: Human-readable explanation of the decision.
          type: string
        required_approvals:
          description: >-
            Number of approvals required (the N in N-of-M), when approval is
            required.
          type: integer
        requires_approval:
          description: True when the matched rule's action is require_approval.
          type: boolean
        result:
          description: >-
            Decision outcome: matched_rule, quorum, no_match, approved,
            rejected, awaiting_approval, or delayed.
          example: matched_rule
          type: string
        rule_id:
          description: Identifier of the specific rule that matched, if any.
          type: string
        trace:
          description: Per-rule evaluation trace; omitted on no_match / quorum-only paths.
          items:
            $ref: '#/components/schemas/PublicRuleTraceEntry'
          type: array
      type: object
    Error:
      description: Standard error envelope.
      properties:
        error:
          properties:
            code:
              type: string
            details:
              additionalProperties: true
              type: object
            message:
              type: string
            param:
              type: string
            request_id:
              type: string
            type:
              type: string
          type: object
      type: object
    PublicRuleTraceEntry:
      properties:
        action:
          description: Action the rule would take when matched.
          type: string
        failed_condition:
          $ref: '#/components/schemas/PublicRuleTraceFailedCondition'
          description: The first condition that failed, when the rule did not match.
        matched:
          description: Whether all of the rule's conditions matched.
          type: boolean
        note:
          description: Additional note about this rule's evaluation.
          type: string
        reason:
          description: Why the rule matched or did not match.
          type: string
        rule_id:
          description: Identifier of the rule.
          type: string
        rule_index:
          description: Zero-based position of the rule in evaluation order.
          example: 0
          type: integer
        rule_name:
          description: Name of the rule.
          type: string
        terminal:
          description: Whether evaluation stopped at this rule (first-match-wins).
          type: boolean
      type: object
    PublicRuleTraceFailedCondition:
      properties:
        actual:
          description: Actual value seen during evaluation.
          example: '500'
          type: string
        expected:
          description: Value the condition expected.
          example: '10000'
          type: string
        field:
          description: The request attribute the failed condition compared.
          example: amount_usd
          type: string
        operator:
          description: Operator of the failed condition.
          example: greater_than
          type: string
      type: object
  securitySchemes:
    ApiKeyAuth:
      in: header
      name: X-API-Key
      type: apiKey

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.