> ## 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.

# Exchange HMAC-signed credentials for a Bearer access token.

> Computes server-side: HMAC-SHA256 of `{client_id}:{timestamp}`
keyed by the credential's `client_secret`, compared in constant
time against the supplied `signature`. On match, mints an
RS256-signed JWT scoped to the credential's `allowed_scopes`.

The `timestamp` must be within ±5 minutes of server time;
outside that window the request is rejected to limit replay.




## OpenAPI

````yaml https://core.singapay.id/identity-verification/docs/swagger.json post /api/v1/kyc/auth/get-auth-token
openapi: 3.0.3
info:
  title: Singapay Identity Verification API
  description: |
    The Identity Verification API exposes Singapay's e-wallet account-name
    verification feature 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

    `POST /api/v1/kyc/ewallet/verify` is 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.

    ## 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.
  version: 1.0.0
  contact:
    name: Singapay Integration Support
    email: integration@singapay.id
  license:
    name: Proprietary
servers:
  - url: https://sandbox.singapay.id
    description: Sandbox — for integration testing.
  - url: https://api.singapay.id
    description: Production.
security:
  - bearerAuth: []
tags:
  - name: Authentication
    description: |
      Exchange long-lived credentials for short-lived access tokens.
      Anonymous endpoints (no Bearer required).
  - name: E-Wallet
    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.
paths:
  /api/v1/kyc/auth/get-auth-token:
    post:
      tags:
        - Authentication
      summary: Exchange HMAC-signed credentials for a Bearer access token.
      description: |
        Computes server-side: HMAC-SHA256 of `{client_id}:{timestamp}`
        keyed by the credential's `client_secret`, compared in constant
        time against the supplied `signature`. On match, mints an
        RS256-signed JWT scoped to the credential's `allowed_scopes`.

        The `timestamp` must be within ±5 minutes of server time;
        outside that window the request is rejected to limit replay.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GetAuthTokenRequest'
            examples:
              standard:
                summary: Standard auth exchange
                value:
                  client_id: kc_live_a3f2c4
                  timestamp: '2026-05-26T07:30:00Z'
                  signature: >-
                    9d4e7a8b1f3c2e5d6a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f
      responses:
        '200':
          description: Access token issued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessToken'
              examples:
                standard:
                  summary: Standard token response
                  value:
                    access_token: >-
                      eyJhbGciOiJSUzI1NiIsImtpZCI6ImsxIn0.eyJpc3MiOiJraWMtdmVyaWZ5IiwiYXVkIjoia3ljLWFwaSIsImV4cCI6MTcyNDgyMTYwMH0...
                    token_type: Bearer
                    expires_in: 3600
                    audience: kyc-api
        '400':
          description: Malformed body (missing field, invalid timestamp format).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: |
            `INVALID_SIGNATURE` (signature didn't match), `UNKNOWN_CLIENT_ID`,
            or `REVOKED_CREDENTIAL` (credential is no longer ACTIVE).
            Same HTTP code for all three to avoid leaking client_id existence.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security: []
components:
  schemas:
    GetAuthTokenRequest:
      type: object
      required:
        - client_id
        - timestamp
        - signature
      properties:
        client_id:
          type: string
          description: Public credential identifier issued from the merchant dashboard.
          example: kc_live_a3f2c4
        timestamp:
          type: string
          format: date-time
          description: RFC 3339 timestamp; ±5 minutes from server time.
          example: '2026-05-26T07:30:00Z'
        signature:
          type: string
          pattern: ^[0-9a-fA-F]{64}$
          description: |
            Hex-encoded HMAC-SHA256 of `{client_id}:{timestamp}` keyed
            with the credential's `client_secret`.
          example: 9d4e7a8b1f3c2e5d6a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f
    AccessToken:
      type: object
      required:
        - access_token
        - token_type
        - expires_in
        - audience
      properties:
        access_token:
          type: string
          description: RS256-signed JWT; opaque from the merchant's view.
        token_type:
          type: string
          enum:
            - Bearer
        expires_in:
          type: integer
          minimum: 1
          description: Seconds until the token expires (default 3600).
          example: 3600
        audience:
          type: string
          enum:
            - kyc-api
          description: |
            JWT audience claim. The gateway accepts only `kyc-api` for the
            programmatic surface.
    Error:
      type: object
      required:
        - error
        - message
      properties:
        error:
          type: string
          description: |
            Machine-readable error code. See the table in the docs/api
            README for the full enum.
          example: INVALID_PHONE_FORMAT
        message:
          type: string
          description: Human-readable explanation; safe to surface to merchant operators.
          example: phone_number must be 9-13 digits starting with 0 or 62
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      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).

````