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

# Verify the holder name on a bank account.

> Looks up the registered holder name on the named bank for the
given account number and compares it against the submitted
`name`.

Responses use the **standard envelope** (`code`, `data` /
`message`, `request_id`, `pricing`). Only `code=SUCCESS` is
billable (`pricing=PAID`).

### Behaviour by outcome

| Upstream result | Envelope |
|---|---|
| Account exists | `code=SUCCESS`, `data.status="found"`, `pricing=PAID` |
| Account not found | `code=SUCCESS`, `data.status="not found"`, `suggestion=reject`, `pricing=PAID` |
| Upstream unavailable after retry | `code=SERVER_ERROR`, `pricing=FREE` |
| Validation error (bad account / bank / name / request_id) | `code=CLIENT_ERROR`, `pricing=FREE` |
| Duplicate / conflicting `request_id` | `code=DUPLICATE_REFERENCE`, `pricing=FREE` |
| Not entitled / not authenticated | `code=UNAUTHORIZED`, `pricing=FREE` |
| Prepaid balance too low | `code=INSUFFICIENT_BALANCE`, `pricing=FREE` |

### Access

Requires a valid Bearer token. Access is gated by feature
entitlement (`BANK_CHECK_INQUIRY`); BOSS enables the feature per
merchant. When the feature is off the call returns
`UNAUTHORIZED`.

### Account number format

Accept digits with optional separators (` `, `-`, `.`, `_`).
The service strips separators and requires 6–20 digits remaining.

### Bank code

Must be an enabled entry in the supported-banks catalogue
(≈104 Indonesian clearing codes). Numeric codes are zero-padded
to 3 digits (`2` → `002`). Fetch the live list from the merchant
dashboard (Supported banks).

### Idempotency

Two requests with the same `(merchant_id, request_id)` return
the same `SUCCESS` response and are billed once **when the prior
attempt completed successfully**. A conflicting payload with the
same `request_id` returns `DUPLICATE_REFERENCE`. A prior upstream
failure is re-run on retry rather than replayed.




## OpenAPI

````yaml https://core.singapay.id/identity-verification/docs/swagger.json post /api/v1/kyc/bank/verify
openapi: 3.0.3
info:
  contact:
    email: integration@singapay.id
    name: Singapay Integration Support
  description: |
    The Identity Verification API exposes Singapay's e-wallet and bank
    account-name verification features to integrating merchants.

    ## Authentication

    Programmatic clients use a two-step flow:

    1. **Exchange credentials for a short-lived JWT** via
       `POST /api/v1/kyc/auth/get-auth-token`. The request carries a
       HMAC-SHA256 signature of `{client_id}:{timestamp}` keyed with the
       merchant's `client_secret`. The response is an access token with
       `Bearer` token type.
    2. **Call business endpoints with the JWT** in an
       `Authorization: Bearer <token>` header. JWT default lifetime
       is one hour.

    Credentials (`client_id` / `client_secret`) are issued from the
    merchant KYC dashboard. `client_secret` is shown ONCE at creation —
    if lost, regenerate from the dashboard.

    ### Signature computation

    ```
    timestamp     = now in RFC 3339 with second precision (UTC)
    string_to_sign = "{client_id}:{timestamp}"
    signature     = hex(HMAC-SHA256(client_secret, string_to_sign))
    ```

    During sandbox integration the helper
    `POST /api/v1/kyc/auth/sample-gen-sign` (deliberately omitted from
    this spec; see the docs/api/README) can compute a signature for you
    given the secret — but never call it in production: it requires you
    to send your secret over the wire, which defeats the purpose of HMAC.

    ## Idempotency

    Both `POST /api/v1/kyc/ewallet/verify` and
    `POST /api/v1/kyc/bank/verify` are idempotent on the merchant-
    supplied `request_id`. Two requests with the same `(merchant_id,
    request_id)` pair return the same response and are billed once
    (for bank: only when the prior attempt completed successfully;
    a prior upstream `FAILED` attempt is re-run on retry).

    ## Rate limits

    Standard tier: 60 requests per second per merchant. Bursts beyond
    the bucket return `429 Too Many Requests` with `Retry-After` and
    `X-RateLimit-*` headers. Contact support to upgrade the tier.

    ## IP allowlisting

    A credential may carry an IP allowlist (CIDR or bare IP). When the
    list is non-empty, requests from other IPs are rejected with
    `403 IP_NOT_ALLOWED`. Empty list = no IP restriction.
  license:
    name: Proprietary
  title: Singapay Identity Verification API
  version: 1.0.0
