openapi: 3.0.3
info:
  title: Chainy API
  version: 1.0.0
  description: >
    Chainy is an AML/KYT API for crypto compliance. Screen wallet addresses and incoming
    deposits, get a risk score and an allow / review / reject decision, and read your account
    balance — all over a simple REST API.


    ## Authentication

    All endpoints require your API key in the `X-API-Key` header. Create and manage keys in
    the Chainy dashboard (API Keys).


    ## Sandbox

    A sandbox API key returns realistic mock responses (with `"sandbox": true`) and is never
    billed — use it to build and test your integration before going live.


    ## Pricing

    Wallet checks are billed per request from your prepaid balance (new accounts start with a
    few free checks). See `GET /api/external/balance`.


    ## Idempotency

    Pass an `Idempotency-Key` header on `POST /api/external/checks/create` to safely retry a
    request without creating or charging twice — the original result is returned for repeats.


    ## Errors

    Errors return `{ "status": false, "error": "<message>" }` with an appropriate HTTP status
    (400 invalid input, 401 missing/invalid key, 402 insufficient balance, 404 not found,
    409 idempotency conflict, 429 rate limited, 500 server error).
servers:
  - url: https://app.chainysoft.io
    description: Production

security:
  - ApiKeyAuth: []

tags:
  - name: Wallet checks
    description: Screen a wallet address for AML risk.
  - name: Deposit checks
    description: Screen an incoming on-chain deposit (transaction) for AML risk.
  - name: Account
    description: Account balance and pricing.
  - name: Risk model
    description: The active risk model applied to your checks.
  - name: Utilities
    description: Supporting data endpoints.
  - name: Webhooks
    description: |
      Instead of polling, register a callback URL and we POST each result to you.

      **Registering.** Webhooks are managed from your dashboard (API Keys → Webhooks),
      not through this API. You choose which events to receive and get a signing
      secret, shown once.

      **The request we send.** `POST` to your URL with `Content-Type: application/json`
      and this body:

      ```json
      { "event": "check.completed", "eventId": "check.completed:1234:COMPLETED", "data": { } }
      ```

      `data` is exactly the object this API would have returned for that subject — the
      same white-labelled shape, never raw provider output.

      Headers on every delivery:

      | Header | Meaning |
      |---|---|
      | `X-Chainy-Signature` | `t=<unix-seconds>,v1=<hex>` — see below |
      | `X-Chainy-Event-Id` | Stable id for this event. **Use it to deduplicate.** |
      | `X-Chainy-Event-Type` | `check.completed` or `deposit.decision` |
      | `X-Chainy-Delivery-Attempt` | 1 for the first try, then 2, 3, … |

      **Verifying the signature.** Take `t` and `v1` from `X-Chainy-Signature`, compute
      `HMAC-SHA256(secret, "<t>.<raw request body>")` and compare it to `v1` in constant
      time. Sign the **raw bytes** you received — re-serialising the parsed JSON will not
      match. Reject a delivery whose `t` is far from your clock to blunt replays.

      **Delivery semantics.** At-least-once: a retry re-signs with a fresh timestamp, so
      dedupe on `X-Chainy-Event-Id`, not on the signature. Reply `2xx` to acknowledge;
      anything else is retried with exponential backoff (~4s, 8s, 16s, … capped at one
      hour) up to 8 attempts, after which the delivery is dropped. Redirects are not
      followed — respond directly on the registered URL.

