{
  "openapi": "3.0.3",
  "info": {
    "title": "Neo24 Public API (product)",
    "version": "1.0.0",
    "description": "Watch blockchain addresses and read matched incoming transfers (native coin and fungible tokens).\n\nAuthenticate with a Sanctum personal access token created in the Users panel:\n`Authorization: Bearer {token}`.\n\nThe account must be active and the email must be verified. Each route also requires\none ability. A token with `*` satisfies every ability.\n\n| Ability | Routes |\n|---|---|\n| `me:read` | `GET /me`, `GET /user` |\n| `blockchains:read` | `GET /blockchains`, `GET /blockchains/{blockchain}` |\n| `addresses:read` | `GET /addresses`, `GET /addresses/{address}` |\n| `addresses:write` | `POST /addresses`, `PATCH /addresses/{address}`, `DELETE /addresses/{address}` |\n| `matches:read` | `GET /matches`, `GET /matches/{match}`, `GET /addresses/{address}/matches` |\n| `ws:connect` | `POST /ws/connection` |\n| `*` | all of the above |\n\nTokens do not expire globally. Optional expiry is chosen when the token is created.\n`transactions:read` is accepted as an alias of `matches:read` and stored as `matches:read`.\n\n**This file** is the product contract. Base URL is `https://api.neo24.net` (no `/api` prefix).\n\n**Pagination.** Blockchains and addresses use Laravel page pagination (`page`, `per_page`;\ndefault 50, max 200). Matches use an opaque cursor (`limit`, `cursor`; default 50, max 200),\nnewest id first. Pass `meta.next_cursor` back as `cursor`.\n\n**Start here.** In the Users panel create a Sanctum token (grant the abilities you need, or `*`).\n`GET /blockchains`, then `POST /addresses`. After that, poll `GET /matches`, or receive the same\nevent live over WebSocket or webhook. WebSocket and webhook destinations are turned on in the\nUsers panel. This API does not create them.\n\n**WebSocket.** The client receives events. It does not send match data on the socket.\n\n1. Add a WebSocket destination in the Users panel. Without it, nothing is published.\n2. `POST /ws/connection` with `Authorization: Bearer {sanctum token}` and ability `ws:connect`.\n   The response `token` is a Centrifugo connection JWT, separate from the Sanctum token.\n   It expires at `expires_at` (unix seconds). Call `POST /ws/connection` again to refresh it.\n3. Connect to `ws_url` with that JWT. Subscribe to `channel` with `{ recoverable: true }`.\n4. On a brand-new connection, call `subscription.history({ limit: history_size })` for the backlog.\n   A reconnect that still has the stream offset recovers on its own.\n5. Each publication `data` is one `incoming_payment` object (shape below).\n\n```js\nimport { Centrifuge } from 'centrifuge'\n\nconst creds = await fetch(baseUrl + '/ws/connection', {\n  method: 'POST',\n  headers: { Authorization: 'Bearer ' + sanctumToken, Accept: 'application/json' },\n}).then((r) => r.json())\n\nconst centrifuge = new Centrifuge(creds.ws_url, { token: creds.token })\nconst sub = centrifuge.newSubscription(creds.channel, { recoverable: true })\n\nsub.on('publication', (ctx) => {\n  console.log(ctx.data) // { type, match_id, fields }\n})\n\nsub.on('subscribed', async () => {\n  const history = await sub.history({ limit: creds.history_size })\n  for (const pub of history.publications) console.log(pub.data)\n})\n\nsub.subscribe()\ncentrifuge.connect()\n```\n\n`history_ttl` (default `2h`) and `history_size` (default `200`) bound that backlog.\nIf WebSocket delivery is disabled on the server, `POST /ws/connection` returns 503.\n\nPublication payload:\n\n```json\n{\n  \"type\": \"incoming_payment\",\n  \"match_id\": 123,\n  \"fields\": {\n    \"chain_slug\": \"bnb\",\n    \"chain_name\": \"BNB Mainnet\",\n    \"address\": \"0x…\",\n    \"amount\": \"1.0\",\n    \"asset\": \"native\",\n    \"txid\": \"0x…\",\n    \"block\": \"100\",\n    \"type\": \"coin\"\n  }\n}\n```\n\n**Webhooks.** Add the destination in the Users panel: an HTTPS URL that resolves to a public\naddress, and a signing secret of at least 16 characters. Neo24 `POST`s the same JSON body as\nthe WebSocket event.\n\n```http\nContent-Type: application/json\nUser-Agent: Neo24-Webhook/1.0\nX-Neo24-Event: incoming_payment\nX-Neo24-Delivery-Id: 123\nX-Neo24-Signature: sha256={hex}\n```\n\n`X-Neo24-Signature` is `sha256=` plus HMAC-SHA256 of the **raw** body, keyed with the signing\nsecret. Verify those exact bytes. Re-encoding the JSON changes the signature.\n\n```php\n$raw = file_get_contents('php://input');\n$sig = $_SERVER['HTTP_X_NEO24_SIGNATURE'] ?? '';\n$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);\n\nif (! hash_equals($expected, $sig)) {\n    http_response_code(401);\n    exit;\n}\n\n$event = json_decode($raw, true);\nhttp_response_code(200);\n```\n\nReply with any HTTP 2xx within 10 seconds. Any other status or a timeout is retried up to 5\ntimes (after 30s, 2m, 10m, and 30m). Redirects are not followed.\n",
    "contact": {
      "name": "Neo24",
      "url": "https://neo24.net"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.neo24.net",
      "description": "Product (`API_DOMAIN` set — no /api prefix)"
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Authenticated profile and plan limits."
    },
    {
      "name": "Blockchains",
      "description": "Active chains the product can watch."
    },
    {
      "name": "Addresses",
      "description": "The caller's watched addresses."
    },
    {
      "name": "Matches",
      "description": "Incoming transfers matched to the caller's addresses."
    },
    {
      "name": "WebSocket",
      "description": "Connect with a Centrifugo token and receive live incoming_payment events. Open this section for the client steps."
    },
    {
      "name": "Webhooks",
      "description": "Verify the signature and accept Neo24's incoming_payment POST."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "webhooks": {
    "incomingPayment": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "deliverIncomingPayment",
        "summary": "Incoming payment",
        "description": "Neo24 POSTs this body to the HTTPS URL saved on the Webhook destination in the Users panel.\nThis API does not create that destination.\n\n```http\nContent-Type: application/json\nUser-Agent: Neo24-Webhook/1.0\nX-Neo24-Event: incoming_payment\nX-Neo24-Delivery-Id: 123\nX-Neo24-Signature: sha256={hex}\n```\n\n`X-Neo24-Signature` is `sha256=` plus HMAC-SHA256 of the raw body, keyed with the signing secret\n(at least 16 characters). Verify those exact bytes. Re-encoding the JSON changes the signature.\n\n```php\n$raw = file_get_contents('php://input');\n$sig = $_SERVER['HTTP_X_NEO24_SIGNATURE'] ?? '';\n$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);\n\nif (! hash_equals($expected, $sig)) {\n    http_response_code(401);\n    exit;\n}\n\n$event = json_decode($raw, true);\nhttp_response_code(200);\n```\n\nAny HTTP 2xx within 10 seconds accepts the delivery. Anything else is retried up to 5 times\n(after 30s, 2m, 10m, and 30m). Redirects are not followed.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type",
                  "match_id",
                  "fields"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "example": "incoming_payment"
                  },
                  "match_id": {
                    "type": "integer"
                  },
                  "fields": {
                    "type": "object",
                    "properties": {
                      "chain_slug": {
                        "type": "string"
                      },
                      "chain_name": {
                        "type": "string"
                      },
                      "address": {
                        "type": "string"
                      },
                      "amount": {
                        "type": "string"
                      },
                      "asset": {
                        "type": "string"
                      },
                      "txid": {
                        "type": "string"
                      },
                      "block": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Any 2xx status accepts the delivery."
          }
        }
      }
    }
  },
  "paths": {
    "/me": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getMe",
        "summary": "Current user",
        "description": "Requires ability `me:read` (or `*`).",
        "x-required-ability": "me:read",
        "responses": {
          "200": {
            "description": "Profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserEnvelope"
                },
                "example": {
                  "data": {
                    "id": 1,
                    "name": "Ada",
                    "email": "ada@example.com",
                    "plan": "free",
                    "plan_label": "Free",
                    "max_addresses": 1,
                    "address_count": 0
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/user": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getUser",
        "summary": "Current user (alias of /me)",
        "description": "Same handler and ability as `GET /me`.",
        "x-required-ability": "me:read",
        "responses": {
          "200": {
            "description": "Profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/blockchains": {
      "get": {
        "tags": [
          "Blockchains"
        ],
        "operationId": "listBlockchains",
        "summary": "List active blockchains",
        "description": "Inactive chains are omitted. Requires `blockchains:read` (or `*`).",
        "x-required-ability": "blockchains:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          },
          {
            "name": "is_testnet",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "When present, filter by testnet flag. Omitted returns both."
          },
          {
            "name": "slug",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of blockchains",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockchainPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/blockchains/{blockchain}": {
      "get": {
        "tags": [
          "Blockchains"
        ],
        "operationId": "getBlockchain",
        "summary": "Get one active blockchain",
        "description": "`{blockchain}` is a numeric id or a slug. Inactive chains return 404.\nRequires `blockchains:read` (or `*`).\n",
        "x-required-ability": "blockchains:read",
        "parameters": [
          {
            "name": "blockchain",
            "in": "path",
            "required": true,
            "description": "Numeric id or slug.",
            "schema": {
              "type": "string",
              "example": "ethereum"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Blockchain",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockchainEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/addresses": {
      "get": {
        "tags": [
          "Addresses"
        ],
        "operationId": "listAddresses",
        "summary": "List watched addresses",
        "description": "Only addresses subscribed by the token owner, newest id first. Requires `addresses:read` (or `*`).",
        "x-required-ability": "addresses:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          },
          {
            "name": "is_active",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Filter the caller's subscription active flag."
          },
          {
            "name": "blockchain_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "blockchain_slug",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of addresses",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddressPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Addresses"
        ],
        "operationId": "createAddress",
        "summary": "Watch an address",
        "description": "Provide `blockchain_id` or `blockchain_slug` (one is required). The on-chain\naddress is canonicalized per chain. Plan capacity is enforced (422 when full).\nRequires `addresses:write` (or `*`). `addresses:read` alone cannot create.\n",
        "x-required-ability": "addresses:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddressCreate"
              },
              "example": {
                "blockchain_slug": "ethereum",
                "address": "0xABCDEFabcdefABCDEFabcdefABCDEFabcdefABCD",
                "name": "treasury",
                "is_active": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddressEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/addresses/{address}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AddressId"
        }
      ],
      "get": {
        "tags": [
          "Addresses"
        ],
        "operationId": "getAddress",
        "summary": "Get one watched address",
        "description": "404 if the id is not subscribed by the caller. Requires `addresses:read` (or `*`).",
        "x-required-ability": "addresses:read",
        "responses": {
          "200": {
            "description": "Address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddressEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Addresses"
        ],
        "operationId": "updateAddress",
        "summary": "Rename or pause a subscription",
        "description": "Updates only the caller's pivot (`name`, `is_active`). The on-chain address\nand chain cannot change. Requires `addresses:write` (or `*`).\n",
        "x-required-ability": "addresses:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddressUpdate"
              },
              "example": {
                "name": "renamed",
                "is_active": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddressEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "tags": [
          "Addresses"
        ],
        "operationId": "deleteAddress",
        "summary": "Stop watching an address",
        "description": "Removes the caller's subscription. Requires `addresses:write` (or `*`).",
        "x-required-ability": "addresses:write",
        "responses": {
          "204": {
            "description": "Unsubscribed. Empty body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/addresses/{address}/matches": {
      "get": {
        "tags": [
          "Matches"
        ],
        "operationId": "listAddressMatches",
        "summary": "Matches for one watched address",
        "description": "Cursor page, newest match id first. 404 if the address is not the caller's.\nRequires `matches:read` (or `*`).\n",
        "x-required-ability": "matches:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/AddressId"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/BlockchainId"
          },
          {
            "$ref": "#/components/parameters/BlockchainSlug"
          },
          {
            "$ref": "#/components/parameters/Txid"
          },
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          }
        ],
        "responses": {
          "200": {
            "description": "Cursor page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchCursorPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/matches": {
      "get": {
        "tags": [
          "Matches"
        ],
        "operationId": "listMatches",
        "summary": "List matches",
        "description": "Cursor page of matches on the caller's addresses, newest id first.\nRequires `matches:read` (or `*`).\n",
        "x-required-ability": "matches:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "name": "address_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "$ref": "#/components/parameters/BlockchainId"
          },
          {
            "$ref": "#/components/parameters/BlockchainSlug"
          },
          {
            "$ref": "#/components/parameters/Txid"
          },
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          }
        ],
        "responses": {
          "200": {
            "description": "Cursor page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchCursorPage"
                },
                "example": {
                  "data": [
                    {
                      "id": 90,
                      "address_id": 4,
                      "block": 1002,
                      "txid": "0xabc",
                      "transfer_index": 1,
                      "from_address": "0xfrom",
                      "amount": "1.5",
                      "asset": null,
                      "type": "coin",
                      "count": 1,
                      "blockchain": {
                        "id": 1,
                        "slug": "ethereum",
                        "name": "Ethereum"
                      },
                      "address": {
                        "id": 4,
                        "address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
                        "address_display": "0xABCDEFabcdefABCDEFabcdefABCDEFabcdefABCD"
                      },
                      "created_at": "2026-10-21T10:00:00+00:00"
                    }
                  ],
                  "meta": {
                    "next_cursor": "eyJpZCI6ODh9",
                    "has_more": true,
                    "limit": 50
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "Invalid cursor",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                },
                "example": {
                  "message": "The given data was invalid.",
                  "errors": {
                    "cursor": [
                      "Invalid cursor."
                    ]
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/matches/{match}": {
      "get": {
        "tags": [
          "Matches"
        ],
        "operationId": "getMatch",
        "summary": "Get one match",
        "description": "404 if the match is not on one of the caller's addresses. Requires `matches:read` (or `*`).",
        "x-required-ability": "matches:read",
        "parameters": [
          {
            "name": "match",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Match",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/ws/connection": {
      "post": {
        "tags": [
          "WebSocket"
        ],
        "operationId": "createWsConnection",
        "summary": "Connect and receive live payments",
        "description": "The client receives events. It does not send match data on the socket.\n\n1. Add a WebSocket destination in the Users panel. Without it, nothing is published.\n2. `POST /ws/connection` with the Sanctum token and ability `ws:connect`. No request body.\n   The response `token` is a Centrifugo JWT, not the Sanctum token. It expires at `expires_at`.\n   Call this endpoint again to refresh it.\n3. Connect to `ws_url` with that JWT. Subscribe to `channel` with `{ recoverable: true }`.\n4. On a new connection, call `subscription.history({ limit: history_size })`.\n5. Each publication `data` is `{ type: incoming_payment, match_id, fields }`.\n\n```js\nimport { Centrifuge } from 'centrifuge'\n\nconst creds = await fetch(baseUrl + '/ws/connection', {\n  method: 'POST',\n  headers: { Authorization: 'Bearer ' + sanctumToken, Accept: 'application/json' },\n}).then((r) => r.json())\n\nconst centrifuge = new Centrifuge(creds.ws_url, { token: creds.token })\nconst sub = centrifuge.newSubscription(creds.channel, { recoverable: true })\nsub.on('publication', (ctx) => console.log(ctx.data))\nsub.on('subscribed', async () => {\n  const history = await sub.history({ limit: creds.history_size })\n  for (const pub of history.publications) console.log(pub.data)\n})\nsub.subscribe()\ncentrifuge.connect()\n```\n\nReturns 503 when WebSocket delivery is disabled.\n",
        "x-required-ability": "ws:connect",
        "responses": {
          "200": {
            "description": "Connection credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WsConnection"
                },
                "example": {
                  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example",
                  "ws_url": "wss://ws.neo24.net/ws",
                  "channel": "personal:#1",
                  "expires_at": 1760000000,
                  "history_size": 200,
                  "history_ttl": "2h"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "description": "WebSocket notifications are disabled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                },
                "example": {
                  "message": "WebSocket notifications are disabled."
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Sanctum",
        "description": "Personal access token from the Users panel.\nHeader: `Authorization: Bearer {token}`.\n"
      }
    },
    "parameters": {
      "Page": {
        "name": "page",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "default": 1
        }
      },
      "PerPage": {
        "name": "per_page",
        "in": "query",
        "required": false,
        "description": "Clamped to 1..200 (config `API_PAGE_MAX`, default max 200). Default 50.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200,
          "default": 50
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "Opaque cursor from `meta.next_cursor`. Invalid values return 422.",
        "schema": {
          "type": "string"
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Clamped to 1..200 (config `API_CURSOR_MAX`, default max 200). Default 50.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200,
          "default": 50
        }
      },
      "AddressId": {
        "name": "address",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "BlockchainId": {
        "name": "blockchain_id",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "BlockchainSlug": {
        "name": "blockchain_slug",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        }
      },
      "Txid": {
        "name": "txid",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        }
      },
      "From": {
        "name": "from",
        "in": "query",
        "required": false,
        "description": "Inclusive lower bound on match `created_at`.",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "To": {
        "name": "to",
        "in": "query",
        "required": false,
        "description": "Inclusive upper bound on match `created_at`.",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid token",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Message"
            },
            "example": {
              "message": "Unauthenticated."
            }
          }
        }
      },
      "Forbidden": {
        "description": "Token lacks the route ability (`Invalid ability provided.`),\nthe email is unverified (`Your email address is not verified.`),\nor the account is deactivated (`Account is deactivated.`).\n",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/Message"
                },
                {
                  "$ref": "#/components/schemas/Deactivated"
                }
              ]
            },
            "examples": {
              "ability": {
                "value": {
                  "message": "Invalid ability provided."
                }
              },
              "unverified": {
                "value": {
                  "message": "Your email address is not verified."
                }
              },
              "deactivated": {
                "value": {
                  "success": false,
                  "message": "Account is deactivated."
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Unknown route returns `{ \"success\": false, \"message\": \"Invalid URL\" }`.\nA missing model returns Laravel's default message.\n",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/Message"
                },
                {
                  "$ref": "#/components/schemas/InvalidUrl"
                }
              ]
            }
          }
        }
      },
      "Unprocessable": {
        "description": "Validation failed, or the plan address cap is full.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ValidationError"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "API rate limit",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Message"
            },
            "example": {
              "message": "Too Many Attempts."
            }
          }
        }
      }
    },
    "schemas": {
      "Message": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string"
          }
        }
      },
      "Deactivated": {
        "type": "object",
        "required": [
          "success",
          "message"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "message": {
            "type": "string",
            "example": "Account is deactivated."
          }
        }
      },
      "InvalidUrl": {
        "type": "object",
        "required": [
          "success",
          "message"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "message": {
            "type": "string",
            "example": "Invalid URL"
          }
        }
      },
      "ValidationError": {
        "type": "object",
        "required": [
          "message",
          "errors"
        ],
        "properties": {
          "message": {
            "type": "string"
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        }
      },
      "PageLinks": {
        "type": "object",
        "properties": {
          "first": {
            "type": "string",
            "nullable": true
          },
          "last": {
            "type": "string",
            "nullable": true
          },
          "prev": {
            "type": "string",
            "nullable": true
          },
          "next": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "PageMeta": {
        "type": "object",
        "properties": {
          "current_page": {
            "type": "integer"
          },
          "from": {
            "type": "integer",
            "nullable": true
          },
          "last_page": {
            "type": "integer"
          },
          "path": {
            "type": "string"
          },
          "per_page": {
            "type": "integer"
          },
          "to": {
            "type": "integer",
            "nullable": true
          },
          "total": {
            "type": "integer"
          }
        }
      },
      "ChainRef": {
        "type": "object",
        "required": [
          "id",
          "slug",
          "name"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "User": {
        "type": "object",
        "required": [
          "id",
          "name",
          "email",
          "plan",
          "plan_label",
          "address_count"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "plan": {
            "type": "string",
            "example": "free"
          },
          "plan_label": {
            "type": "string",
            "example": "Free"
          },
          "max_addresses": {
            "type": "integer",
            "nullable": true,
            "description": "Null means unlimited."
          },
          "address_count": {
            "type": "integer"
          }
        }
      },
      "UserEnvelope": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/User"
          }
        }
      },
      "Blockchain": {
        "type": "object",
        "required": [
          "id",
          "name",
          "slug",
          "is_testnet"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "symbol": {
            "type": "string",
            "nullable": true
          },
          "decimals": {
            "type": "integer",
            "nullable": true
          },
          "chain_type": {
            "type": "string",
            "nullable": true,
            "example": "evm-based"
          },
          "chain_id": {
            "type": "string",
            "nullable": true
          },
          "network": {
            "type": "string",
            "nullable": true
          },
          "is_testnet": {
            "type": "boolean"
          },
          "explorer_url": {
            "type": "string",
            "nullable": true
          },
          "explorer_tx_template": {
            "type": "string",
            "nullable": true
          },
          "explorer_address_template": {
            "type": "string",
            "nullable": true
          },
          "explorer_block_template": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "BlockchainEnvelope": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Blockchain"
          }
        }
      },
      "BlockchainPage": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Blockchain"
            }
          },
          "links": {
            "$ref": "#/components/schemas/PageLinks"
          },
          "meta": {
            "$ref": "#/components/schemas/PageMeta"
          }
        }
      },
      "Address": {
        "type": "object",
        "required": [
          "id",
          "is_active",
          "address"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "is_active": {
            "type": "boolean"
          },
          "address": {
            "type": "string",
            "description": "Canonical on-chain form."
          },
          "address_display": {
            "type": "string",
            "nullable": true
          },
          "blockchain": {
            "$ref": "#/components/schemas/ChainRef"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "AddressEnvelope": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Address"
          }
        }
      },
      "AddressPage": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Address"
            }
          },
          "links": {
            "$ref": "#/components/schemas/PageLinks"
          },
          "meta": {
            "$ref": "#/components/schemas/PageMeta"
          }
        }
      },
      "AddressCreate": {
        "type": "object",
        "required": [
          "address"
        ],
        "properties": {
          "blockchain_id": {
            "type": "integer",
            "nullable": true,
            "description": "Required when `blockchain_slug` is absent."
          },
          "blockchain_slug": {
            "type": "string",
            "nullable": true,
            "maxLength": 100,
            "description": "Required when `blockchain_id` is absent."
          },
          "address": {
            "type": "string",
            "maxLength": 255
          },
          "name": {
            "type": "string",
            "nullable": true,
            "maxLength": 255
          },
          "is_active": {
            "type": "boolean",
            "default": true
          }
        }
      },
      "AddressUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true,
            "maxLength": 255
          },
          "is_active": {
            "type": "boolean"
          }
        }
      },
      "MatchAddress": {
        "type": "object",
        "required": [
          "id",
          "address"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "address": {
            "type": "string"
          },
          "address_display": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Match": {
        "type": "object",
        "required": [
          "id",
          "address_id",
          "block",
          "txid",
          "transfer_index",
          "amount",
          "type",
          "count"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "address_id": {
            "type": "integer"
          },
          "block": {
            "type": "integer"
          },
          "txid": {
            "type": "string"
          },
          "transfer_index": {
            "type": "integer"
          },
          "from_address": {
            "type": "string",
            "nullable": true
          },
          "amount": {
            "type": "string",
            "description": "Decimal string, not a float."
          },
          "asset": {
            "type": "string",
            "nullable": true,
            "description": "Null means the chain native asset."
          },
          "type": {
            "type": "string",
            "example": "coin"
          },
          "count": {
            "type": "integer"
          },
          "blockchain": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/ChainRef"
              }
            ]
          },
          "address": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/MatchAddress"
              }
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "MatchEnvelope": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Match"
          }
        }
      },
      "MatchCursorPage": {
        "type": "object",
        "required": [
          "data",
          "meta"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Match"
            }
          },
          "meta": {
            "type": "object",
            "required": [
              "next_cursor",
              "has_more",
              "limit"
            ],
            "properties": {
              "next_cursor": {
                "type": "string",
                "nullable": true
              },
              "has_more": {
                "type": "boolean"
              },
              "limit": {
                "type": "integer"
              }
            }
          }
        }
      },
      "WsConnection": {
        "type": "object",
        "required": [
          "token",
          "ws_url",
          "channel",
          "expires_at",
          "history_size",
          "history_ttl"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "Centrifugo HS256 connection JWT. `sub` is the user id. No channels claim."
          },
          "ws_url": {
            "type": "string",
            "example": "wss://ws.neo24.net/ws"
          },
          "channel": {
            "type": "string",
            "example": "personal:#1"
          },
          "expires_at": {
            "type": "integer",
            "description": "Unix timestamp (seconds)."
          },
          "history_size": {
            "type": "integer",
            "example": 200
          },
          "history_ttl": {
            "type": "string",
            "example": "2h"
          }
        }
      }
    }
  }
}