servers:
  - description: Sandbox — for integration testing.
    url: https://sandbox.singapay.id
  - description: Production.
    url: https://api.singapay.id
security:
  - bearerAuth: []
tags:
  - description: |
      Exchange long-lived credentials for short-lived access tokens.
      Anonymous endpoints (no Bearer required).
    name: Authentication
  - description: |
      Verify the registered holder name on an e-wallet account against
      a name the merchant submits. Requires a valid Bearer token with
      the `kyc.ewallet.verify` scope. Responses use the standard
      envelope (`code`, `data`/`message`, `request_id`, `pricing`).
    name: E-Wallet
  - description: |
      Verify the registered holder name on a bank account against a
      name the merchant submits. Requires a valid Bearer token; access
      is gated by feature entitlement (`BANK_CHECK_INQUIRY`). Responses
      use the standard envelope (`code`, `data`/`message`, `request_id`,
      `pricing`).
    name: Bank
paths:
  /api/v1/kyc/bank/verify:
    post:
      tags:
        - Bank
      summary: Verify the holder name on a bank account.
      description: >
        Looks up the registered holder name on the named bank for the

        given account number and compares it against the submitted

        `name`.


        Responses use the **standard envelope** (`code`, `data` /

        `message`, `request_id`, `pricing`). Only `code=SUCCESS` is

        billable (`pricing=PAID`).


        ### Behaviour by outcome


        | Upstream result | Envelope |

        |---|---|

        | Account exists | `code=SUCCESS`, `data.status="found"`, `pricing=PAID`
        |

        | Account not found | `code=SUCCESS`, `data.status="not found"`,
        `suggestion=reject`, `pricing=PAID` |

        | Upstream unavailable after retry | `code=SERVER_ERROR`, `pricing=FREE`
        |

        | Validation error (bad account / bank / name / request_id) |
        `code=CLIENT_ERROR`, `pricing=FREE` |

        | Duplicate / conflicting `request_id` | `code=DUPLICATE_REFERENCE`,
        `pricing=FREE` |

        | Not entitled / not authenticated | `code=UNAUTHORIZED`, `pricing=FREE`
        |

        | Prepaid balance too low | `code=INSUFFICIENT_BALANCE`, `pricing=FREE`
        |


        ### Access


        Requires a valid Bearer token. Access is gated by feature

        entitlement (`BANK_CHECK_INQUIRY`); BOSS enables the feature per

        merchant. When the feature is off the call returns

        `UNAUTHORIZED`.


        ### Account number format


        Accept digits with optional separators (` `, `-`, `.`, `_`).

        The service strips separators and requires 6–20 digits remaining.


        ### Bank code


        Must be an enabled entry in the supported-banks catalogue

        (≈104 Indonesian clearing codes). Numeric codes are zero-padded

        to 3 digits (`2` → `002`). Fetch the live list from the merchant

        dashboard (Supported banks).


        ### Idempotency


        Two requests with the same `(merchant_id, request_id)` return

        the same `SUCCESS` response and are billed once **when the prior

        attempt completed successfully**. A conflicting payload with the

        same `request_id` returns `DUPLICATE_REFERENCE`. A prior upstream

        failure is re-run on retry rather than replayed.
      requestBody:
        content:
          application/json:
            examples:
              bca_partial:
                summary: BCA lookup, name partially matches
                value:
                  account_number: 1234-5678-9012
                  bank_code: '014'
                  name: Siti R.
                  request_id: f1c2a3b4-5d6e-7f80-9012-345678901234
              bri_match:
                summary: Successful BRI lookup with a strong name match
                value:
                  account_number: '1234567890'
                  bank_code: '002'
                  name: Budi Santoso
                  request_id: TXN-20260727-001
            schema:
              $ref: '#/components/schemas/BankVerifyRequest'
        required: true
      responses:
        '200':
          content:
            application/json:
              examples:
                found:
                  summary: Account exists with a matching name → pass
                  value:
                    code: SUCCESS
                    data:
                      similarity: 100
                      status: found
                      suggestion: pass
                    message: OK
                    pricing: PAID
                    request_id: TXN-20260727-001
                found_review:
                  summary: Account exists with a partial name match → review
                  value:
                    code: SUCCESS
                    data:
                      similarity: 55
                      status: found
                      suggestion: review
                    message: OK
                    pricing: PAID
                    request_id: a7e3c1d0-2b4f-4a6c-8e90-1f2a3b4c5d6e
                not_found:
                  summary: No account at the bank → reject (still PAID)
                  value:
                    code: SUCCESS
                    data:
                      similarity: 0
                      status: not found
                      suggestion: reject
                    message: OK
                    pricing: PAID
                    request_id: f1c2a3b4-5d6e-7f80-9012-345678901234
              schema:
                $ref: '#/components/schemas/BankVerifyResponse'
          description: |
            Verification complete (whether the account exists or not).
            `code` is always `SUCCESS`; inspect `data.status` /
            `data.suggestion` for the outcome. `pricing` is `PAID`.
        '400':
          content:
            application/json:
              example:
                code: CLIENT_ERROR
                message: Error message
                pricing: FREE
                request_id: 8b1d6f3e-9a02-4c5d-9f7a-2c8e1b3d4f5a
              schema:
                $ref: '#/components/schemas/FailureResponse'
          description: |
            `CLIENT_ERROR` — validation failure (empty `request_id`,
            empty name, invalid account number, unsupported bank code,
            malformed JSON).
        '401':
          content:
            application/json:
              example:
                code: UNAUTHORIZED
                message: You don't have authorization to access.
                pricing: FREE
                request_id: 8b1d6f3e-9a02-4c5d-9f7a-2c8e1b3d4f5a
              schema:
                $ref: '#/components/schemas/FailureResponse'
          description: |
            `UNAUTHORIZED` — missing / expired / malformed Bearer token,
            or the merchant is not entitled to `BANK_CHECK_INQUIRY`, or
            the client IP is not on the credential allowlist.
        '402':
          content:
            application/json:
              example:
                code: INSUFFICIENT_BALANCE
                message: Balance is not enough, please top up
                pricing: FREE
                request_id: 8b1d6f3e-9a02-4c5d-9f7a-2c8e1b3d4f5a
              schema:
                $ref: '#/components/schemas/FailureResponse'
          description: |
            `INSUFFICIENT_BALANCE` — prepaid balance below the call's fee.
            Top up via the dashboard. *(Only when billing is enabled.)*
        '409':
          content:
            application/json:
              example:
                code: DUPLICATE_REFERENCE
                message: Duplicate reference number
                pricing: FREE
                request_id: 8b1d6f3e-9a02-4c5d-9f7a-2c8e1b3d4f5a
              schema:
                $ref: '#/components/schemas/FailureResponse'
          description: |
            `DUPLICATE_REFERENCE` — the same `request_id` was previously
            used with a different payload.
        '429':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FailureResponse'
          description: |
            Rate limit exceeded for this merchant + product. The
            `Retry-After` header indicates when to try again.
          headers:
            Retry-After:
              description: Seconds until the bucket resets.
              schema:
                minimum: 1
                type: integer
            X-RateLimit-Limit:
              schema:
                type: integer
            X-RateLimit-Remaining:
              schema:
                type: integer
            X-RateLimit-Reset:
              schema:
                type: integer
        '500':
          content:
            application/json:
              example:
                code: INTERNAL_ERROR
                message: Error, please try again later
                pricing: FREE
                request_id: 8b1d6f3e-9a02-4c5d-9f7a-2c8e1b3d4f5a
              schema:
                $ref: '#/components/schemas/FailureResponse'
          description: |
            `INTERNAL_ERROR` — unexpected failure inside the service.
            `pricing=FREE`.
        '502':
          content:
            application/json:
              example:
                code: SERVER_ERROR
                message: Error, please try again later
                pricing: FREE
                request_id: 8b1d6f3e-9a02-4c5d-9f7a-2c8e1b3d4f5a
              schema:
                $ref: '#/components/schemas/FailureResponse'
          description: |
            `SERVER_ERROR` — bank inquiry upstream returned a 5xx or a
            transport failure after retry. Not billed. Retrying the same
            `request_id` re-runs verification.