paths:
  /api/external/checks/create:
    post:
      tags: [Wallet checks]
      summary: Create a wallet check
      description: Screen a wallet address. Billed one check from your balance (or a free check, if any remain).
      operationId: createCheck
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [address, network]
              properties:
                address:
                  type: string
                  example: '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae'
                network:
                  type: string
                  description: Blockchain network.
                  enum: [eth, bsc, trx, sol, btc, ltc]
                  example: eth
      responses:
        '200':
          description: Check created (and, depending on network, scored).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/WalletCheck' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/ServerError' }
      callbacks:
        checkCompleted:
          $ref: '#/components/callbacks/CheckCompleted'

  /api/external/checks/{uuid}:
    get:
      tags: [Wallet checks]
      summary: Get a wallet check
      operationId: getCheck
      parameters:
        - $ref: '#/components/parameters/Uuid'
      responses:
        '200':
          description: The check.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/WalletCheck' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/external/checks/list:
    get:
      tags: [Wallet checks]
      summary: List your wallet checks
      operationId: listChecks
      responses:
        '200':
          description: Your recent wallet checks.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/WalletCheck' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/external/deposit-checks:
    post:
      tags: [Deposit checks]
      summary: Create a deposit check
      description: Screen an incoming on-chain deposit (by transaction hash and receiving address).
      operationId: createDepositCheck
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [network, tx_hash, deposit_address]
              properties:
                network: { type: string, enum: [ETH, BSC, TRX, SOL], example: TRX }
                tx_hash: { type: string, example: '0xabc123…' }
                deposit_address: { type: string, example: 'TXYZ…' }
                token_id:
                  type: string
                  nullable: true
                  description: Token contract for token transfers; omit for native.
                amount: { type: string, nullable: true, example: '1000.00' }
      responses:
        '200':
          description: Deposit check created.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/DepositCheck' }
                      already_exists: { type: boolean }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/ServerError' }
      callbacks:
        depositDecision:
          $ref: '#/components/callbacks/DepositDecision'
    get:
      tags: [Deposit checks]
      summary: List your deposit checks
      operationId: listDepositChecks
      responses:
        '200':
          description: Your recent deposit checks.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/DepositCheck' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/external/deposit-checks/{uuid}:
    get:
      tags: [Deposit checks]
      summary: Get a deposit check
      operationId: getDepositCheck
      parameters:
        - $ref: '#/components/parameters/Uuid'
      responses:
        '200':
          description: The deposit check.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/DepositCheck' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/external/balance:
    get:
      tags: [Account]
      summary: Get account balance
      operationId: getBalance
      responses:
        '200':
          description: Balance and pricing.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Balance' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/external/risk-models/current:
    get:
      tags: [Risk model]
      summary: Get the active risk model
      description: The risk model (thresholds and version) currently applied to your checks.
      operationId: getCurrentRiskModel
      responses:
        '200':
          description: The active risk model.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/RiskModel' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/external/risk-models/simulate:
    post:
      tags: [Risk model]
      summary: Simulate scoring with the active risk model
      description: Run the active risk model against a supplied payload to preview the score and decision. Not billed and performs no on-chain lookup.
      operationId: simulateRiskModel
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [checkType]
              properties:
                checkType: { type: string, enum: [wallet, deposit] }
                payload:
                  type: object
                  description: Inputs to score (e.g. provider risk score, risk signals, exposure).
                  properties:
                    riskScore: { type: number }
                    risks: { type: array, items: { type: object } }
      responses:
        '200':
          description: Simulated result.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/SimulationResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/external/usdt-transfers:
    post:
      tags: [Utilities]
      summary: List USDT transfers for an address
      description: Recent USDT (stablecoin) transfers for an address, with sent / received / net totals.
      operationId: getUsdtTransfers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [address, network]
              properties:
                address: { type: string }
                network: { type: string, enum: [eth, bsc, trx, sol], example: trx }
                limit: { type: integer, default: 50, example: 50 }
      responses:
        '200':
          description: USDT transfers and totals.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/UsdtTransfers' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/ServerError' }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

  parameters:
    Uuid:
      name: uuid
      in: path
      required: true
      schema: { type: string }
      description: The check's unique id.
    WebhookSignature:
      name: X-Chainy-Signature
      in: header
      required: true
      schema: { type: string }
      example: 't=1769472000,v1=8f1c1c0a…'
      description: '`t=<unix-seconds>,v1=<hex>`. v1 is HMAC-SHA256(secret, "<t>.<raw body>").'
    WebhookEventId:
      name: X-Chainy-Event-Id
      in: header
      required: true
      schema: { type: string }
      description: Stable per event and repeated on retries — deduplicate on this.
    WebhookEventType:
      name: X-Chainy-Event-Type
      in: header
      required: true
      schema: { type: string, enum: [check.completed, deposit.decision] }
    WebhookDeliveryAttempt:
      name: X-Chainy-Delivery-Attempt
      in: header
      required: true
      schema: { type: integer, minimum: 1 }
      description: 1 on the first attempt, incremented on each retry.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string }
      description: Provide a unique key to make a create request safely retryable.

  responses:
    BadRequest:
      description: Invalid input.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unauthorized:
      description: Missing or invalid API key.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    PaymentRequired:
      description: Insufficient balance.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NotFound:
      description: Not found.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Conflict:
      description: A request with this Idempotency-Key is already in progress.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    RateLimited:
      description: Too many requests.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    ServerError:
      description: Internal server error.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }

  callbacks:
    # Out-of-band subscriptions: the URL is registered in the dashboard, not passed in
    # the request, so the key below is the customer's own endpoint rather than a runtime
    # expression. See the Webhooks tag for signing and retry semantics.
    CheckCompleted:
      'https://your-server.example.com/chainy-webhook':
        post:
          summary: check.completed
          description: Sent once a wallet check reaches a final state. Not sent for failed or refunded checks.
          operationId: onCheckCompleted
          parameters:
            - $ref: '#/components/parameters/WebhookSignature'
            - $ref: '#/components/parameters/WebhookEventId'
            - $ref: '#/components/parameters/WebhookEventType'
            - $ref: '#/components/parameters/WebhookDeliveryAttempt'
          requestBody:
            required: true
            content:
              application/json:
                schema:
                  allOf:
                    - $ref: '#/components/schemas/WebhookEnvelope'
                    - type: object
                      properties:
                        event: { type: string, enum: [check.completed] }
                        data: { $ref: '#/components/schemas/WalletCheck' }
          responses:
            '200':
              description: Acknowledged. Any non-2xx (or no response) is retried.
    DepositDecision:
      'https://your-server.example.com/chainy-webhook':
        post:
          summary: deposit.decision
          description: Sent when a deposit check reaches a terminal decision (ALLOW, HOLD or REJECT).
          operationId: onDepositDecision
          parameters:
            - $ref: '#/components/parameters/WebhookSignature'
            - $ref: '#/components/parameters/WebhookEventId'
            - $ref: '#/components/parameters/WebhookEventType'
            - $ref: '#/components/parameters/WebhookDeliveryAttempt'
          requestBody:
            required: true
            content:
              application/json:
                schema:
                  allOf:
                    - $ref: '#/components/schemas/WebhookEnvelope'
                    - type: object
                      properties:
                        event: { type: string, enum: [deposit.decision] }
                        data: { $ref: '#/components/schemas/DepositCheck' }
          responses:
            '200':
              description: Acknowledged. Any non-2xx (or no response) is retried.

  schemas:
    WebhookEnvelope:
      type: object
      description: Every webhook body has this shape; `data` is the subject in its usual API form.
      required: [event, eventId, data]
      properties:
        event:
          type: string
          description: Event type. Matches the X-Chainy-Event-Type header.
          enum: [check.completed, deposit.decision]
        eventId:
          type: string
          description: |
            Stable identifier for this event, repeated on every retry. Deduplicate on
            this value — delivery is at-least-once.
          example: 'check.completed:1234:COMPLETED'
        data:
          type: object
          description: The subject, in the same shape this API returns it.
    Envelope:
      type: object
      properties:
        status: { type: boolean, example: true }
      required: [status]
    Error:
      type: object
      properties:
        status: { type: boolean, example: false }
        error: { type: string, example: 'address and network are required' }
      required: [status, error]
    Decision:
      type: string
      description: The compliance decision for the subject.
      enum: [ALLOW, HOLD, REJECT, PENDING]
    RiskLevel:
      type: string
      enum: [none, low, medium, high]
    WalletCheck:
      type: object
      description: A wallet screening result.
      properties:
        uuid: { type: string }
        status: { type: string, description: 'Processing status, e.g. NEW, COMPLETED.' }
        network: { type: string, example: eth }
        address: { type: string }
        decision: { $ref: '#/components/schemas/Decision' }
        risk_level: { $ref: '#/components/schemas/RiskLevel' }
        risk_score: { type: number, description: '0–100 risk score.', example: 0 }
        risks:
          type: array
          description: Risk signals contributing to the score.
          items:
            type: object
            properties:
              category: { type: string }
              level: { $ref: '#/components/schemas/RiskLevel' }
        createdAt: { type: string, format: date-time }
    DepositCheck:
      type: object
      description: An incoming-deposit screening result.
      properties:
        uuid: { type: string }
        network: { type: string, example: TRX }
        tx_hash: { type: string }
        deposit_address: { type: string }
        token_id: { type: string, nullable: true }
        kyt_status: { type: string, description: 'Processing status, e.g. NEW, REGISTERED, COMPLETED.' }
        decision: { $ref: '#/components/schemas/Decision' }
        risk_score: { type: number, nullable: true, example: 0 }
        createdAt: { type: string, format: date-time }
    Balance:
      type: object
      properties:
        balance: { type: number, example: 25.00 }
        currency: { type: string, example: USD }
        pricePerCheck: { type: number, example: 1.00 }
        availableChecks: { type: integer, example: 25 }
    RiskModel:
      type: object
      properties:
        id: { type: string }
        version: { type: integer }
        name: { type: string }
        thresholds:
          type: object
          properties:
            holdMin: { type: number, example: 50 }
            rejectMin: { type: number, example: 75 }
    SimulationResult:
      type: object
      description: Result of running the active risk model against a supplied payload.
      properties:
        model:
          type: object
          properties:
            code: { type: string }
            name: { type: string }
            version: { type: integer }
        riskEngine:
          type: object
          properties:
            score: { type: number, example: 0 }
            decision: { $ref: '#/components/schemas/Decision' }
    UsdtTransfer:
      type: object
      properties:
        from: { type: string }
        to: { type: string }
        valueUsdt: { type: number }
        hash: { type: string }
    UsdtTransfers:
      type: object
      properties:
        address: { type: string }
        network: { type: string }
        contractAddress: { type: string, description: 'USDT token contract on the network.' }
        transfers:
          type: array
          items: { $ref: '#/components/schemas/UsdtTransfer' }
        stats:
          type: object
          properties:
            total: { type: integer }
            totalSent: { type: string, example: '0.00' }
            totalReceived: { type: string, example: '0.00' }
            netBalance: { type: string, example: '0.00' }
