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

# /api/v1/beneficiaries/addBeneficiary

> Save a recipient once for a country corridor, then reuse it by id when sending payouts.

Creates a saved recipient for one **corridor** (`country_code` + `currency_code`). Pass the returned
`beneficiary_id` to [`sendPayoutRequest`](/api-reference/send-payout) instead of repeating the full
recipient details on every payout. See [Beneficiaries](/beneficiaries) for how the flow fits
together.

<Warning>
  The fields above are what you **encrypt**, not what goes on the wire. The body is always
  `{ "data": "<aes-256-gcm ciphertext>" }` — see [Authentication](/authentication).
</Warning>

<Note>
  **A JWT is required**, same as every payout endpoint — beneficiaries are part of the payout
  product. Log in first via [`/api/v1/user/login`](/api-reference/login).
</Note>

<Info>
  Every corridor requires `recipient_details.name`. The remaining fields differ per destination and
  are validated at create time, not at payout time — which is the point of saving a beneficiary. See
  [corridor requirements](/beneficiaries#corridor-requirements).
</Info>

## Idempotency

Reusing an `idempotency_key` — **or posting identical details** — returns the beneficiary that
already exists rather than creating a duplicate. A retry after a network timeout is safe.

**The status code is how you tell the two apart**: `201` means you just created it, `200` means you
are getting the existing one back.

## Example request

<CodeGroup>
  ```js Node theme={null}
  const res = await call(
    '/api/v1/beneficiaries/addBeneficiary',
    {
      country_code: 'NG',
      currency_code: 'NGN',
      payment_method: 'bank_local',
      nickname: 'Payroll — Ada',
      recipient_details: {
        name: 'Ada Okafor',
        account_number: '0123456789',
        branch_code: '058',
      },
    },
    jwt,
  )

  const { beneficiary_id } = res.data
  ```

  ```python Python theme={null}
  status, body = call('/api/v1/beneficiaries/addBeneficiary', {
      'country_code': 'NG',
      'currency_code': 'NGN',
      'payment_method': 'bank_local',
      'nickname': 'Payroll — Ada',
      'recipient_details': {
          'name': 'Ada Okafor',
          'account_number': '0123456789',
          'branch_code': '058',
      },
  }, jwt)

  beneficiary_id = body['data']['beneficiary_id']
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.pontisglobe.com/api/v1/beneficiaries/addBeneficiary" \
    -H "content-type: application/json" \
    -H "x-api-key: YOUR_API_KEY" \
    -H "x-timestamp: 1748023400" \
    -H "x-signature: 2f8a9b…" \
    -H "authorization: Bearer eyJhbGciOi…" \
    -d '{"data":"<encrypted blob>"}'
  ```
</CodeGroup>

## Errors

| Status | Code               | Meaning                                                                       |
| ------ | ------------------ | ----------------------------------------------------------------------------- |
| 400    | `validation_error` | A field failed validation — the message names it                              |
| 400    | `bad_request`      | Bad envelope: missing or skewed `x-timestamp`, or a body we could not decrypt |
| 401    | `unauthorized`     | Missing or invalid API key, signature or JWT                                  |
| 403    | `forbidden`        | Your IP is not allow-listed, or your account does not hold the payout product |
| 429    | `rate_limited`     | Too many requests — back off and retry                                        |

See [Errors](/errors) for the envelope shape shared by every endpoint.


## OpenAPI

````yaml openapi.yaml POST /api/v1/beneficiaries/addBeneficiary
openapi: 3.1.0
info:
  title: PontisGlobe API
  version: 1.0.0
  summary: Accept stablecoin payments and send cross-border payouts.
  description: |
    Two products on one balance.

    **Global Collections** — accept USDT and USDC through a hosted checkout.
    **Global Payouts** — pay out in local currency across 50+ countries.

    Every endpoint is `POST`, authenticated with an API key and an HMAC-SHA256
    signature, and carries an AES-256-GCM encrypted body. Payout and beneficiary
    endpoints additionally require a short-lived JWT obtained from
    `/api/v1/user/login`.

    ## Request bodies are encrypted

    The fields documented on each endpoint are what you **encrypt**, not what
    goes on the wire. Every request body is a single envelope:

    ```json
    { "data": "<base64 AES-256-GCM ciphertext>" }
    ```

    So a client that posts the documented JSON directly will be rejected before
    validation. Read
    [Authentication](https://docs.pontisglobe.com/authentication) and build the
    envelope first — it is about fifteen lines of helper code, and the docs give
    it in Node and Python.
  contact:
    name: PontisGlobe Support
    url: https://www.pontisglobe.com/contact
servers:
  - url: https://api.pontisglobe.com
    description: Live. Real money.
  - url: https://sandbox.pontisglobe.com
    description: |
      Sandbox. Deterministic outcomes driven by the trailing cents of
      `source_amount` — see https://docs.pontisglobe.com/sandbox.
security:
  - ApiKeyAuth: []
tags:
  - name: Authentication
    description: Exchange credentials for a short-lived JWT.
  - name: Payouts
    description: Send money to bank accounts and mobile money in 50+ countries.
  - name: Beneficiaries
    description: Save a recipient once, then pay them by id.
  - name: Payins
    description: Accept stablecoin payments through a hosted checkout.
paths:
  /api/v1/beneficiaries/addBeneficiary:
    post:
      tags:
        - Beneficiaries
      summary: Save a recipient
      description: >
        `201` means you just created it; `200` means this one already existed
        and

        you are getting it back. The status code is how you tell them apart.
      operationId: addBeneficiary
      parameters:
        - $ref: '#/components/parameters/TimestampHeader'
        - $ref: '#/components/parameters/SignatureHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - country_code
                - currency_code
                - payment_method
                - recipient_details
              properties:
                country_code:
                  type: string
                  minLength: 2
                  maxLength: 2
                  example: NG
                currency_code:
                  type: string
                  minLength: 2
                  maxLength: 8
                  example: NGN
                payment_method:
                  type: string
                  minLength: 1
                  maxLength: 32
                  example: bank_local
                recipient_details:
                  type: object
                  additionalProperties: true
                  description: >
                    Per-corridor fields, plus `name`. Mobile-money corridors
                    carry

                    their network here as `payment_network` — unlike a payout,
                    it

                    is **not** a top-level field on this endpoint, and one sent
                    at

                    the top level is stripped rather than saved.
                nickname:
                  type: string
                  maxLength: 80
                idempotency_key:
                  type: string
                  format: uuid
      responses:
        '200':
          description: Already existed — the same beneficiary, returned again.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: '#/components/schemas/BeneficiaryRef'
        '201':
          description: Beneficiary created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: '#/components/schemas/BeneficiaryRef'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - ApiKeyAuth: []
          BearerAuth: []
components:
  parameters:
    TimestampHeader:
      name: x-timestamp
      in: header
      required: true
      description: |
        Unix epoch **in seconds** — not milliseconds. Must be within ±5 minutes
        of our clock, which is what makes a captured request unusable later.
        Keep your client's clock NTP-synced.
      schema:
        type: integer
        format: int64
        example: 1748023400
    SignatureHeader:
      name: x-signature
      in: header
      required: true
      description: |
        HMAC-SHA256 over the signing string, hex encoded. The timestamp is part
        of what is signed, so a replayed body cannot be re-dated. See
        https://docs.pontisglobe.com/authentication for how it is built.
      schema:
        type: string
        example: 2f8a9b4c1d7e0a3f6b8c2d5e9f1a4b7c0d3e6f9a2b5c8d1e4f7a0b3c6d9e2f5a
  schemas:
    BeneficiaryRef:
      type: object
      required:
        - beneficiary_id
        - status
      properties:
        beneficiary_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - active
            - archived
    Error:
      type: object
      required:
        - ok
        - request_id
        - error
      properties:
        ok:
          type: boolean
          enum:
            - false
        request_id:
          type: string
          format: uuid
          description: |
            Identifies this exact request in our logs. Quote it when contacting
            support — it is the fastest path to an answer.
          example: 8c1f4b2a-9d3e-4a71-b6c8-5e2f0a7d1934
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Stable, machine-readable. Branch on this, never on `message`.
              example: insufficient_funds
            message:
              type: string
              description: Human-readable. Wording may change without notice.
              example: 'Insufficient available balance. Available: 5.00 USDT'
  responses:
    BadRequest:
      description: >-
        `validation_error` — schema mismatch. Fix and retry; do not retry
        unchanged.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: >-
        `unauthorized` or `jwt_expired` — bad credentials or an expired JWT.
        Re-login.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: '`forbidden` — KYC incomplete, product not enabled, or account suspended.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: '`rate_limited` — back off and retry with a longer interval.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServerError:
      description: '`internal_error` — contact support quoting `request_id`.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: Identifies your account. Issued from Developer Tools in the dashboard.
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Short-lived token from `/api/v1/user/login`, bound to your account and
        mode. Expires in 900 seconds.

````