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

# Fiat operations: accept pay-ins and send pay-outs to cards

> Accept fiat payins and send payouts to customer cards with the PointZero Fiat API: integration scenarios, statuses, fees, webhooks, sandbox and go-live.

The PointZero Fiat API lets you accept fiat payments (payins), send funds to cards (payouts) and track every operation on a single organization account. This page covers the full integration: where `card_id` comes from, the four integration scenarios, statuses, fees, webhooks, sandbox testing and the go-live checklist.

<Note>
  The Fiat API (version 1.1.0) uses its own base URLs and API keys (`pz_live_`, `pz_test_`). They are separate from the Card Issuing API.
</Note>

## Overview

Payins and payouts are part of one fiat product but are separate API operations:

| Operation | Endpoint | What it does |
| - | - | - |
| Payin | `POST /payments` | Collects fiat from a customer and credits your organization balance |
| Payout | `POST /payouts` | Debits your organization balance and sends funds to a card |

Both operations work with the same organization account, the same balance and the same API keys. In the PointZero dashboard all operations (payins, payouts and card operations) are shown in one history and can be filtered by operation type. The same data is available through `GET /transactions`.

### Core objects

| Object | ID prefix | Description |
| - | - | - |
| Card | `card_` | An existing bank card registered by your customer. Used as the destination for payouts |
| Payment | `pay_` | A payin. Fiat collected from a customer |
| Payout | `po_` | A transfer of funds from your balance to a card |
| Balance | n/a | Funds available on your organization account, per currency |
| Transaction | `txn_` | A ledger entry on your balance (payin, payout, fee, reversal, adjustment) |
| Event | `evt_` | A record of a status change, delivered to you as a webhook |

### Where `card_id` comes from

PointZero does not issue cards for fiat operations. The card already exists: it is the customer's own bank card, and the customer enters its details in your interface.

<Steps>
  <Step title="Customer enters card details">
    Your customer enters card details in your app.
  </Step>

  <Step title="Register the card">
    You send them to `POST /cards`.
  </Step>

  <Step title="PointZero returns a card_id">
    PointZero registers the card, generates a `card_id` and returns it together with the masked card data (`first6`, `last4`, brand, currency, country, expiry).
  </Step>

  <Step title="Store the card_id">
    You store the `card_id` against your customer.
  </Step>

  <Step title="Use it for payouts">
    You pass that `card_id` in every payout (and optionally in a payment) that should send funds to this card.
  </Step>
</Steps>

A registered card can be reused for any number of payouts until it expires or is deleted. Do not register the same card again for every operation. If the same card number is submitted again for the same `external_customer_id`, PointZero returns the existing `card_id` with `200` instead of `201`.

```bash Register a card theme={null}
curl https://api.pointzero.com/v1/cards \
  -X POST \
  -H "Authorization: Bearer pz_live_xxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: card_user_10442_1" \
  -d '{
    "number": "4242424242424242",
    "expiry_month": 9,
    "expiry_year": 2029,
    "holder_name": "MARIA LOPEZ",
    "external_customer_id": "user_10442"
  }'
```

```json Response 201 theme={null}
{
  "id": "card_8fK2mQ1xZ",
  "object": "card",
  "status": "active",
  "brand": "visa",
  "funding_type": "debit",
  "first6": "424242",
  "last4": "4242",
  "expiry_month": 9,
  "expiry_year": 2029,
  "holder_name": "MARIA LOPEZ",
  "currency": "USD",
  "country": "ES",
  "issuer": "CaixaBank",
  "payouts_supported": true,
  "external_customer_id": "user_10442",
  "metadata": {},
  "created_at": "2026-09-21T11:52:03Z",
  "updated_at": "2026-09-21T11:52:03Z"
}
```

Card statuses: `active` (can receive payouts), `expired` (expiry date has passed), `inactive` (deleted by you or disabled by PointZero).

## Integration scenarios

