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

# Create a wallet credential

> Create a fiat or crypto funding credential on a wallet identified
by the `wal_…` id in the path.

## Same-currency credentials (default)

By default the credential's currency matches the wallet's currency — deposits
land directly on the wallet balance. `network` must be a network supported by
the wallet's currency (e.g. `base`, `ethereum`, `solana` for crypto). For fiat
credentials only `bank` is supported today — even if the wallet's currency
advertises other fiat networks (e.g. `mobile_money`), a non-`bank` network is
rejected with `invalid_input`. `type` is optional — when omitted it is derived
from the wallet's currency; when supplied it must match the wallet's currency
type. `merchantReference` is optional and is generated when not provided.

## Cross-currency credentials (auto-onramp / auto-offramp)

Passing an explicit `currency` different from the wallet's currency creates a
cross-currency credential. Every deposit is automatically converted into the
wallet's currency at the FX rate live at deposit time:

- **Auto-onramp** — a fiat currency on a crypto wallet (e.g. `NGN` on a `USDC`
  wallet). The payer sends fiat; the wallet is credited in crypto.
- **Auto-offramp** — a crypto currency on a fiat wallet (e.g. `USDC` on an
  `NGN` wallet). The payer sends crypto; the wallet is credited in fiat.

Only crypto↔fiat pairs are supported — a crypto credential on a crypto wallet
(or fiat on fiat) is rejected. Both currencies in the pair must have matching capabilities: `canOnramp` for auto-onramp, `canOfframp` for
auto-offramp. `network` is validated against the **credential's own** currency,
not the wallet's — so an `NGN` credential on a `USDC` wallet uses `bank`, and a
`USDC` credential on an `NGN` wallet uses `base` / `ethereum` / `solana`.

## Disambiguating multi-country currencies (`country_code`)

Some fiat currency codes exist for more than one country (e.g. `XOF` for CFA-franc
countries). Pass an ISO 3166-1 alpha-2 `country_code` to pin which country's
currency the credential should hold:

- **Cross-currency** — when `currency` resolves to multiple country rows and no
  `country_code` is supplied, the request is rejected with `invalid_input` and
  the response message lists the available countries. When `country_code` is
  supplied, the credential currency is pinned to that country (falling back to
  the null-country row for crypto codes).
- **Same-currency** — the credential inherits the wallet's currency and its
  country, so no lookup happens. If `country_code` is supplied it must equal the
  parent wallet's country or the request is rejected with `invalid_input`.




## OpenAPI

