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

# Open an account

> Opens an account for your business, or — when `customerId` is supplied — a wallet account for that customer. The account opens `active` with a zero balance and a freshly minted transfer tag. It cannot yet send or receive externally: issue a detail first.

## What you may open

Three checks run in order, and each has its own failure:

1. **The currency must be open for issuance.** A currency Sakawave has switched off answers `409 currency_capability_unavailable`.
2. **A crypto account needs tier 2.** Below it, the denial answers `403` with `reason`, `currentTier`, and `requiredTier` — and no `code`.
3. **A customer wallet must be NGN.** Any other currency answers `403 international_customer_wallet_not_supported`, even when your own business is approved for it. Your approval does not extend to your customers.

Your own fiat account is not checked against your tier, your verification, or your international access. It opens in any supported currency, at any tier. Verification and international access are checked when you [issue its details](/api-reference/accounts/issue-account-detail); the tier is checked when money moves, and a non-NGN account sends money or takes deposits through its details only at tier 3.

Because customers are always tier 1, **a customer can never hold a crypto wallet**.

<Note>Opening an account emits an `account.created` webhook carrying this response body.</Note>

## Error codes

| Status | `code` | Cause |
| - | - | - |
| `400` | `validation_failed` | A field failed validation — including a `type` outside `operational` and `savings`. |
| `403` | `international_customer_wallet_not_supported` | A customer wallet was requested in a currency other than NGN. |
| `403` | `— (`reason: feature\_not\_available`)` | A crypto account was requested below tier 2. |
| `404` | `customer_not_found` | The `customerId` is not one of your customers. |
| `404` | `client_unavailable` | The key authenticated, but its business no longer exists. |
| `409` | `currency_capability_unavailable` | This currency is not currently open for account issuance. |
| `401` | `INVALID_API_KEY` | The key is missing, invalid, inactive, expired, from the other environment, or blocked by its IP allowlist. |
| `429` | `api_key_rate_limit_exceeded` | The per-minute limit for this key was exceeded. |


## OpenAPI

````yaml /openapi.json post /v1/accounts
openapi: 3.1.0
info:
  title: Sakawave Business API
  version: 1.0.0-draft
  summary: >-
    Accounts, transfers, FX quotes, and transaction reconciliation for Sakawave
    businesses.
  description: >-
    Generated from the public NestJS controllers and their Zod request schemas.
    Public URLs and policy statements remain draft until the Phase 1 contract
    checklist is approved.
  contact:
    name: Sakawave API Support
    email: api-support@sakawave.example
servers:
  - url: https://sandbox-api.sakawave.example
    description: Sandbox (default)
  - url: https://api.sakawave.example
    description: Live
security:
  - bearerAuth: []
tags:
  - name: Bill payments
    description: Browse the biller catalog, validate a customer, and pay a bill.
  - name: Cards
    description: Issue, fund, and control USD virtual and disposable cards.
  - name: eSIM
    description: >-
      Sell mobile data abroad: browse the catalog, buy an eSIM, and manage it
      once installed.
  - name: Currencies and countries
    description: >-
      Reference data: what may be held, what may be sent where, and which
      jurisdictions are served.
  - name: Customers
    description: Create and manage customers of your business.
  - name: Accounts
    description: >-
      Open business and customer accounts, and issue the destinations that fund
      them.
  - name: Transfer destinations
    description: Resolve temporary destinations and saved recipients.
  - name: FX quotes
    description: Create and retrieve time-limited cross-currency quotes.
  - name: Transfers
    description: Preview, create, and monitor transfers.
  - name: Transactions
    description: Read ledger entries for reconciliation.
paths:
  /v1/accounts:
    post:
      tags:
        - Accounts
      summary: Open an account
      description: >-
        Opens an account for your business, or — when `customerId` is supplied —
        a wallet account for that customer. The account opens `active` with a
        zero balance and a freshly minted transfer tag. It cannot yet send or
        receive externally: issue a detail first.
      operationId: openAccount
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OpenAccountRequest'
            example:
              name: Ada NGN wallet
              type: operational
              currency: NGN
              customerId: 01K2Z3AB4C5D6E7F8G9HJKMNPQ
              metadata:
                cohort: sandbox-tutorial
      responses:
        '201':
          description: Successful response.
          headers:
            X-Request-ID:
              description: >-
                The request identifier assigned by Sakawave. Include it in
                support requests.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountResponse'
              example:
                id: 01K2Z3AB4D5E6F7G8H9JKMNPQR
                name: Ada NGN wallet
                type: operational
                category: fiat
                status: active
                currency: NGN
                balance: '2500000'
                balanceFormatted: '25000.00'
                tag: AA0271849366
                canDebit: true
                canCredit: true
                isWalletAccount: true
                transferReady: true
                customerId: 01K2Z3AB4C5D6E7F8G9HJKMNPQ
                metadata:
                  cohort: sandbox-tutorial
                closedAt: null
                closureReason: null
                createdAt: '2026-08-14T09:02:00.000Z'
                updatedAt: '2026-08-14T09:02:00.000Z'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/AuthenticationError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '409':
          $ref: '#/components/responses/ConflictError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - bearerAuth: []