<Tabs>
  <Tab title="A. Payout to card">
    Use this when the customer is only receiving money, for example after selling an asset on your platform.

    ```text theme={null}
    1. Customer enters card details      POST /cards          -> card_id
    2. Funds are on your PointZero balance (proceeds of the sale or prefunding)
    3. You create a payout               POST /payouts        card_id, amount
    4. PointZero sends funds to the card                      payout.processing
    5. Card is credited                                       payout.succeeded
    ```

    Payouts to cards are completed within **15 minutes** of creation (see [Service levels](#service-levels)).

    ```bash theme={null}
    curl https://api.pointzero.com/v1/payouts \
      -X POST \
      -H "Authorization: Bearer pz_live_xxx" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: sell_order_88412" \
      -d '{
        "amount": "500.00",
        "currency": "EUR",
        "card_id": "card_8fK2mQ1xZ",
        "external_id": "sell_order_88412",
        "description": "Sale proceeds, order 88412",
        "customer": {
          "external_customer_id": "user_10442",
          "first_name": "Maria",
          "last_name": "Lopez",
          "country": "ES"
        }
      }'
    ```

    `amount + fee` is reserved on the balance immediately. If the payout fails or is cancelled, the reserved funds are returned.
  </Tab>

  <Tab title="B. Fiat to card">
    Use this when the customer pays fiat and the funds should end up on a card.

    ```text theme={null}
    1. Customer enters card details      POST /cards          -> card_id
    2. You create a payment with card_id POST /payments       -> payment instructions
    3. Customer pays                                          payment.processing
    4. Funds received                                         payment.succeeded
    5. PointZero creates a payout automatically               payout.created (payment_id set)
    6. Card is credited                                       payout.succeeded
    ```

    When `card_id` is set on a payment, PointZero creates the payout to that card as soon as the payment succeeds. You do not need to call `POST /payouts`. The payout carries the `payment_id` of the payment that funded it, and the payment carries the `payout_id`.

    ```bash theme={null}
    curl https://api.pointzero.com/v1/payments \
      -X POST \
      -H "Authorization: Bearer pz_live_xxx" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: order_12345" \
      -d '{
        "amount": "1000.00",
        "currency": "EUR",
        "payment_method": "bank_transfer",
        "card_id": "card_8fK2mQ1xZ",
        "external_id": "order_12345",
        "customer": {
          "external_customer_id": "user_10442",
          "first_name": "Maria",
          "last_name": "Lopez",
          "email": "maria.lopez@example.com",
          "country": "ES"
        }
      }'
    ```

    If you prefer to control the second step yourself (for example to run your own checks), create the payment without `card_id` and call `POST /payouts` after `payment.succeeded`.
  </Tab>

  <Tab title="C. Payin only">
    Create a payment without `card_id`. When the payment succeeds, the net amount is credited to your organization balance and stays there until you use it for payouts.

    ```bash theme={null}
    curl https://api.pointzero.com/v1/payments \
      -X POST \
      -H "Authorization: Bearer pz_live_xxx" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: order_12346" \
      -d '{
        "amount": "250.00",
        "currency": "EUR",
        "payment_method": "card",
        "external_id": "order_12346",
        "return_url": "https://client.com/checkout/complete",
        "customer": {
          "external_customer_id": "user_20981",
          "email": "j.meyer@example.com",
          "country": "DE"
        }
      }'
    ```

    For `card`, redirect the customer to the `redirect_url` in the response. For `bank_transfer`, show all fields of `payment_instructions` to the customer. The `reference` must be included in the transfer, otherwise it cannot be matched automatically and is returned.
  </Tab>

  <Tab title="D. Repeat payout">
    Use the stored `card_id`. Check the card with `GET /cards/{card_id}` if it has not been used for a while: cards with status `expired` or `inactive` cannot receive payouts.
  </Tab>
</Tabs>

### Payment methods

| Method | How the customer pays | Default expiry |
| - | - | - |
| `bank_transfer` | Sends a bank transfer using the details in `payment_instructions` | 86400 seconds (24 hours) |
| `card` | Pays by card on the PointZero hosted page (`redirect_url`) | 1800 seconds (30 minutes) |

Override the expiry with `expires_in` (300 to 604800 seconds). Available methods depend on your contract.

### Edge cases

| Situation | What happens | What you should do |
| - | - | - |
| Customer does not pay before `expires_at` | Payment moves to `expired` | Create a new payment if the customer wants to retry |
| Customer pays a different amount by bank transfer | Payment succeeds with the amount actually received (`received_amount`) | Use `received_amount` and `net_amount`, not `amount` |
| Payment or payout needs manual checks | Status `compliance_review` | Wait for the next webhook. Do not create a duplicate operation |
| Not enough funds on balance | `POST /payouts` returns `422 insufficient_balance` | Top up the balance or reduce the amount |
| Card issuer declines the payout | Payout moves to `failed`, amount and fee are returned to the balance | Show the `failure_code` to the customer and ask for another card if needed |
| Card issuer returns funds after success | Payout moves to `reversed`, a `payout_reversal` transaction credits the balance | Update the customer's operation status |
| Request timed out on your side | The operation may or may not have been created | Repeat the request with the same `Idempotency-Key` |
| Webhook not received | Delivery is retried by PointZero | Reconcile with `GET /payments/{id}`, `GET /payouts/{id}` or `GET /events` |
| Automatic payout after payment fails | Payment stays `succeeded`, payout is `failed`, net amount stays on your balance | Create a new payout manually to the same or another card |

## Environments and authentication

| Environment | Base URL | API key prefix |
| - | - | - |
| Production | `https://api.pointzero.com/v1` | `pz_live_` |
| Sandbox | `https://sandbox-api.pointzero.com/v1` | `pz_test_` |

Sandbox and production are fully isolated. Cards, payments, payouts and balances created in one environment are not visible in the other.

All requests are authenticated with an API key sent as a Bearer token:

```http theme={null}
Authorization: Bearer pz_live_xxx
Content-Type: application/json
```

One API key gives access to all endpoints of your organization account: payments, payouts, cards, balances, transactions, events and limits. There are no separate keys for payins and payouts.

API keys are created and revoked in the dashboard under **Settings → API keys**. Keep keys on your server only. Never put them in a mobile app or browser code. If a key is exposed, revoke it and create a new one; existing operations are not affected.

Requests without a key or with an invalid key return `401`. Requests to a resource that belongs to another organization return `404`.

## Requests and responses

<AccordionGroup>
  <Accordion title="Amounts">
    Amounts are decimal strings with up to two decimal places, for example `"1000.00"`. Do not send amounts as numbers: floating point values lose precision.
  </Accordion>

  <Accordion title="Currencies, countries and timestamps">
    Currencies use ISO 4217 codes (`EUR`, `USD`). Countries use ISO 3166-1 alpha-2 codes (`ES`, `DE`). Supported currencies, payment methods and countries depend on your contract and are returned by `GET /limits`.

    All timestamps are in UTC, ISO 8601 format: `2026-09-21T12:07:31Z`.
  </Accordion>

  <Accordion title="Your own references">
    Every payment and payout accepts `external_id`, your own identifier for the operation (order ID, withdrawal ID). It is returned in responses and webhooks and can be used as a filter in list endpoints. `external_id` must be unique per operation type.

    `metadata` accepts up to 20 key-value pairs (keys up to 40 characters, values up to 500 characters). PointZero stores it and returns it without changes.
  </Accordion>

  <Accordion title="Pagination">
    List endpoints use cursor pagination with `limit` (1 to 100, default 20) and `starting_after` (ID of the last item from the previous page).

    ```json theme={null}
    {
      "data": [ ... ],
      "has_more": true
    }
    ```

    To get the next page, pass the `id` of the last item in `data` as `starting_after`. Items are sorted by `created_at`, newest first.
  </Accordion>

  <Accordion title="Idempotency">
    `POST` requests that create objects (`/payments`, `/payouts`, `/cards`) and cancel operations accept an `Idempotency-Key` header.

    * If a request with the same key and the same body is repeated, PointZero returns the original response and does not create a new object.
    * If the same key is reused with a different body, PointZero returns `409 idempotency_key_reused`.
    * Keys are stored for 24 hours.
    * Use your own operation ID (order ID, withdrawal ID) or a UUID.
  </Accordion>
</AccordionGroup>

<Warning>
  Always send an `Idempotency-Key` for payouts. It is the only safe way to retry a payout request after a timeout or network error.
</Warning>

## FX and fees

If the payout currency differs from the card currency, PointZero converts the funds at the rate applied when the payout is processed. The payout object shows the result:

```json theme={null}
{
  "amount": "500.00",
  "currency": "EUR",
  "exchange_rate": "1.17042",
  "credited_amount": "585.21",
  "credited_currency": "USD"
}
```

`credited_amount = amount × exchange_rate`, rounded to two decimal places. The card currency is returned in the card object (`currency`), so you can show the customer which currency the card will receive.

Fees are set in your contract and returned in each object.

| Operation | How the fee is applied |
| - | - |
| Payment | Deducted from the received amount. `net_amount = received_amount − fee` is credited to the balance |
| Payout | Debited from the balance on top of the amount. `total_debit = amount + fee` |
| Automatic payout (Scenario B) | The fee for the whole flow is charged on the payment. The payout uses the full `net_amount` and has `fee` = `0.00` |

Example, Scenario B, EUR 1,000 to a USD card with a fee of EUR 10:

```text theme={null}
Payment     amount 1000.00 EUR   fee 10.00 EUR   net_amount 990.00 EUR
Payout      amount  990.00 EUR   fee  0.00 EUR   rate 1.17042
Card        credited 1158.72 USD
```

## Statuses

<Tabs>
  <Tab title="Payment">
    | Status | Meaning | Final |
    | - | - | - |
    | `pending` | Payment created, waiting for the customer to pay | No |
    | `processing` | Funds are on the way or being checked | No |
    | `compliance_review` | Payment is held for additional checks | No |
    | `succeeded` | Funds received, net amount credited to your balance | Yes |
    | `failed` | Payment failed. See `failure_code` | Yes |
    | `cancelled` | Payment cancelled by you before funds arrived | Yes |
    | `expired` | Customer did not pay before `expires_at` | Yes |

    ```text theme={null}
    pending ──> processing ──> succeeded
       │             │  └────> failed
       │             └──> compliance_review ──> succeeded | failed
       ├──> cancelled
       └──> expired
    ```

    Only `pending` payments can be cancelled with `POST /payments/{payment_id}/cancel`. If funds arrive after cancellation, they are returned to the sender.
  </Tab>

  <Tab title="Payout">
    | Status | Meaning | Final |
    | - | - | - |
    | `pending` | Payout created, funds reserved on your balance | No |
    | `processing` | Payout sent to the card network | No |
    | `compliance_review` | Payout is held for additional checks | No |
    | `succeeded` | Card credited | Yes, can later become `reversed` |
    | `failed` | Payout failed. Amount and fee returned to the balance | Yes |
    | `cancelled` | Payout cancelled by you while `pending` | Yes |
    | `reversed` | Card issuer returned the funds after success | Yes |

    ```text theme={null}
    pending ──> processing ──> succeeded ──> reversed
       │             │  └────> failed
       │             └──> compliance_review ──> processing | failed
       └──> cancelled
    ```

    Only `pending` payouts can be cancelled with `POST /payouts/{payout_id}/cancel`. Once a payout is `processing`, it cannot be cancelled.
  </Tab>
</Tabs>

<Note>
  Treat only `succeeded` as a completed operation. A successfully created payment or payout (`201` response) means that the operation was accepted, not that money has moved.
</Note>

## Service levels

| Operation | Target time |
| - | - |
| Payout to card, from creation to `succeeded` or `failed` | up to 15 minutes |
| Automatic payout (Scenario B), from `payment.succeeded` to `succeeded` or `failed` | up to 15 minutes |
| Payment by card | usually under 1 minute after the customer confirms |
| Payment by bank transfer | depends on the sending bank and the transfer scheme |

Time spent in `compliance_review` is not counted toward the payout SLA. Each payout includes `expected_completion_at` so you can show the customer when to expect the funds.

## Balances and transactions

`GET /balances` returns the organization balance in each currency. This is the same balance shown in the dashboard and used for payouts.

| Field | Description |
| - | - |
| `available` | Can be used for payouts now |
| `reserved` | Held for payouts in `pending`, `processing` or `compliance_review` |
| `pending` | Incoming payments in `processing` or `compliance_review`. Not available yet |

`GET /transactions` returns the operation history. Filter by `type`, `currency` or `source_id` (the payment or payout ID). Amounts are positive for credits and negative for debits; `net` is the actual change of the balance.

| Type | Description |
| - | - |
| `payin` | Payment credited to the balance |
| `payout` | Payout debited from the balance |
| `payout_refund` | Failed or cancelled payout returned to the balance |
| `payout_reversal` | Funds returned by the card issuer after a successful payout |
| `top_up` | Balance funded by you or by PointZero (for example, proceeds of a sale) |
| `adjustment` | Manual correction by PointZero |

## Webhooks

PointZero sends a webhook every time a payment, payout or card changes status. Webhooks are the main way to learn about results. Polling should only be used for reconciliation.

### Setup

Add your endpoint URL in the dashboard under **Settings → Webhooks**, for example `https://client.com/api/pointzero/webhooks`. The endpoint must use HTTPS. After saving, the dashboard shows the webhook signing secret (`whsec_...`). Sandbox and production have separate endpoints and secrets.

### Delivery

```http theme={null}
POST /api/pointzero/webhooks
Content-Type: application/json
X-PointZero-Event-Id: evt_3Lm9QpX2a
X-PointZero-Timestamp: 1758456582
X-PointZero-Signature: 5d41402abc4b2a76b9719d911017c592...
```

* Respond with any `2xx` status within 10 seconds. Process the event asynchronously if your handling takes longer.
* Any other response or a timeout counts as a failed delivery. PointZero retries with increasing intervals for up to 24 hours.
* Events may arrive more than once and not always in order. Use the event `id` to skip duplicates, and compare `data.status` with what you already have stored instead of relying on arrival order.
* Failed deliveries can be viewed and resent from the dashboard. All events for the last 30 days are also available through `GET /events`.

### Signature verification

The signature is an HMAC-SHA256 hex digest of the string `{X-PointZero-Timestamp}.{raw request body}`, using your webhook signing secret as the key.

1. Read the raw request body before any JSON parsing.
2. Build the signed string: timestamp, a dot, the raw body.
3. Compute HMAC-SHA256 with your signing secret.
4. Compare with `X-PointZero-Signature` using a constant-time comparison.
5. Reject the request if the timestamp is more than 5 minutes old.

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  function verifyPointZeroWebhook(rawBody, headers, secret) {
    const timestamp = headers["x-pointzero-timestamp"];
    const signature = headers["x-pointzero-signature"];

    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex");

    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
  }
  ```

  ```python Python theme={null}
  import hmac, hashlib, time

  def verify_pointzero_webhook(raw_body: bytes, headers: dict, secret: str) -> bool:
      timestamp = headers["X-PointZero-Timestamp"]
      signature = headers["X-PointZero-Signature"]

      if abs(time.time() - int(timestamp)) > 300:
          return False

      signed = f"{timestamp}.".encode() + raw_body
      expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, signature)
  ```
</CodeGroup>

### Event types

| Event | Sent when |
| - | - |
| `payment.created` | Payment created |
| `payment.processing` | Funds detected, processing started |
| `payment.compliance_review` | Payment held for checks |
| `payment.succeeded` | Funds received and credited to balance |
| `payment.failed` | Payment failed |
| `payment.cancelled` | Payment cancelled |
| `payment.expired` | Payment expired |
| `payout.created` | Payout created (manually or automatically after a payment) |
| `payout.processing` | Payout sent to the card network |
| `payout.compliance_review` | Payout held for checks |
| `payout.succeeded` | Card credited |
| `payout.failed` | Payout failed |
| `payout.cancelled` | Payout cancelled |
| `payout.reversed` | Funds returned by the card issuer |
| `card.created` | Card registered |
| `card.updated` | Card status changed, for example to `expired` |

The `data` field of every event contains the full object (payment, payout or card) in the same format as the API response.

## Compliance

Depending on the jurisdiction, payment method and amount, PointZero may need additional information before an operation can be completed:

* customer identification (name, date of birth, address, country);
* KYC or KYB data;
* source of funds information;
* sanctions and transaction screening results.

Pass the `customer` object in payments and payouts. `external_customer_id` is required; other fields help avoid `compliance_review`. Missing data does not block creation but can move an operation to `compliance_review`. While an operation is in review, the funds are held and no action from your side is needed unless the PointZero compliance team contacts you. The review ends with a `*.succeeded`, `*.processing` or `*.failed` event.

<Warning>
  `POST /cards` accepts the full card number. Sending card numbers from your servers requires PCI DSS compliance on your side. Confirm your PCI DSS status with PointZero before going live. PointZero never returns the full card number: responses contain only `first6` and `last4`.
</Warning>

## Limits and rate limiting

Limits depend on country, payment method, currency, operation type, customer and card. Read the current values with `GET /limits` and validate amounts in your interface before creating an operation. Pass `card_id` or `external_customer_id` to get the remaining amount for that card or customer today. If a limit is exceeded, the API returns `422 limit_exceeded` and the `error.param` field names the limit.

Request limits are set per organization. Every response includes:

| Header | Description |
| - | - |
| `X-RateLimit-Limit` | Requests allowed per second |
| `X-RateLimit-Remaining` | Requests left in the current window |
| `Retry-After` | Sent with `429`, seconds to wait before retrying |

## Errors

Errors have the same format on every endpoint. Include `request_id` when contacting PointZero support.

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_amount",
    "message": "Amount must be greater than 0.",
    "param": "amount",
    "request_id": "req_7Hk2LpQ9s"
  }
}
```

<AccordionGroup>
  <Accordion title="HTTP status codes">
    | Code | Meaning | Retry |
    | - | - | - |
    | `200` | Success | n/a |
    | `201` | Object created | n/a |
    | `400` | Malformed request (invalid JSON, wrong types) | No, fix the request |
    | `401` | Missing or invalid API key | No |
    | `403` | Operation not enabled for your account | No |
    | `404` | Object not found | No |
    | `409` | Conflict: idempotency key reused or invalid status transition | No |
    | `422` | Request is valid but cannot be processed (balance, limits, card status) | No, fix the cause |
    | `429` | Too many requests | Yes, after `Retry-After` |
    | `500`, `502`, `503` | PointZero error | Yes, with the same `Idempotency-Key` |
  </Accordion>

  <Accordion title="Error types">
    | Type | Description |
    | - | - |
    | `invalid_request_error` | The request is invalid |
    | `authentication_error` | API key problem |
    | `permission_error` | Operation is not allowed for your account |
    | `processing_error` | Operation cannot be completed (balance, card, limits) |
    | `rate_limit_error` | Too many requests |
    | `api_error` | Internal PointZero error |
  </Accordion>

  <Accordion title="Error codes">
    | Code | HTTP | Description |
    | - | - | - |
    | `invalid_amount` | 422 | Amount is zero, negative or has more than two decimals |
    | `invalid_currency` | 422 | Currency is not supported for this operation |
    | `invalid_payment_method` | 422 | Payment method is not enabled for your account |
    | `unsupported_country` | 422 | Customer or card country is not supported |
    | `invalid_card_number` | 422 | Card number fails validation |
    | `card_expired` | 422 | Card is expired |
    | `card_not_found` | 404 | No card with this `card_id` |
    | `card_not_active` | 422 | Card cannot receive payouts (status `inactive` or `expired`) |
    | `card_not_supported` | 422 | Card type or issuer does not accept payouts |
    | `insufficient_balance` | 422 | Not enough available balance for `amount + fee` |
    | `limit_exceeded` | 422 | A transaction, daily or monthly limit is exceeded |
    | `payment_not_found` | 404 | No payment with this ID |
    | `payout_not_found` | 404 | No payout with this ID |
    | `invalid_status_transition` | 409 | For example, cancelling a payout that is already `processing` |
    | `duplicate_external_id` | 409 | `external_id` already used for another operation of this type |
    | `idempotency_key_reused` | 409 | Same `Idempotency-Key` sent with a different request body |
    | `rate_limit_exceeded` | 429 | Too many requests |
    | `internal_error` | 500 | Internal error |
  </Accordion>

  <Accordion title="Failure codes">
    When a payment or payout ends in `failed`, the object contains `failure_code` and `failure_message`.

    | Failure code | Applies to | Description |
    | - | - | - |
    | `card_declined` | payout, card payment | Declined by the card issuer |
    | `card_expired` | payout, card payment | Card expired before processing |
    | `insufficient_funds` | card payment | Not enough funds on the customer's card |
    | `authentication_failed` | card payment | 3-D Secure not completed |
    | `bank_transfer_returned` | payment | Transfer returned by the sending bank |
    | `compliance_rejected` | payment, payout | Rejected after compliance review |
    | `limit_exceeded` | payment, payout | Limit exceeded at processing time |
    | `processing_error` | payment, payout | Network or processor error |
  </Accordion>
</AccordionGroup>

## Sandbox testing

The sandbox behaves like production but does not move real money. Use `pz_test_` keys and the sandbox base URL. Each sandbox organization starts with a balance of 100,000.00 EUR and 100,000.00 USD. Successful sandbox payments add to it.

### Test cards for payouts

Any future expiry date and any holder name can be used.

| Card number | Currency | Result |
| - | - | - |
| `4242 4242 4242 4242` | EUR | Payout succeeds |
| `4000 0000 0000 0077` | USD | Payout succeeds with FX |
| `4000 0000 0000 0002` | EUR | Payout fails with `card_declined` |
| `4000 0000 0000 0069` | EUR | Card registers with status `expired` |
| `4000 0000 0000 0259` | EUR | Payout succeeds, then is reversed after 2 minutes |
| `4000 0000 0000 9235` | EUR | Payout goes to `compliance_review`, then succeeds |

### Simulating payments

Bank transfers do not happen in the sandbox. Move a payment to the status you need with `POST /sandbox/payments/{payment_id}/simulate`. Webhooks are sent as in production. To test underpayment or overpayment by bank transfer, pass `received_amount` together with `status: succeeded`.

```bash theme={null}
curl https://sandbox-api.pointzero.com/v1/sandbox/payments/pay_7Hc2Xp9Ld/simulate \
  -X POST \
  -H "Authorization: Bearer pz_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "status": "succeeded", "received_amount": "950.00" }'
```

Allowed statuses: `processing`, `compliance_review`, `succeeded`, `failed` (with an optional `failure_code`) and `expired`.

## Go-live checklist

* [ ] Production API key created and stored on the server only
* [ ] Production webhook endpoint added, signing secret stored
* [ ] Webhook signature and timestamp verified
* [ ] Duplicate events skipped by event `id`
* [ ] `Idempotency-Key` sent on every `POST`, retries reuse the same key
* [ ] `card_id` stored per customer, cards reused instead of re-registered
* [ ] All payment and payout statuses handled, including `compliance_review` and `reversed`
* [ ] `received_amount` and `net_amount` used for bank transfers
* [ ] `insufficient_balance` handled for payouts
* [ ] Limits checked before creating operations
* [ ] Error handling with `request_id` logged
* [ ] Reconciliation job using `GET /transactions` or `GET /events`
* [ ] Customer data passed for compliance where required
* [ ] PCI DSS status confirmed with PointZero
* [ ] All scenarios tested in the sandbox

## FAQ

<AccordionGroup>
  <Accordion title="Are payins and payouts separate products?">
    No. They are one fiat product with two separate API operations: `POST /payments` for payins and `POST /payouts` for payouts.
  </Accordion>

  <Accordion title="Do we need different API keys for /payments and /payouts?">
    No. One API key works for all endpoints of your organization account.
  </Accordion>

  <Accordion title="Is there a separate dashboard for payouts?">
    No. There is one organization account with a shared balance and one operation history. Payins, payouts and card operations are separated by operation type.
  </Accordion>

  <Accordion title="Where does card_id come from? There is no endpoint to issue a card.">
    PointZero does not issue cards for fiat operations. The customer enters an existing card in your app, you register it with `POST /cards`, and PointZero returns the `card_id` together with the masked card data. You store it and use it as the payout destination.
  </Accordion>

  <Accordion title="The customer only sells and does not buy anything. What do we send?">
    Register the customer's card with `POST /cards` to get a `card_id`, then create a payout with `POST /payouts`. No payment is needed. The payout is completed within 15 minutes.
  </Accordion>

  <Accordion title="Is card_id required when creating a payment?">
    No. Pass it only if the funds from this payment should go to a card automatically (Scenario B). Without it, the funds stay on your balance.
  </Accordion>

  <Accordion title="Can we check the balance of the customer's card?">
    No. The card belongs to the customer's bank, so its balance is not available. Use the payout status to confirm that funds were credited.
  </Accordion>

  <Accordion title="How do we know that the money has arrived on the card?">
    The `payout.succeeded` webhook, or `status: succeeded` in `GET /payouts/{payout_id}`.
  </Accordion>

  <Accordion title="What happens to the money if a payout fails?">
    The amount and the fee are returned to your available balance. A `payout_refund` transaction appears in `GET /transactions`.
  </Accordion>
</AccordionGroup>

## Versioning and changelog

The version is part of the URL (`/v1`). Within a version PointZero may add new endpoints, optional request fields, response fields, enum values and event types. Your integration should ignore unknown fields and handle unknown enum values gracefully. Breaking changes are released only in a new version, with advance notice.

| Version | Changes |
| - | - |
| 1.1.0 | Added payouts (`/payouts`) as a separate operation. Added card registration (`POST /cards`). `card_id` on payments is now optional and triggers an automatic payout. Added balances, transactions and events. Card funding (`/fundings`) is replaced by payouts. Card balance endpoint removed |
| 1.0.0 | Initial version |

Questions about your integration: [hi@pointzero.com](mailto:hi@pointzero.com).


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