````yaml /api-reference/openapi.json post /v1/wallets/{id}/credentials
openapi: 3.0.0
info:
  title: GlobalStack API
  version: 1.0.0
  description: >
    # Introduction


    GlobalStack API for cross-border payments powered by stablecoin rails.


    Getting started: authenticate with a Bearer token, make your first call to

    `GET /v1/currencies`, and explore.


    **Key operations:**


    - **Onramp** — convert external fiat into crypto credited to a wallet.

    - **Offramp** — convert crypto from a wallet into fiat delivered to a
    beneficiary.

    - **Conversion** — move value between two wallets belonging to the same
    customer.

    - **Transfer** — send crypto from a wallet to an external address.

    - **Deposit** — receive external crypto into a wallet via a deposit address.


    See the sections below for authentication, response shape, idempotency,

    references, metadata, and webhooks.


    ## Authentication


    All API requests require a Bearer token in the `Authorization` header:


    ```

    Authorization: Bearer gsa_sk_live_your_api_key

    ```


    API keys use the `gsa_sk_live_` prefix.


    API keys are scoped to a single merchant account. All resources created with

    a key belong to that merchant. Keep your keys secret — do not expose them in

    client-side code or public repositories.


    To rotate a key, generate a new one from your dashboard and update your

    integration before revoking the old key.


    ## API Response Shape


    Every response — success or failure — follows the same envelope:


    ```json

    {
      "status": true,
      "message": "Onramp created successfully",
      "code": "ok",
      "data": { },
      "meta": {
        "request_id": "req_01ABC...",
        "timestamp": "2026-04-22T02:00:00Z",
        "version": "1"
      }
    }

    ```


    **Fields:**


    - `status` (boolean) — whether the HTTP call succeeded. This is not the
      business status of the underlying resource.
    - `message` (string) — human-readable explanation for developers and logs.
      Do not show to end users.
    - `code` (string) — stable machine-readable result. `ok` on success, or an
      error code like `bad_request`.
    - `data` — the resource payload on success (object, array, or scalar).
    `null`
      on failure.
    - `meta` — request-scoped metadata, always present. Includes `request_id`
      (quote this in support tickets), `timestamp`, and `version`. On list
      endpoints, also includes `page`, `per_page`, `total`, `has_more`. On
      validation errors, includes `errors` array and `next_steps`.

    Error responses include `next_steps` in `meta` with an `action` string and a

    `docs_url` link:


    ```json

    {
      "status": false,
      "message": "Your request has validation errors.",
      "code": "bad_request",
      "data": null,
      "meta": {
        "request_id": "req_01ABC...",
        "timestamp": "2026-04-22T02:00:00Z",
        "version": "1",
        "errors": [
          { "field": "amount", "code": "required", "message": "Amount is required." }
        ],
        "next_steps": {
          "action": "Review the request and retry with corrected parameters.",
          "docs_url": "https://docs.globalstack.io/errors#bad_request"
        }
      }
    }

    ```


    ## Idempotency


    Pass an `Idempotency-Key` header on any create endpoint to make the request

    safely retryable:


    ```

    Idempotency-Key: your-unique-key-here

    ```


    **Rules:**


    - Same key + same body = returns the original response (no side effects).

    - Same key + different body = returns `idempotency_conflict` (409).

    - Keys are scoped to your merchant account.

    - Keys expire after 24 hours.


    Use idempotency keys for any operation that creates a resource or moves

    money. This protects against network retries, duplicate webhooks, and

    client-side retry loops.


    ## Rate Limits


    Requests over the rate limit are rejected with HTTP `429` and the error code

    `rate_limited`:


    ```json

    {
      "status": false,
      "message": "Too many requests. Slow down and retry shortly.",
      "code": "rate_limited"
    }

    ```


    **Money-movement writes** (creating transfers, offramps, conversions, or

    wallet operations) are limited **per merchant account** — the budget is

    shared across all of your API keys and dashboard sessions. Reads are not

    limited.


    On a `429`, back off and retry with jitter; for money movement, reuse the

    same `Idempotency-Key` so the retry stays safe.


    ## Reference


    Every money movement operation (onramp, offramp, conversion, transfer)

    accepts an optional `reference` field — a merchant-supplied identifier that

    is unique per merchant account.


    ```json

    {
      "reference": "INV-2026-001",
      "wallet_id": "wal_..."
    }

    ```


    If you omit `reference`, the API generates one automatically (prefixed

    `ref_`).


    References are unique per merchant — attempting to create two operations

    with the same reference returns the existing operation (same behavior as

    idempotency, but permanent and not time-limited).


    Use `reference` to correlate GlobalStack operations with your own system

    records (invoices, orders, payouts).


    ## Metadata


    Most create endpoints accept an optional `metadata` object — a flat set of

    string key/value pairs stored on the resource:


    ```json

    {
      "metadata": {
        "order_id": "ord_12345",
        "customer_email": "alice@example.com"
      }
    }

    ```


    Metadata is returned on every GET response and in webhook payloads. It is

    not used by GlobalStack for processing — it exists for your own

    record-keeping.


    Resources that accept metadata: customers, wallets, senders, beneficiaries,

    wallet credentials, deposit addresses, onramps, offramps, conversions, and

    transfers.


    ## Webhook Signatures


    Every webhook delivery is signed so you can verify it came from GlobalStack
    and

    was not tampered with in transit. Signatures follow the

    [Svix](https://docs.svix.com/receiving/verifying-payloads/how) standard, so
    you

    can verify them with any Svix library — or manually with the construction
    below.


    Each delivery carries three headers:


    ```

    svix-id: msg_2gT8sV...          # unique message id (the body's event_id,
    prefixed msg_)

    svix-timestamp: 1718960400      # unix seconds when the delivery was signed

    svix-signature: v1,g0hM9SsE...  # space-separated list of
    v1,<base64-signature>

    ```


    Your signing secret (prefixed `whsec_`) is returned in plaintext **exactly

    once** — when you create a webhook endpoint or rotate its secret. Store it

    securely; you cannot retrieve it again.


    **Verify with a Svix library (recommended):**


    ```python

    from svix.webhooks import Webhook


    wh = Webhook(signing_secret)                            # the whsec_...
    value

    payload = wh.verify(raw_request_body, request_headers)  # raises on a bad
    signature

    ```


    **Verify manually:**


    1. Build the signed content by joining the id, timestamp, and the **raw**
       request body with dots:

       ```
       signed_content = svix_id + "." + svix_timestamp + "." + raw_body
       ```

    2. Take your signing secret, drop the `whsec_` prefix, and base64-decode the
       remainder — those bytes are your HMAC key.

    3. Compute `base64(HMAC-SHA256(key, signed_content))`.


    4. The `svix-signature` header is a space-separated list of `v1,<signature>`
       entries (an endpoint can have more than one valid secret during a rotation).
       Compare your computed value against each entry's signature — the part after
       `v1,` — using a constant-time comparison, and accept if any matches.

    5. Reject deliveries whose `svix-timestamp` differs from your current time
    by
       more than a few minutes, to guard against replay.

    The body's `event_id` equals the `svix-id` without its `msg_` prefix, so you
    can

    use either to deduplicate redelivered events.


    ## Request IDs


    Every response carries an `X-Request-Id` header (also surfaced in

    `meta.request_id`). Include the value when reporting issues — it scopes

    backend logs to your specific request.


    You may also send your own `X-Request-Id` header. Values matching

    `^[A-Za-z0-9_-]{1,128}$` are honored verbatim; anything else is replaced
    with

    a generated id.


    ## Versioning


    Public endpoints are versioned under `/v1/`. Breaking changes ship under a
    new

    major version; additive changes (new fields, new endpoints) ship under the

    existing version.
  contact:
    name: Paystack Cross-Border
    url: https://globalstack.io
servers:
  - url: https://api.globalstack.io
    description: Production
security:
  - BearerAuth: []
tags:
  - name: Supported Countries
  - name: Supported Currencies
  - name: Customers
  - name: Wallets
  - name: Beneficiaries
  - name: Quotes
  - name: Onramps
  - name: Offramps
  - name: Conversions
  - name: Transfers
  - name: Transactions
  - name: Notification Webhooks
  - name: Me
paths:
  /v1/wallets/{id}/credentials:
    post:
      tags:
        - Wallets
      summary: Create a wallet credential
      description: >
        Create a fiat or crypto funding credential on a wallet identified

        by the `wal_…` id in the path.


        ## Same-currency credentials (default)


        By default the credential's currency matches the wallet's currency —
        deposits

        land directly on the wallet balance. `network` must be a network
        supported by

        the wallet's currency (e.g. `base`, `ethereum`, `solana` for crypto).
        For fiat

        credentials only `bank` is supported today — even if the wallet's
        currency

        advertises other fiat networks (e.g. `mobile_money`), a non-`bank`
        network is

        rejected with `invalid_input`. `type` is optional — when omitted it is
        derived

        from the wallet's currency; when supplied it must match the wallet's
        currency

        type. `merchantReference` is optional and is generated when not
        provided.


        ## Cross-currency credentials (auto-onramp / auto-offramp)


        Passing an explicit `currency` different from the wallet's currency
        creates a

        cross-currency credential. Every deposit is automatically converted into
        the

        wallet's currency at the FX rate live at deposit time:


        - **Auto-onramp** — a fiat currency on a crypto wallet (e.g. `NGN` on a
        `USDC`
          wallet). The payer sends fiat; the wallet is credited in crypto.
        - **Auto-offramp** — a crypto currency on a fiat wallet (e.g. `USDC` on
        an
          `NGN` wallet). The payer sends crypto; the wallet is credited in fiat.

        Only crypto↔fiat pairs are supported — a crypto credential on a crypto
        wallet

        (or fiat on fiat) is rejected. Both currencies in the pair must have
        matching capabilities: `canOnramp` for auto-onramp, `canOfframp` for

        auto-offramp. `network` is validated against the **credential's own**
        currency,

        not the wallet's — so an `NGN` credential on a `USDC` wallet uses
        `bank`, and a

        `USDC` credential on an `NGN` wallet uses `base` / `ethereum` /
        `solana`.


        ## Disambiguating multi-country currencies (`country_code`)


        Some fiat currency codes exist for more than one country (e.g. `XOF` for
        CFA-franc

        countries). Pass an ISO 3166-1 alpha-2 `country_code` to pin which
        country's

        currency the credential should hold:


        - **Cross-currency** — when `currency` resolves to multiple country rows
        and no
          `country_code` is supplied, the request is rejected with `invalid_input` and
          the response message lists the available countries. When `country_code` is
          supplied, the credential currency is pinned to that country (falling back to
          the null-country row for crypto codes).
        - **Same-currency** — the credential inherits the wallet's currency and
        its
          country, so no lookup happens. If `country_code` is supplied it must equal the
          parent wallet's country or the request is rejected with `invalid_input`.
      operationId: CredentialController.create
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
            pattern: ^[A-Za-z0-9_\-]{8,255}$
          description: >-
            Optional client-supplied key. Identical key + identical body within
            24h replays the original response. Identical key + different body
            returns 409 idempotency_conflict. The hash is over raw bytes —
            clients retrying must send the byte-identical body; a re-serialised
            JSON payload will produce a different hash and a 409. Strongly
            recommended for retry-safe clients.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCredentialDto'
        description: CreateCredentialDto
        required: false
      responses:
        '200':
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccessEnvelope'
                  - type: object
                    required:
                      - data
                      - meta
                    properties:
                      message:
                        type: string
                        example: Wallet credential created successfully
                      data:
                        $ref: '#/components/schemas/CredentialResponse'
                      meta:
                        $ref: '#/components/schemas/ApiResponseMetaSchema'
          description: Wallet credential created successfully
        '400':
          description: >-
            Bad request. `bad_request` covers class-validator failures on the
            body (missing/invalid `network`, `type`, `currency`, `country_code`,
            `label`, `metadata`, `merchantReference`). `invalid_input` covers
            domain-level rejections: a malformed `wal_…` path id; a malformed
            `Idempotency-Key` header; an unsupported `network` for the
            credential's own currency; a fiat credential requested on a
            non-`bank` network (only `bank` is supported for fiat credentials
            today); a supplied `type` that does not match the wallet's currency
            on the same-currency path; a cross-currency pair that is not
            crypto↔fiat; a cross-currency pair where either currency does not
            advertise the required `canOnramp` / `canOfframp` capability; a
            `currency` that resolves to multiple countries without a
            `country_code` to disambiguate; a `country_code` that has no
            matching row for the requested `currency`; or a `country_code` that
            does not match the parent wallet on the same-currency path. Branch
            on the `code` field of the response envelope.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiErrorEnvelope'
                  - type: object
                    properties:
                      code:
                        type: string
                        enum:
                          - invalid_input
                          - bad_request
        '401':
          description: Unauthorized — invalid or missing API key
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiErrorEnvelope'
                  - type: object
                    properties:
                      code:
                        type: string
                        enum:
                          - authentication_failed
        '404':
          description: >-
            Not found — the wallet id does not resolve in this integration. An
            unknown `currency` code is returned as `400 invalid_input` (see
            above), not `404`, so credential resolution aligns with the wallet
            endpoint.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiErrorEnvelope'
                  - type: object
                    properties:
                      code:
                        type: string
                        enum:
                          - not_found
        '409':
          description: >-
            Conflict — possible codes: `idempotency_conflict` (Idempotency-Key
            reused with a different request body), `idempotency_in_progress` (a
            request with the same Idempotency-Key is still in flight). Branch on
            the `code` field of the response envelope.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiErrorEnvelope'
                  - type: object
                    properties:
                      code:
                        type: string
                        enum:
                          - idempotency_conflict
                          - idempotency_in_progress
components:
  schemas:
    CreateCredentialDto:
      properties:
        network:
          type: string
          enum:
            - mobile_money
            - bank
            - solana
            - base
            - ethereum
          example: bank
        type:
          type: string
          enum:
            - crypto
            - fiat
        currency:
          pattern: ^[A-Z]{2,10}$
          type: string
          example: NGN
        country_code:
          pattern: ^[A-Z]{2}$
          type: string
          example: NG
        label:
          maxLength: 128
          type: string
        metadata:
          type: object
        merchantReference:
          maxLength: 128
          type: string
      type: object
      required:
        - network
    ApiSuccessEnvelope:
      properties:
        status:
          type: boolean
          enum:
            - true
          example: true
        message:
          type: string
          description: Human-readable summary
          example: Resource fetched successfully
        code:
          type: string
          enum:
            - ok
          example: ok
      type: object
      required:
        - status
        - message
        - code
    CredentialResponse:
      properties:
        id:
          type: string
          description: Credential external id (prefixed wcr_).
          example: wcr_01HXYZABC1234567890ABCDEFG
        type:
          type: string
          description: crypto | fiat
          example: crypto
        currency:
          type: string
          description: Uppercase currency code of the wallet
          example: USDC
        network:
          type: string
          description: >-
            Network the credential pays in (e.g. base, ethereum for crypto; bank
            for fiat). Stable across the credential lifecycle — unlike
            `details`, which is replaced with provider-specific fields once fiat
            provisioning completes.
          example: base
        wallet:
          $ref: '#/components/schemas/CredentialWalletRef'
        label:
          type: string
          nullable: true
          example: Treasury deposit
        metadata:
          type: object
          description: Merchant-supplied opaque metadata. Values are stored as strings.
          example:
            team: ops
          additionalProperties:
            type: string
        details:
          type: object
          description: >-
            Credential detail. For crypto credentials contains the network on
            create; the address is added once FX Engine mints the resource. For
            fiat credentials contains the FX Engine fiat-account details
            (account_name, account_number, bank_name, bank_code, currency) once
            provisioning completes, plus an optional `reference` — a
            rail-specific deposit reference the merchant must echo on the wire
            when funding the account (e.g. ZAR bank rails such as ABSA). Present
            only for currencies whose rail requires it; treat its absence as "no
            reference needed" rather than an error.
          example:
            accountName: Medard Ilunga
            accountNumber: '1234567890'
            bankName: Wema Bank
            bankCode: '035'
            currency: NGN
            reference: GSVRYTZ496CM
        status:
          type: string
          description: pending on create
          example: pending
        created_at:
          type: string
          format: date-time
          example: '2026-05-20T12:00:00.000Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-05-20T12:00:00.000Z'
      type: object
      required:
        - id
        - type
        - currency
        - network
        - wallet
        - metadata
        - details
        - status
        - created_at
        - updated_at
    ApiResponseMetaSchema:
      properties:
        request_id:
          type: string
          description: ULID-prefixed identifier for this request
          example: req_01HXYZ4K5ABCDEFGHJKLMNPQRS
        timestamp:
          type: string
          format: date-time
          example: '2026-05-14T15:43:55.732Z'
        version:
          type: string
          description: Major API version parsed from /vN/
          example: '1'
      type: object
      required:
        - request_id
        - timestamp
        - version
    ApiErrorEnvelope:
      properties:
        status:
          type: boolean
          enum:
            - false
          example: false
        message:
          type: string
          description: Human-readable failure summary
          example: Resource not found
        code:
          type: string
          description: Stable error code for branching logic
          example: not_found
        data:
          type: object
          nullable: true
          example: null
        meta:
          $ref: '#/components/schemas/ApiErrorMetaSchema'
      type: object
      required:
        - status
        - message
        - code
        - data
        - meta
    CredentialWalletRef:
      properties:
        id:
          type: string
          description: Wallet external id (prefixed wal_).
          example: wal_01HXYZABC1234567890ABCDEFG
        currency:
          type: string
          description: Uppercase currency code
          example: USDC
        label:
          type: string
          nullable: true
          example: USDC Wallet
      type: object
      required:
        - id
        - currency
    ApiErrorMetaSchema:
      properties:
        next_steps:
          $ref: '#/components/schemas/NextStepsSchema'
        request_id:
          type: string
          description: ULID-prefixed identifier for this request
          example: req_01HXYZ4K5ABCDEFGHJKLMNPQRS
        timestamp:
          type: string
          format: date-time
          example: '2026-05-14T15:43:55.732Z'
        version:
          type: string
          description: Major API version parsed from /vN/
          example: '1'
      type: object
      required:
        - next_steps
        - request_id
        - timestamp
        - version
    NextStepsSchema:
      properties:
        action:
          type: string
          description: Suggested action for the caller
          example: Verify the resource identifier and retry.
        docs_url:
          type: string
          format: uri
          example: https://docs.globalstack.io/errors#not_found
      type: object
      required:
        - action
        - docs_url
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: API key issued during merchant onboarding.

````