components:
  schemas:
    BankVerifyRequest:
      properties:
        account_number:
          description: |
            Bank account number. Spaces, dashes, dots, and underscores
            are stripped; the digits-only form is stored and sent
            upstream. After normalisation the value must be 6–20 digits.
          example: '1234567890'
          type: string
        bank_code:
          description: |
            Indonesian bank clearing code from the supported-banks
            catalogue (≈104 entries). Numeric codes are zero-padded to
            3 digits (`2` → `002`, `14` → `014`). Unknown or disabled
            codes return `CLIENT_ERROR`.
          example: '014'
          type: string
        name:
          description: |
            Name to verify against the registered account holder name.
            Trimmed before validation.
          example: Budi Santoso
          maxLength: 200
          minLength: 1
          type: string
        request_id:
          description: |
            Merchant-supplied idempotency / reference key. Recommended:
            UUID v4. Two requests with the same key return the same
            answer and are billed once.
          example: TXN-20260727-001
          type: string
      required:
        - request_id
        - account_number
        - name
        - bank_code
      type: object
    BankVerifyResponse:
      properties:
        code:
          description: Business status code. Always `SUCCESS` on this shape.
          enum:
            - SUCCESS
          example: SUCCESS
          type: string
        data:
          $ref: '#/components/schemas/BankVerifyData'
        message:
          description: |
            Always `OK`. Present so every response carries a `message`,
            whether it succeeded or not; branch on `code`, not on this.
          enum:
            - OK
          example: OK
          type: string
        pricing:
          description: |
            Billing outcome for this call. `PAID` when verification ran
            (including `data.status = "not found"`).
          enum:
            - PAID
          example: PAID
          type: string
        request_id:
          description: Echo of the request's `request_id`.
          example: TXN-20260727-001
          type: string
      required:
        - code
        - message
        - data
        - request_id
        - pricing
      type: object
    FailureResponse:
      description: |
        Standard failure envelope for verify endpoints (e-wallet and bank).
      properties:
        code:
          description: |
            Standard business error code. See the response-code table in
            docs/api/README.
          enum:
            - DUPLICATE_REFERENCE
            - SERVER_ERROR
            - INTERNAL_ERROR
            - CLIENT_ERROR
            - UNAUTHORIZED
            - INSUFFICIENT_BALANCE
          example: CLIENT_ERROR
          type: string
        message:
          description: Human-readable explanation; safe to surface to operators.
          example: Error message
          type: string
        pricing:
          description: Failures are never billed.
          enum:
            - FREE
          example: FREE
          type: string
        request_id:
          description: |
            Echo of the request's `request_id` when it was parseable;
            may be empty if the body could not be read.
          example: 8b1d6f3e-9a02-4c5d-9f7a-2c8e1b3d4f5a
          type: string
      required:
        - code
        - message
        - request_id
        - pricing
      type: object
    BankVerifyData:
      properties:
        similarity:
          description: |
            Name-match score (0.0–100.0). This score describes how similar
            the name inputted and the account holder name on the bank
            account.
          format: float
          maximum: 100
          minimum: 0
          type: number
        status:
          description: |
            Account state at the named bank:
            - `found` — account exists; holder name was returned.
            - `not found` — no account for this number at this bank.
          enum:
            - found
            - not found
          type: string
        suggestion:
          description: |
            Recommended action, from `similarity` vs the configured pass
            threshold.
          enum:
            - pass
            - review
            - reject
          type: string
      required:
        - status
        - similarity
        - suggestion
      type: object
  securitySchemes:
    bearerAuth:
      bearerFormat: JWT
      description: |
        RS256-signed JWT obtained from `POST /api/v1/kyc/auth/get-auth-token`.
        Default lifetime one hour. JWKS is published at
        `/.well-known/jwks.json` on the service side (used by the gateway,
        not by merchants).
      scheme: bearer
      type: http

````