> ## 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/gateway/payments/create

> Open a payment and get a checkout URL to send your customer to.

Creates a payin and returns the hosted checkout URL. See [Payins](/payins) 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>
  **No JWT.** Unlike the payout endpoints, payin endpoints do not take an `authorization` header —
  creating a payment link moves no money on its own, so it is authenticated by your API key, request
  signature and IP allowlist alone. You can skip the login call entirely for a payin-only
  integration.
</Note>

<Info>
  `success_url` and `fail_url` hosts must be registered on your merchant profile before they will be
  accepted — an unregistered host is rejected with `redirect_domain_not_allowed` before a payment is
  created. Ask your account manager to add them.
</Info>

## Idempotency

Reusing a `merchant_reference` returns the **existing** payment rather than creating a second one.
A retry after a network timeout is safe and gives you back the original `checkout_url`.

**The status code is how you tell the two apart**: `201` means you just created it, `200` means this
`merchant_reference` was already used and you are getting the original payment — and its original
`checkout_url` — back.

<Warning>
  The `checkout_url` is a bearer credential. Anyone holding it can see the order amount and the
  deposit address. Send it to your customer directly — do not log it, email it to a shared inbox, or
  put it anywhere indexable.
</Warning>

## Example request

<Info>
  All examples assume you've already encrypted the body and signed the request. The transport is
  identical to payouts, so the helper in [Payouts
  quickstart](/quickstart#2-set-up-the-request-helper) is the same one both products use.
</Info>

<CodeGroup>
  ```js Node theme={null}
  const payin = await call('/api/v1/gateway/payments/create', {
    merchant_reference: 'ORDER-1042',
    amount: '100.00',
    currency: 'USDT',
    success_url: 'https://shop.example.com/thanks',
    fail_url: 'https://shop.example.com/cancelled',
  })

  // Send the customer here.
  redirect(payin.data.checkout_url)
  ```

  ```python Python theme={null}
  payin = call("/api/v1/gateway/payments/create", {
      "merchant_reference": "ORDER-1042",
      "amount": "100.00",
      "currency": "USDT",
      "success_url": "https://shop.example.com/thanks",
      "fail_url": "https://shop.example.com/cancelled",
  })

  redirect(payin["data"]["checkout_url"])
  ```
</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 or signature, or API access is disabled            |
| 403    | `forbidden`                   | Your IP is not allow-listed, or your account does not hold the payin product  |
| 403    | `profile_missing`             | No merchant profile has been set up for your account yet                      |
| 403    | `gateway_not_enabled`         | The product is held but payins are switched off on your merchant profile      |
| 409    | `wallet_frozen`               | Your wallet is frozen, so nothing can be credited to it                       |
| 422    | `redirect_domain_not_allowed` | A redirect host is not registered on your merchant profile                    |
| 422    | `amount_below_minimum`        | Below the minimum payment amount configured for your account                  |
| 429    | `rate_limited`                | Over 300 creates per minute — back off and retry                              |
| 503    | `no_provider_available`       | We cannot open a payment right now. Retry with backoff                        |
| 503    | `pricing_unavailable`         | Fees are not configured for your account — contact us                         |

```json theme={null}
{
  "ok": false,
  "request_id": "8c1f4b2a-9d3e-4a71-b6c8-5e2f0a7d1934",
  "error": {
    "code": "redirect_domain_not_allowed",
    "message": "Redirect URL host is not in this account’s allowed domains"
  }
}
```

<Info>
  Quote `request_id` when you contact support — it identifies the exact request in our logs.
</Info>

A `503` is about us, not your request: retry the same payload with backoff and it will succeed once
the condition clears. A `4xx` will not change on retry.

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


## OpenAPI

````yaml openapi.yaml POST /api/v1/gateway/payments/create
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/gateway/payments/create:
    post:
      tags:
        - Payins
      summary: Create a payin
      description: >
        Creates a payment and returns a hosted checkout URL to redirect your

        customer to.


        **No JWT.** Payin endpoints authenticate with the API key, timestamp and

        signature alone — there is no login step.


        `merchant_reference` doubles as the idempotency key: replaying a create

        with the same reference returns the existing payment with `200` instead

        of creating a second with `201`.


        Act on the **signed callback**, never on the browser redirect. A
        customer

        can close the tab before returning, and the redirect proves nothing
        about

        whether money arrived.
      operationId: createPayin
      parameters:
        - $ref: '#/components/parameters/TimestampHeader'
        - $ref: '#/components/parameters/SignatureHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - merchant_reference
                - amount
                - success_url
                - fail_url
              properties:
                merchant_reference:
                  type: string
                  minLength: 1
                  maxLength: 128
                  description: Your own order id. Also the idempotency key.
                  example: ORDER-1042
                amount:
                  type: string
                  pattern: ^\d+(\.\d+)?$
                  description: >
                    A positive decimal **string**, at most 2 decimal places.
                    More

                    than two is rejected rather than rounded — a sub-cent order

                    would have the payer charged something the merchant's own

                    order does not say.
                  example: '100.00'
                currency:
                  type: string
                  enum:
                    - USDT
                  default: USDT
                success_url:
                  type: string
                  format: uri
                  description: >
                    `https` only, no embedded credentials, and the host must be
                    on your

                    account's allowed domains.
                fail_url:
                  type: string
                  format: uri
                expires_in_seconds:
                  type: integer
                  minimum: 300
                  maximum: 86400
                  description: >-
                    5 minutes to 24 hours. Also how long a deposit address stays
                    open.
      responses:
        '200':
          description: |
            Idempotent replay — this `merchant_reference` already existed. The
            status code is the only difference from `201`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: '#/components/schemas/PayinCreated'
        '201':
          description: Payment created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: '#/components/schemas/PayinCreated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          description: |
            Codes: `redirect_domain_not_allowed` (the host is not on your
            allowlist), `amount_below_minimum`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
        '503':
          description: '`pricing_unavailable` — fees could not be quoted. Retry shortly.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - ApiKeyAuth: []
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:
    PayinCreated:
      type: object
      required:
        - id
        - merchant_reference
        - status
        - requested_amount
        - requested_currency
        - checkout_url
        - merchant_fee_amount
        - expires_at
      properties:
        id:
          type: string
          format: uuid
        merchant_reference:
          type: string
          description: Echoed back from your request.
          example: ORDER-1042
        status:
          type: string
          enum:
            - created
            - awaiting_payment
            - underpaid
            - overpaid
            - paid
            - expired
            - failed
          description: |
            `created` on a `201`. On a `200` — an idempotent replay of a
            reference that already exists — this is whatever that payment has
            since become, which may be anything up to `paid`. Do not assume
            `created` and re-send a customer to a checkout they already paid.
          example: created
        requested_amount:
          type: string
          example: '100.00'
        requested_currency:
          type: string
          example: USDT
        checkout_url:
          type: string
          format: uri
          description: |
            Redirect your customer here. **Treat it as a secret** — anyone
            holding this URL can view the payment.
          example: https://pay.pontisglobe.com/c/pay_live_TOKEN
        merchant_fee_amount:
          type: string
          description: Total we will charge for this payment. Always present on create.
          example: '0.50'
        expires_at:
          type: string
          format: date-time
    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.

````