> ## 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/status

> Read back one payin by your own order reference — status, amounts, and the currency you were credited in.

Returns the current state of a payment you created. Scoped to your own account: a reference
belonging to another merchant is indistinguishable from one that does not exist — both return
`not_found`.

<Info>
  This is for **reconciliation and support**, not for driving fulfilment. Fulfil on the
  [callback](/payin-callbacks); polling this endpoint in a loop will be rate limited and will always
  be slower than the callback already on its way to you.
</Info>

<Warning>
  The field above is what you **encrypt**, not what goes on the wire. The body is always
  `{ "data": "<aes-256-gcm ciphertext>" }`, and your IP must be on the account allowlist — both
  covered in [Authentication](/authentication).
</Warning>

<Note>
  **No JWT** — same as [create](/api-reference/create-payin). Payin endpoints are authenticated by
  your API key, request signature and IP allowlist. A payin-only integration never calls
  `/api/v1/user/login`.
</Note>

<Note>
  You look a payment up by **your own** reference, not by our `id`. There is nothing extra to store:
  the order id you already have is the key.
</Note>

## Example request

<Info>
  All examples assume you have 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 res = await call('/api/v1/gateway/payments/status', {
    merchant_reference: 'ORDER-1042',
  })

  const payment = res.data

  // Paid in full — safe to fulfil. Amounts are decimal strings, so compare with
  // a decimal library, never by parsing to a float.
  if (payment.status === 'paid' || payment.status === 'overpaid') {
    fulfil(payment.merchant_reference)
  } else if (payment.status === 'underpaid') {
    // Money arrived, but LESS than the order. Never fulfil on this alone.
    holdForReview(payment.merchant_reference)
  }
  ```

  ```python Python theme={null}
  status, body = call('/api/v1/gateway/payments/status', {
      'merchant_reference': 'ORDER-1042',
  })

  payment = body['data']

  # Paid in full — safe to fulfil. Amounts are decimal strings; use Decimal,
  # never float.
  if payment['status'] in ('paid', 'overpaid'):
      fulfil(payment['merchant_reference'])
  elif payment['status'] == 'underpaid':
      # Money arrived, but LESS than the order. Never fulfil on this alone.
      hold_for_review(payment['merchant_reference'])
  ```

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

  ```go Go theme={null}
  req, _ := http.NewRequest("POST",
    "https://api.pontisglobe.com/api/v1/gateway/payments/status",
    strings.NewReader(`{"data":"`+encryptedBody+`"}`))
  req.Header.Set("content-type", "application/json")
  req.Header.Set("x-api-key", apiKey)
  req.Header.Set("x-timestamp", timestamp)
  req.Header.Set("x-signature", signature)
  res, _ := http.DefaultClient.Do(req)
  ```

  ```java Java theme={null}
  HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.pontisglobe.com/api/v1/gateway/payments/status"))
    .header("content-type", "application/json")
    .header("x-api-key", apiKey)
    .header("x-timestamp", timestamp)
    .header("x-signature", signature)
    .POST(HttpRequest.BodyPublishers.ofString("{\"data\":\"" + encryptedBody + "\"}"))
    .build();
  HttpResponse<String> res = HttpClient.newHttpClient().send(req, BodyHandlers.ofString());
  ```

  ```kotlin Kotlin theme={null}
  val req = Request.Builder()
    .url("https://api.pontisglobe.com/api/v1/gateway/payments/status")
    .post("""{"data":"$encryptedBody"}""".toRequestBody("application/json".toMediaType()))
    .addHeader("x-api-key", apiKey)
    .addHeader("x-timestamp", timestamp)
    .addHeader("x-signature", signature)
    .build()
  val res = OkHttpClient().newCall(req).execute()
  ```

  ```php PHP theme={null}
  $ch = curl_init('https://api.pontisglobe.com/api/v1/gateway/payments/status');
  curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
      'content-type: application/json',
      "x-api-key: $apiKey",
      "x-timestamp: $timestamp",
      "x-signature: $signature",
    ],
    CURLOPT_POSTFIELDS => json_encode(['data' => $encryptedBody]),
    CURLOPT_RETURNTRANSFER => true,
  ]);
  $res = curl_exec($ch);
  ```

  ```ruby Ruby theme={null}
  uri = URI('https://api.pontisglobe.com/api/v1/gateway/payments/status')
  req = Net::HTTP::Post.new(uri, {
    'content-type' => 'application/json',
    'x-api-key' => api_key,
    'x-timestamp' => timestamp,
    'x-signature' => signature,
  })
  req.body = { data: encrypted_body }.to_json
  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
  ```

  ```csharp C# theme={null}
  var req = new HttpRequestMessage(HttpMethod.Post,
    "https://api.pontisglobe.com/api/v1/gateway/payments/status");
  req.Headers.Add("x-api-key", apiKey);
  req.Headers.Add("x-timestamp", timestamp);
  req.Headers.Add("x-signature", signature);
  req.Content = new StringContent($"{{\"data\":\"{encryptedBody}\"}}",
    Encoding.UTF8, "application/json");
  var res = await new HttpClient().SendAsync(req);
  ```

  ```swift Swift theme={null}
  var req = URLRequest(url: URL(string: "https://api.pontisglobe.com/api/v1/gateway/payments/status")!)
  req.httpMethod = "POST"
  req.setValue("application/json", forHTTPHeaderField: "content-type")
  req.setValue(apiKey, forHTTPHeaderField: "x-api-key")
  req.setValue(timestamp, forHTTPHeaderField: "x-timestamp")
  req.setValue(signature, forHTTPHeaderField: "x-signature")
  req.httpBody = #"{"data":"\#(encryptedBody)"}"#.data(using: .utf8)
  let (data, _) = try await URLSession.shared.data(for: req)
  ```
</CodeGroup>

## Reading the response

All amounts are **decimal strings**, never numbers — `"100.00"`, not `100.00`. Parsing them into a
floating-point type will eventually cost you precision on a real payment.

<Warning>
  **Three different currencies appear in this one response.** `requested_currency` is what you
  priced in, `deposit_asset` is what you were credited in, and `received_currency` belongs to our
  internal settlement figure. Reconcile against `credited_amount` **and** `deposit_asset` together —
  see [the three currencies](/payins#the-three-currencies-on-a-payment).
</Warning>

### Status values

| `status`           | Money moved? | Meaning                                                | What to do                           |
| ------------------ | ------------ | ------------------------------------------------------ | ------------------------------------ |
| `created`          | no           | Link issued, customer has not chosen how to pay yet    | Wait                                 |
| `awaiting_payment` | no           | Method chosen, waiting for funds on-chain              | Wait                                 |
| `paid`             | **yes**      | Settled for the expected amount                        | **Fulfil the order**                 |
| `underpaid`        | **yes**      | **Customer paid less than the order.** Credited anyway | **Do not auto-fulfil** — your policy |
| `overpaid`         | **yes**      | Settled for more than expected. Credited in full       | Fulfil; you owe the difference       |
| `expired`          | no           | The window passed with no payment                      | Not terminal — see below             |
| `failed`           | no           | The payment could not be completed                     | Treat as unpaid                      |

<Warning>
  All three of `paid`, `underpaid` and `overpaid` moved your balance — but only `paid` and
  `overpaid` mean the customer paid **in full**. There is no minimum on an underpayment: 10 USDT
  against a 100 USDT order settles as `underpaid`. Compare `credited_amount` against
  `requested_amount` before fulfilling anything.
</Warning>

<Note>
  `expired` means *we stopped waiting*, not *no money arrived*. The deposit address stays live, and
  a late payment still settles, still credits you, and still fires a callback. A payment can move
  from `expired` to `paid`, so do not make it terminal in your own system.
</Note>

### Which fields are populated when

Money fields are `null` until the payment settles. This is the single most common surprise here:

| Field                                                  | `created` | `awaiting_payment` | Settled |
| ------------------------------------------------------ | --------- | ------------------ | ------- |
| `id`, `merchant_reference`, `status`, `requested_*`    | ✅         | ✅                  | ✅       |
| `expires_at`, `created_at`                             | ✅         | ✅                  | ✅       |
| `deposit_asset`                                        | default   | ✅                  | ✅       |
| `surface`, `deposit_chain`                             | `null`    | ✅                  | ✅       |
| `received_*`, `merchant_fee_amount`, `credited_amount` | `null`    | `null`             | ✅       |
| `paid_at`                                              | `null`    | `null`             | ✅       |

<Note>
  `deposit_asset` is never `null` — before your customer picks a method it holds the default
  (`USDT`) rather than a choice they have made. Only read it as "the currency you were credited in"
  once `status` is settled or `deposit_chain` is non-null.
</Note>

### Telling the two 400s apart

`validation_error` means `merchant_reference` was missing, empty, or over 128 characters — the
payload reached us and failed validation.

`bad_request` means the **envelope** was wrong, so nothing was ever validated: a missing or skewed
`x-timestamp`, a body that is not `{"data":"…"}`, or a payload we could not decrypt with your
encryption secret. If you are seeing this on every call, the problem is in your signing helper, not
in your fields.

A `403` is either an IP that is not on your allowlist or the payin product not being enabled on the
account. A `429` means you are over **600 lookups per minute**.

<Note>
  A reference that belongs to a different merchant returns `not_found` too. The endpoint never
  confirms that someone else's order exists.
</Note>

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

## Polling, if you must

The callback is the intended path and it is retried up to 8 times — but if you are recovering from
a missed callback or reconciling a batch:

* Look up **on demand** (a support query, an order older than expected), not on a fixed loop over
  every open order
* Stop at the first settled status: `paid`, `underpaid` or `overpaid` are final for fulfilment
* Keep polling `expired` orders only briefly. A late payment fires a callback anyway, and we keep
  re-checking lapsed payments on our side

<Info>
  If a callback never arrived and this endpoint reports a settled status, the money **is** in your
  balance — the delivery failed, not the payment. Check your callback URL is reachable and returning
  2xx: see [retries](/payin-callbacks#retries-and-delivery-failures).
</Info>


## OpenAPI

````yaml openapi.yaml POST /api/v1/gateway/payments/status
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/status:
    post:
      tags:
        - Payins
      summary: Get payin status
      description: >
        Looked up by your own `merchant_reference`.


        **`underpaid` and `overpaid` are settled states, not failures.** Money

        arrived and was credited. Reconcile on `credited_amount` and

        `deposit_asset`; fulfilling only on `paid` silently holds customer
        funds.
      operationId: getPayinStatus
      parameters:
        - $ref: '#/components/parameters/TimestampHeader'
        - $ref: '#/components/parameters/SignatureHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - merchant_reference
              properties:
                merchant_reference:
                  type: string
                  minLength: 1
                  maxLength: 128
                  example: ORDER-1042
      responses:
        '200':
          description: Current state of the payment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: '#/components/schemas/PayinStatus'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      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:
    PayinStatus:
      type: object
      required:
        - id
        - merchant_reference
        - status
        - requested_amount
        - requested_currency
        - deposit_asset
        - expires_at
        - created_at
      properties:
        id:
          type: string
          format: uuid
        merchant_reference:
          type: string
          example: ORDER-1042
        status:
          type: string
          enum:
            - created
            - awaiting_payment
            - underpaid
            - overpaid
            - paid
            - expired
            - failed
          description: >
            `underpaid` and `overpaid` are **settled states, not failures** —
            money

            has arrived and been credited. Treat them as
            paid-with-a-discrepancy,

            and reconcile on `credited_amount`. Fulfilling only on `paid`
            silently

            holds customer funds.
        requested_amount:
          type: string
          example: '100.00'
        requested_currency:
          type: string
          example: USDT
        surface:
          type:
            - string
            - 'null'
          enum:
            - pontis_hosted
            - provider_hosted
            - null
          description: Which checkout the payer used. `null` until they choose.
        deposit_chain:
          type:
            - string
            - 'null'
          enum:
            - trc20
            - erc20
            - optimism
            - null
          description: Network the payer sent on. `null` until they choose.
        deposit_asset:
          type: string
          description: |
            **What your customer sent, and what you were credited in.**
            Reconcile on this.

            Never null — before the payer picks a method it holds the default
            rather than a choice they have made, so only read it as "the
            currency you were credited in" once `status` has settled or
            `deposit_chain` is non-null.
          example: USDC
        received_amount:
          type:
            - string
            - 'null'
          description: |
            Our internal settlement figure. Traceability only — **do not
            reconcile on it**; it is not denominated in what you were credited.
          example: '99.80'
        received_currency:
          type:
            - string
            - 'null'
          example: USD
        merchant_fee_amount:
          type:
            - string
            - 'null'
          description: Total charged, denominated in `deposit_asset`.
          example: '0.50'
        credited_amount:
          type:
            - string
            - 'null'
          description: '**What reached your balance**, after fees, in `deposit_asset`.'
          example: '99.50'
        expires_at:
          type: string
          format: date-time
        paid_at:
          type:
            - string
            - 'null'
          format: date-time
        created_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'
    NotFound:
      description: >
        `not_found`. Also returned when the resource belongs to another account
        —

        the API never confirms that someone else's record exists.
      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.

````