{
  "info": {
    "_postman_id": "1e39cd22-7c7b-4d88-980c-1f95856c5d10",
    "name": "Neo24 Public API (product)",
    "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",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{token}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api.neo24.net",
      "type": "string"
    },
    {
      "key": "token",
      "value": "",
      "type": "string"
    },
    {
      "key": "address",
      "value": "1",
      "type": "string"
    },
    {
      "key": "match",
      "value": "1",
      "type": "string"
    },
    {
      "key": "blockchain",
      "value": "ethereum",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "Account",
      "item": [
        {
          "name": "Current user",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "me"
              ],
              "query": []
            },
            "description": "Requires ability `me:read` (or `*`)."
          },
          "response": []
        },
        {
          "name": "Current user (alias of /me)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/user",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "user"
              ],
              "query": []
            },
            "description": "Same handler and ability as `GET /me`."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Blockchains",
      "item": [
        {
          "name": "List active blockchains",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/blockchains",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "blockchains"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "per_page",
                  "value": "",
                  "description": "Clamped to 1..200 (config `API_PAGE_MAX`, default max 200). Default 50.",
                  "disabled": true
                },
                {
                  "key": "is_testnet",
                  "value": "",
                  "description": "When present, filter by testnet flag. Omitted returns both.",
                  "disabled": true
                },
                {
                  "key": "slug",
                  "value": "",
                  "description": "",
                  "disabled": true
                }
              ]
            },
            "description": "Inactive chains are omitted. Requires `blockchains:read` (or `*`)."
          },
          "response": []
        },
        {
          "name": "Get one active blockchain",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/blockchains/{{blockchain}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "blockchains",
                "{{blockchain}}"
              ],
              "query": []
            },
            "description": "`{blockchain}` is a numeric id or a slug. Inactive chains return 404.\nRequires `blockchains:read` (or `*`).\n"
          },
          "response": []
        }
      ]
    },
    {
      "name": "Addresses",
      "item": [
        {
          "name": "List watched addresses",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/addresses",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "addresses"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "per_page",
                  "value": "",
                  "description": "Clamped to 1..200 (config `API_PAGE_MAX`, default max 200). Default 50.",
                  "disabled": true
                },
                {
                  "key": "is_active",
                  "value": "",
                  "description": "Filter the caller's subscription active flag.",
                  "disabled": true
                },
                {
                  "key": "blockchain_id",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "blockchain_slug",
                  "value": "",
                  "description": "",
                  "disabled": true
                }
              ]
            },
            "description": "Only addresses subscribed by the token owner, newest id first. Requires `addresses:read` (or `*`)."
          },
          "response": []
        },
        {
          "name": "Watch an address",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/addresses",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "addresses"
              ],
              "query": []
            },
            "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",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"blockchain_slug\": \"ethereum\",\n  \"address\": \"0xABCDEFabcdefABCDEFabcdefABCDEFabcdefABCD\",\n  \"name\": \"treasury\",\n  \"is_active\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Get one watched address",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/addresses/{{address}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "addresses",
                "{{address}}"
              ],
              "query": []
            },
            "description": "404 if the id is not subscribed by the caller. Requires `addresses:read` (or `*`)."
          },
          "response": []
        },
        {
          "name": "Rename or pause a subscription",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/addresses/{{address}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "addresses",
                "{{address}}"
              ],
              "query": []
            },
            "description": "Updates only the caller's pivot (`name`, `is_active`). The on-chain address\nand chain cannot change. Requires `addresses:write` (or `*`).\n",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"renamed\",\n  \"is_active\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Stop watching an address",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/addresses/{{address}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "addresses",
                "{{address}}"
              ],
              "query": []
            },
            "description": "Removes the caller's subscription. Requires `addresses:write` (or `*`)."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Matches",
      "item": [
        {
          "name": "Matches for one watched address",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/addresses/{{address}}/matches",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "addresses",
                "{{address}}",
                "matches"
              ],
              "query": [
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Opaque cursor from `meta.next_cursor`. Invalid values return 422.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "",
                  "description": "Clamped to 1..200 (config `API_CURSOR_MAX`, default max 200). Default 50.",
                  "disabled": true
                },
                {
                  "key": "blockchain_id",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "blockchain_slug",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "txid",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "from",
                  "value": "",
                  "description": "Inclusive lower bound on match `created_at`.",
                  "disabled": true
                },
                {
                  "key": "to",
                  "value": "",
                  "description": "Inclusive upper bound on match `created_at`.",
                  "disabled": true
                }
              ]
            },
            "description": "Cursor page, newest match id first. 404 if the address is not the caller's.\nRequires `matches:read` (or `*`).\n"
          },
          "response": []
        },
        {
          "name": "List matches",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/matches",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "matches"
              ],
              "query": [
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Opaque cursor from `meta.next_cursor`. Invalid values return 422.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "",
                  "description": "Clamped to 1..200 (config `API_CURSOR_MAX`, default max 200). Default 50.",
                  "disabled": true
                },
                {
                  "key": "address_id",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "blockchain_id",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "blockchain_slug",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "txid",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "from",
                  "value": "",
                  "description": "Inclusive lower bound on match `created_at`.",
                  "disabled": true
                },
                {
                  "key": "to",
                  "value": "",
                  "description": "Inclusive upper bound on match `created_at`.",
                  "disabled": true
                }
              ]
            },
            "description": "Cursor page of matches on the caller's addresses, newest id first.\nRequires `matches:read` (or `*`).\n"
          },
          "response": []
        },
        {
          "name": "Get one match",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/matches/{{match}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "matches",
                "{{match}}"
              ],
              "query": []
            },
            "description": "404 if the match is not on one of the caller's addresses. Requires `matches:read` (or `*`)."
          },
          "response": []
        }
      ]
    },
    {
      "name": "WebSocket",
      "item": [
        {
          "name": "Mint Centrifugo credentials",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/ws/connection",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "ws",
                "connection"
              ],
              "query": []
            },
            "description": "Requires `ws:connect` (or `*`). No request body.\nReturns 503 when `CENTRIFUGO_ENABLED` is false.\n"
          },
          "response": []
        }
      ]
    }
  ]
}