components:
  schemas:
    OpenAccountRequest:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
          description: Your own label. Trimmed on the way in.
        type:
          type: string
          enum:
            - operational
            - savings
          description: >-
            Both behave identically — transactable, no interest, no lock.
            `savings` is a bucket you choose, not different treatment.
        currency:
          $ref: '#/components/schemas/AssetCode'
        customerId:
          $ref: '#/components/schemas/Ulid'
          description: >-
            Supply it and the account becomes that customer’s wallet. Omit it
            and the account belongs to your business.
        metadata:
          type: object
          additionalProperties: true
          description: >-
            Free-form JSON stored and returned verbatim. Never send anything
            here that you would not want in a support conversation.
      required:
        - name
        - type
        - currency
    AccountResponse:
      type: object
      additionalProperties: false
      properties:
        id:
          $ref: '#/components/schemas/Ulid'
        name:
          type: string
          description: Your own label for the account. Not shown to anyone paying into it.
        type:
          type: string
          enum:
            - operational
            - savings
            - settlement
            - collection
            - fee
            - revenue
            - suspense
          description: >-
            What the account is for. `operational` and `savings` behave
            identically; the rest are platform-managed.
        category:
          type: string
          enum:
            - fiat
            - crypto
          description: Follows from the currency; it is never chosen.
        status:
          type: string
          enum:
            - active
            - inactive
            - frozen
            - closed
          description: >-
            Only `active` can move money. `frozen` is a compliance or provider
            hold. `closed` is terminal.
        currency:
          $ref: '#/components/schemas/AssetCode'
        balance:
          $ref: '#/components/schemas/MinorUnits'
        balanceFormatted:
          type: string
          description: The same figure rendered for display. Never use it in a calculation.
        tag:
          type: string
          pattern: ^[A-Z]{2}\d{10}$
          description: >-
            The account’s public transfer handle: two letters and ten digits,
            the last a check digit. Platform-minted and unique across Sakawave,
            so another business can pay this account by tag.
        canDebit:
          type: boolean
          description: >-
            Whether money may leave. Held separately from status: an account can
            be stopped in one direction only.
        canCredit:
          type: boolean
          description: Whether money may arrive.
        isWalletAccount:
          type: boolean
          description: >-
            True when the account belongs to one of your customers rather than
            to your business.
        transferReady:
          type: boolean
          description: >-
            True once the account is bound to a payment provider. A freshly
            opened account is false until its first detail is issued, and cannot
            yet send or receive externally.
        customerId:
          anyOf:
            - $ref: '#/components/schemas/Ulid'
              description: The owning customer, when this is a wallet account.
            - type: 'null'
        metadata:
          anyOf:
            - type: object
              additionalProperties: true
              description: >-
                Whatever you stored at creation, returned unchanged. Sakawave
                never reads it.
            - type: 'null'
        closedAt:
          anyOf:
            - $ref: '#/components/schemas/IsoTimestamp'
            - type: 'null'
        closureReason:
          anyOf:
            - type: string
              enum:
                - client_request
                - no_longer_needed
                - duplicate
                - compliance
            - type: 'null'
        createdAt:
          $ref: '#/components/schemas/IsoTimestamp'
        updatedAt:
          $ref: '#/components/schemas/IsoTimestamp'
      required:
        - id
        - name
        - type
        - category
        - status
        - currency
        - balance
        - balanceFormatted
        - tag
        - canDebit
        - canCredit
        - isWalletAccount
        - transferReady
        - customerId
        - metadata
        - closedAt
        - closureReason
        - createdAt
        - updatedAt
    AssetCode:
      type: string
      enum:
        - NGN
        - USD
        - GBP
        - EUR
        - GHS
        - ZAR
        - UGX
        - CAD
        - MXN
        - AED
        - BTC
        - ETH
        - USDT
        - USDC
    Ulid:
      type: string
      minLength: 26
      maxLength: 26
      pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
      example: 01K2Z3AB4C5D6E7F8G9HJKMNPQ
      description: A 26-character ULID.
    MinorUnits:
      type: string
      pattern: ^-?\d+$
      example: '2500000'
      description: >-
        An integer number of the asset’s smallest unit. Use this value for
        arithmetic.
    IsoTimestamp:
      type: string
      format: date-time
      example: '2026-08-14T09:00:00.000Z'
    RequestError:
      anyOf:
        - $ref: '#/components/schemas/ValidationError'
        - $ref: '#/components/schemas/Error'
      description: >-
        Either a field-level validation failure carrying an errors array, or a
        rule the request broke, identified by code.
    Error:
      type: object
      additionalProperties: true
      properties:
        statusCode:
          type: integer
        message:
          type: string
          description: >-
            A sentence suitable for a log or a support conversation, not for a
            user interface.
        code:
          type: string
          description: >-
            The stable machine-readable reason. Branch on this, never on the
            message.
      required:
        - message
        - code
      description: >-
        An error body. Individual errors may add context fields alongside these
        three.
    ForbiddenError:
      type: object
      additionalProperties: false
      properties:
        statusCode:
          const: 403
        message:
          type: string
        code:
          type: string
          enum:
            - kyc_approval_required
            - international_questionnaire_required
            - international_account_approval_required
            - international_customer_wallet_not_supported
          description: >-
            Present on verification, international access, and customer wallet
            refusals. Absent on tier denials, which carry `reason` instead.
        reason:
          type: string
          enum:
            - currency_not_available
            - feature_not_available
          description: Present on tier denials. Absent on every other refusal.
        currency:
          $ref: '#/components/schemas/AssetCode'
        feature:
          type: string
        currentTier:
          type: integer
          enum:
            - 1
            - 2
            - 3
        requiredTier:
          type: integer
          enum:
            - 1
            - 2
            - 3
      required:
        - statusCode
        - message
    RateLimitError:
      type: object
      additionalProperties: false
      properties:
        statusCode:
          const: 429
        message:
          type: string
        code:
          const: api_key_rate_limit_exceeded
        retryAfterSeconds:
          type: integer
          minimum: 1
      required:
        - statusCode
        - message
        - code
        - retryAfterSeconds
    ValidationError:
      type: object
      additionalProperties: false
      properties:
        statusCode:
          const: 400
        message:
          type: string
          description: The single failure, or a count when more than one field failed.
        code:
          const: validation_failed
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ValidationFailure'
      required:
        - statusCode
        - message
        - code
        - errors
    ValidationFailure:
      type: object
      additionalProperties: false
      properties:
        message:
          type: string
        field:
          type: string
        code:
          type: string
        path:
          type: array
          items:
            anyOf:
              - type: string
              - type: integer
      required:
        - message
        - field
        - code
        - path
  responses:
    ValidationError:
      description: >-
        The request was rejected before it was acted on. Field failures list
        every failing field.
      headers:
        X-Request-ID:
          description: >-
            The request identifier assigned by Sakawave. Include it in support
            requests.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RequestError'
          example:
            statusCode: 400
            message: Must be a valid identifier.
            code: validation_failed
            errors:
              - message: Must be a valid identifier.
                field: accountId
                code: custom
                path:
                  - accountId
    AuthenticationError:
      description: >-
        The API key is missing, invalid, inactive, expired, for another
        environment, or blocked by its IP allowlist.
      headers:
        X-Request-ID:
          description: >-
            The request identifier assigned by Sakawave. Include it in support
            requests.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            statusCode: 401
            code: INVALID_API_KEY
            message: Invalid or inactive API credentials.
    ForbiddenError:
      description: >-
        Authenticated, but not permitted to hold or use this currency.
        Entitlement refusals carry `code`; tier denials carry `reason`,
        `currentTier`, and `requiredTier` instead.
      headers:
        X-Request-ID:
          description: >-
            The request identifier assigned by Sakawave. Include it in support
            requests.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ForbiddenError'
          example:
            statusCode: 403
            message: >-
              International wallets for your customers are not available yet.
              Your own approval cannot be used for a customer wallet.
            code: international_customer_wallet_not_supported
            currency: USD
    NotFoundError:
      description: The requested resource was not found for this business.
      headers:
        X-Request-ID:
          description: >-
            The request identifier assigned by Sakawave. Include it in support
            requests.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            statusCode: 404
            code: resource_not_found
            message: Resource not found.
    ConflictError:
      description: The request conflicts with the current resource state.
      headers:
        X-Request-ID:
          description: >-
            The request identifier assigned by Sakawave. Include it in support
            requests.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            statusCode: 409
            code: resource_conflict
            message: The request conflicts with the current resource state.
    RateLimitError:
      description: The API key rate limit was exceeded.
      headers:
        X-Request-ID:
          description: >-
            The request identifier assigned by Sakawave. Include it in support
            requests.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitError'
          example:
            statusCode: 429
            message: Too many API requests. Try again later.
            code: api_key_rate_limit_exceeded
            retryAfterSeconds: 17
    ServerError:
      description: An unexpected server error occurred.
      headers:
        X-Request-ID:
          description: >-
            The request identifier assigned by Sakawave. Include it in support
            requests.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            statusCode: 500
            code: internal_server_error
            message: An unexpected error occurred.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Sakawave API key
      description: >-
        A secret API key, sent as `Authorization: Bearer sk_test_…` in sandbox
        or `Bearer sk_live_…` in live.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.