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

# Payments Integration

> Issue invoices, pay beneficiaries and read a unified statement from any system using the SalesOS Payments API and API Keys.

# Payments Integration

<Tip>
  Try signed requests in your browser at the [API Sandbox](https://play2sellsa.github.io/api-sandbox/) — paste your API key and secret, and the playground signs requests automatically.
</Tip>

<Note>
  Examples below show the wire-format `Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE` header. To compute the signature in your code, use the `signedRequest` helper in [Authentication](/api/authentication).
</Note>

The Payments integration is a provider-agnostic REST API for the core financial operations every backoffice needs:

1. **Issue an invoice (charge)** — `POST /payments/invoices`. An *Invoice* here is a payment request — a charge collected via PIX, boleto, card, ACH or SEPA depending on region.
2. **Cancel an invoice** — `POST /payments/invoices/{id}/cancel` (voids unpaid charge).
3. **Issue a tax document (NFS-e/NF-e/PEPPOL)** — `POST /payments/invoices/{id}/tax-documents`. Optional, region-specific (Brazil today, Europe later). Can also be auto-issued from the Invoice creation call.
4. **Pay a beneficiary (a "prize" or any payout)** — `POST /payments/payouts`.
5. **Read a bank statement** — `GET /payments/balance-transactions`.

Every transaction carries first-class control fields used by your finance team for reconciliation: `cost_center` and `contractor_reference` you provide on the request; `our_number` is generated by SalesOS and returned in the response (and on every statement entry). Behind the scenes, SalesOS routes the request to the right provider — you do not pick the provider unless you want to.

<Note>
  **Charge ≠ tax document.** An *Invoice* is the request for payment (boleto, PIX, card). The fiscal document (NFS-e in BR, PEPPOL in EU) is a separate, optional resource attached to the Invoice. This split keeps the API portable across BR / US / EU.
</Note>

<Note>
  The API follows accepted market conventions: REST resource model, integer minor-unit money, ISO-8601 timestamps, RFC 9457 Problem Details for errors, `Idempotency-Key` header for safe retries, cursor pagination, signed webhooks. If you have integrated with any major payments API you will feel at home.
</Note>

## Region routing

SalesOS picks the right backend automatically from the customer's `country` and `currency`. You do not need to choose.

| Customer country | Currency    | Status        | Methods                                          |
| ---------------- | ----------- | ------------- | ------------------------------------------------ |
| BR               | BRL         | live          | `pix`, `boleto`, `bolepix`, `card` (card-as-PIX) |
| US               | USD         | *coming soon* | `card`, `ach_debit`, `bank_transfer`             |
| EU member states | EUR / local | *coming soon* | `card`, `sepa_debit`, `bank_transfer`            |

| Region | Tax document type    | Status                                                  |
| ------ | -------------------- | ------------------------------------------------------- |
| BR     | `nfse` (services)    | live                                                    |
| BR     | `nfe` (products)     | *coming soon*                                           |
| EU     | `peppol` (e-invoice) | *coming soon*                                           |
| US     | n/a                  | sales tax via Invoice line metadata; no fiscal document |

Outbound payouts route by destination: BRL → PIX cashout; other currencies → international transfer.

***

## How It Works

1. **You get an API Key** in the SalesOS Dashboard (Admin > Integrations > API Keys) with the right scopes (`payments:write`, `payments:read`).
2. **You issue invoices** (charges). SalesOS creates the payment request for the customer's region (BR live; US/EU coming soon). You receive a payable artifact: PIX QR-code/copy-paste, boleto barcode, card payment URL or hosted checkout link.
3. **(Optional) A tax document is attached.** When `currency = BRL` and you set `tax_document.auto_issue: true`, SalesOS issues an NFS-e automatically after the charge is confirmed paid. You can also call `POST /invoices/{id}/tax-documents` later to issue or retry.
4. **You pay beneficiaries** (employees, partners, prize winners). SalesOS routes BRL via PIX and other currencies via international transfer.
5. **You read a unified statement** — every invoice, payout, fee and tax-document fee in one ledger, filterable by `cost_center`, `our_number`, `contractor_reference`, period and provider.
6. **You listen to webhooks** for terminal events (`invoice.paid`, `invoice.canceled`, `tax_document.issued`, `payout.paid`, …) instead of polling.

***

## Quick Start

<Steps>
  <Step title="Get your API Key">
    Go to **Admin > Integrations > API Keys** in the SalesOS Dashboard. Create a new key with the scopes `payments:write` and `payments:read`. Copy the key — it will only be shown once.

    Your key looks like: `sk_live_a1b2c3d4e5f6g7h8i9j0...`
  </Step>

  <Step title="Issue your first invoice (BR — PIX charge with auto NFS-e)">
    Create a PIX charge for a Brazilian customer. The optional `tax_document` block tells SalesOS to issue the NFS-e automatically after the charge is paid.

    ```bash theme={null}
    curl -X POST https://api.play2sell.com/functions/v1/payments/invoices \
      -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
      -H "Idempotency-Key: inv-2026-04-27-001" \
      -H "Content-Type: application/json" \
      -d '{
        "customer": {
          "country": "BR",
          "tax_id": "12345678000190",
          "name": "Acme Corp Ltda",
          "email": "billing@acme.com.br"
        },
        "amount": 12345,
        "currency": "BRL",
        "description": "Consulting services — April/2026",
        "payment_method": { "type": "pix", "expires_in_seconds": 3600 },
        "tax_document": {
          "auto_issue": true,
          "type": "nfse",
          "service_code": "01.05"
        },
        "cost_center": "CC-OPERATIONS",
        "contractor_reference": "PO-9981",
        "metadata": { "campaign": "spring-2026", "owner_user_id": "usr-7782" }
      }'
    ```

    **Response (201 Created):**

    ```json theme={null}
    {
      "id": "inv_01HW1Z3K8C7G6Y9PQ4M5R2X0NA",
      "status": "open",
      "amount": 12345,
      "currency": "BRL",
      "region": "br",
      "payment_method": {
        "type": "pix",
        "pix": {
          "qr_code": "00020126580014br.gov.bcb.pix...",
          "qr_code_image_url": "https://api.play2sell.com/.../qr.png",
          "expires_at": "2026-04-27T15:00:00Z"
        }
      },
      "tax_document": {
        "id": "txd_01HW1Z3K8C7G6Y9PQ4M5R2X0NX",
        "type": "nfse",
        "status": "pending",
        "auto_issue": true
      },
      "cost_center": "CC-OPERATIONS",
      "our_number": "INV-2026-000123",
      "contractor_reference": "PO-9981",
      "metadata": { "campaign": "spring-2026", "owner_user_id": "usr-7782" },
      "created": "2026-04-27T14:00:00Z"
    }
    ```

    Status flows from `open` → `paid` (on PIX confirmation, \~seconds) → `tax_document.status: issued` (NFS-e emission, seconds to minutes). Listen to webhooks `invoice.paid` and `tax_document.issued` instead of polling.
  </Step>

  <Step title="(Optional) Issue an invoice for a US customer (coming soon)">
    For US/EU customers SalesOS routes the charge to its international backend. This path is queued behind the Play2Sell LLC onboarding — calls today return `501 provider_not_configured`.

    ```bash theme={null}
    curl -X POST https://api.play2sell.com/functions/v1/payments/invoices \
      -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
      -H "Idempotency-Key: inv-us-2026-04-27-001" \
      -H "Content-Type: application/json" \
      -d '{
        "customer": {
          "country": "US",
          "name": "Acme Inc",
          "email": "billing@acme.com"
        },
        "amount": 12345,
        "currency": "USD",
        "description": "Consulting services — April/2026",
        "payment_method": { "type": "card" },
        "cost_center": "CC-OPERATIONS",
        "contractor_reference": "PO-9981"
      }'
    ```

    No `tax_document` block — US has no national fiscal document. Sales tax goes inside `metadata` or future `line_items`. **The customer's SSN is not collected** by this endpoint: regular B2C/B2B charges in the US do not require it. SSN/EIN handling for 1099 reporting (when paying out US contractors) is a separate flow under `Payouts`, not `Invoices`.
  </Step>

  <Step title="Pay a beneficiary">
    Pay a prize winner via PIX (BRL — routed automatically):

    ```bash theme={null}
    curl -X POST https://api.play2sell.com/functions/v1/payments/payouts \
      -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
      -H "Idempotency-Key: payout-2026-04-27-001" \
      -H "Content-Type: application/json" \
      -d '{
        "beneficiary": {
          "name": "Maria Santos",
          "document": "12345678901",
          "phone": "+5511999000111",
          "email": "maria@example.com",
          "bank_account": { "type": "pix", "key_type": "cpf", "key": "12345678901" }
        },
        "amount": 50000,
        "currency": "BRL",
        "purpose": "prize",
        "cost_center": "CC-AWARDS",
        "contractor_reference": "EVENT-Q2-WINNER-12",
        "metadata": { "event_id": "evt-2026-q2", "user_id": "usr-1101" }
      }'
    ```

    **Response (201 Created):**

    ```json theme={null}
    {
      "id": "po_01HW1Z9P7B5F2X8KQ4M5R2X0NB",
      "status": "processing",
      "amount": 50000,
      "currency": "BRL",
      "region": "br",
      "cost_center": "CC-AWARDS",
      "our_number": "PO-2026-000045",
      "contractor_reference": "EVENT-Q2-WINNER-12",
      "created": "2026-04-27T14:05:00Z"
    }
    ```

    You will receive a `payout.paid` webhook within seconds for sandbox or under a minute for production PIX.
  </Step>

  <Step title="Read your statement">
    List every entry tagged with `cost_center=CC-AWARDS` for the month:

    ```bash theme={null}
    curl -X GET "https://api.play2sell.com/functions/v1/payments/balance-transactions?cost_center=CC-AWARDS&created[gte]=2026-04-01&created[lte]=2026-04-30&limit=50" \
      -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE"
    ```

    **Response (200 OK):**

    ```json theme={null}
    {
      "data": [
        {
          "id": "btxn_01HW1Z9P7B5F2X8KQ4M5R2X0NC",
          "type": "payout",
          "amount": -50000,
          "currency": "BRL",
          "net": -50100,
          "fee": 100,
          "available_on": "2026-04-27",
          "created": "2026-04-27T14:05:30Z",
          "source": { "resource": "payout", "id": "po_01HW1Z9P7B5F2X8KQ4M5R2X0NB" },
          "cost_center": "CC-AWARDS",
          "our_number": "PO-2026-000045",
          "contractor_reference": "EVENT-Q2-WINNER-12"
        }
      ],
      "has_more": false
    }
    ```
  </Step>
</Steps>

***

## Authentication

All requests require an API Key in the `Authorization` header:

```
Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE
```

See the [Authentication page](/api/authentication) for details on creating and managing API Keys.

| Property       | Details                                                           |
| -------------- | ----------------------------------------------------------------- |
| **Header**     | `Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE`          |
| **Scopes**     | `payments:write` (create/cancel), `payments:read` (list/retrieve) |
| **Rate limit** | Configurable per key (default: 1000 requests/hour)                |
| **Key format** | `sk_live_` (production) or `sk_test_` (testing)                   |

***

## Environments

<Tabs>
  <Tab title="Production">
    **Base URL:** `https://api.play2sell.com`

    **Dashboard:** `https://dashboard.play2sell.com`
  </Tab>

  <Tab title="Staging">
    **Base URL:** `https://api-staging.play2sell.com`

    **Dashboard:** `https://dashboard-staging.play2sell.com`

    Staging routes payouts to provider sandboxes. No real money moves.
  </Tab>
</Tabs>

***

## Endpoint Reference

Base path: `/functions/v1/payments`. All endpoints accept and return JSON.

### Invoices

An *Invoice* is a payment request — a charge that will be collected from your customer via PIX, boleto, card or another method depending on region. The Invoice does **not** include a fiscal document by default; see [Tax Documents](#tax-documents) below.

#### `POST /functions/v1/payments/invoices` — create a charge

<ParamField body="customer" type="object" required>
  Customer that will be billed.
</ParamField>

<ParamField body="customer.country" type="string" required>
  ISO-3166-1 alpha-2 (`BR`, `US`, `DE`, …). Drives charge-provider routing.
</ParamField>

<ParamField body="customer.tax_id" type="string">
  CPF (11 digits) or CNPJ (14 digits) for BR (required when `tax_document` is set). For US/EU charges this field is **not** required — leave it out unless your accounting workflow specifically needs it. SSN/EIN are not collected here for normal charges.
</ParamField>

<ParamField body="customer.name" type="string" required>
  Full legal name (max 255 chars).
</ParamField>

<ParamField body="customer.email" type="string" required>
  Valid email — used for receipts and (when applicable) fiscal-document delivery.
</ParamField>

<ParamField body="customer.phone" type="string">
  Optional phone number (max 20 chars). E.164 format recommended (e.g. `+5511999000111`). Used by some providers for AML/KYC matching and transactional notifications.
</ParamField>

<ParamField body="amount" type="integer" required>
  Total amount in **minor units** (e.g. `12345` = R\$ 123,45). Must be positive.
</ParamField>

<ParamField body="currency" type="string" required>
  ISO-4217 code. Drives charge-provider routing alongside `customer.country`.
</ParamField>

<ParamField body="description" type="string" required>
  Human description shown on the payment artifact and (when issued) the fiscal document body (max 1000 chars).
</ParamField>

<Note>
  **The `description` field is shown to the payer.** For PIX charges it appears in the payer's bank/wallet app next to the QR code; for boleto it prints on the slip; for fiscal documents it goes into the document body. Pick a value that reads well in all three contexts (e.g. `"Consulting — April/2026"`, not `"INV-2026-000123 internal-billing-row"`).
</Note>

<ParamField body="payment_method" type="object" required>
  How the charge will be collected.
</ParamField>

<ParamField body="payment_method.type" type="string" required>
  One of `pix`, `boleto`, `bolepix`, `card` (BR); `card`, `ach_debit`, `bank_transfer` (US — coming soon); `card`, `sepa_debit`, `bank_transfer` (EU — coming soon).
</ParamField>

<ParamField body="payment_method.expires_in_seconds" type="integer">
  For `pix` / `boleto` / `bolepix`: how long the payment artifact stays valid. Defaults to 3600 (PIX) / 3 days (boleto).
</ParamField>

<ParamField body="payment_method.return_url" type="string">
  For `card`: where to send the customer after card-checkout completion.
</ParamField>

<ParamField body="tax_document" type="object">
  Optional. When set, SalesOS will issue a fiscal document automatically after the charge is paid. BR-only today.
</ParamField>

<ParamField body="tax_document.auto_issue" type="boolean">
  When `true`, the fiscal document is issued the moment the charge transitions to `paid`. When `false` (or the field is omitted), use `POST /v1/invoices/{id}/tax-documents` later.
</ParamField>

<ParamField body="tax_document.type" type="string">
  `"nfse"` (BR services — live), `"nfe"` (BR products — coming soon), `"peppol"` (EU — coming soon).
</ParamField>

<ParamField body="tax_document.service_code" type="string">
  Municipal service code, required when `type="nfse"` (e.g. `"01.05"` for São Paulo consulting).
</ParamField>

<ParamField body="cost_center" type="string">
  Your internal cost center code (max 64 chars). Optional — when omitted, the charge is filed against the tenant's default cost center. Sending a code that is not registered for the tenant returns `422 unknown_cost_center`. See [Common Control Fields](#common-control-fields).
</ParamField>

<ParamField body="contractor_reference" type="string">
  External customer/contract reference (max 64 chars).
</ParamField>

<ParamField body="metadata" type="object">
  Free-form key-value pairs (max 50 keys, value max 500 chars).
</ParamField>

<Note>
  `our_number` is **not** part of the request — SalesOS generates it on creation and returns it on the response (format: `INV-<YYYY>-<sequence>`). Store it for reconciliation.
</Note>

#### `GET /functions/v1/payments/invoices/{id}` — retrieve

```bash theme={null}
curl -X GET https://api.play2sell.com/functions/v1/payments/invoices/inv_01HW1Z3K8C7G6Y9PQ4M5R2X0NA \
  -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE"
```

The response always includes the current `payment_method` artifact (PIX QR, boleto barcode, card checkout URL) and the inline `tax_document` summary when present.

#### `GET /functions/v1/payments/invoices` — list

Filters: `status` (`open|paid|canceled|failed`), `payment_method.type`, `cost_center`, `our_number`, `contractor_reference`, `created[gte]`, `created[lte]`. Cursor pagination via `limit` (≤ 100), `starting_after`, `ending_before`.

```bash theme={null}
curl -X GET "https://api.play2sell.com/functions/v1/payments/invoices?status=paid&cost_center=CC-OPERATIONS&limit=50" \
  -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE"
```

#### `POST /functions/v1/payments/invoices/{id}/cancel` — void unpaid charge

```bash theme={null}
curl -X POST https://api.play2sell.com/functions/v1/payments/invoices/inv_01HW1Z3K8C7G6Y9PQ4M5R2X0NA/cancel \
  -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
  -H "Idempotency-Key: cancel-inv-001" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "customer_request" }'
```

If the Invoice has an attached `tax_document` already in `issued` state, the cancellation cascades into the fiscal document when the issuing municipality is still inside its cancellation window. A `cancellation_window_expired` error is returned otherwise.

<Note>
  **Refunding a paid charge is not exposed in v1.** If you need to reverse a settled payment, contact support — the team can process it manually. A future version of the API may expose a programmatic refund flow once the use-cases stabilize.
</Note>

***

### Tax Documents

A *TaxDocument* is the fiscal artifact attached to an Invoice — currently NFS-e (BR services). It has its own lifecycle and can be issued, retrieved or canceled independently of the underlying charge. Use this resource when you opted out of `auto_issue` at Invoice creation, when an auto-issue attempt failed and you want to retry, or when you need to cancel only the fiscal document.

#### `POST /functions/v1/payments/invoices/{id}/tax-documents` — issue/retry

<ParamField body="type" type="string" required>
  `"nfse"` today. Future: `"nfe"`, `"peppol"`.
</ParamField>

<ParamField body="service_code" type="string">
  Required when `type="nfse"`.
</ParamField>

<ParamField body="metadata" type="object">
  Optional free-form metadata, persisted on the document row.
</ParamField>

```bash theme={null}
curl -X POST https://api.play2sell.com/functions/v1/payments/invoices/inv_01HW1Z3K8C7G6Y9PQ4M5R2X0NA/tax-documents \
  -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
  -H "Idempotency-Key: txd-2026-04-27-001" \
  -H "Content-Type: application/json" \
  -d '{ "type": "nfse", "service_code": "01.05" }'
```

**Response (202 Accepted):**

```json theme={null}
{
  "id": "txd_01HW1Z3K8C7G6Y9PQ4M5R2X0NX",
  "invoice_id": "inv_01HW1Z3K8C7G6Y9PQ4M5R2X0NA",
  "type": "nfse",
  "status": "pending",
  "created": "2026-04-27T14:10:00Z"
}
```

Issuance is asynchronous. Listen for `tax_document.issued` (or `tax_document.failed`) webhooks. On success the document carries `document_number`, `xml_url`, `pdf_url` and `issued_at`.

#### `GET /functions/v1/payments/invoices/{id}/tax-documents/{doc_id}` — retrieve

#### `POST /functions/v1/payments/invoices/{id}/tax-documents/{doc_id}/cancel` — cancel

```bash theme={null}
curl -X POST "https://api.play2sell.com/functions/v1/payments/invoices/inv_.../tax-documents/txd_.../cancel" \
  -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
  -H "Idempotency-Key: cancel-txd-001" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "billing_correction" }'
```

Subject to the issuing municipality's cancellation window (typically the day of issuance for NFS-e). A `cancellation_window_expired` error is returned otherwise.

***

### Payouts

#### `POST /functions/v1/payments/payouts` — pay a beneficiary

Routing is automatic: `currency = "BRL"` → PIX cashout; any other currency → international transfer.

<ParamField body="beneficiary" type="object" required>
  Recipient of the payout.
</ParamField>

<ParamField body="beneficiary.name" type="string" required>
  Full legal name (max 255 chars).
</ParamField>

<ParamField body="beneficiary.document" type="string" required>
  Tax/identity document of the beneficiary. CPF (11 digits) or CNPJ (14 digits) for BR; passport or local tax ID for international. **Mandatory.**
</ParamField>

<ParamField body="beneficiary.phone" type="string" required>
  Phone number in E.164 format (e.g. `"+5511999000111"`). **Mandatory** — used for AML/KYC matching and payout-status notifications.
</ParamField>

<ParamField body="beneficiary.email" type="string">
  Optional email address. When provided, payout receipts are sent to this address.
</ParamField>

<ParamField body="beneficiary.bank_account" type="object" required>
  Destination account. For BR PIX, set `type: "pix"` and provide `key_type` (`cpf|cnpj|email|phone|evp`) and `key`. For international, set `type: "bank_transfer"` and provide `country`, `iban` or `account_number` + `routing_number` per the destination country's banking requirements.
</ParamField>

<ParamField body="amount" type="integer" required>
  Amount in minor units. Must be positive.
</ParamField>

<ParamField body="currency" type="string" required>
  ISO-4217. Drives backend routing.
</ParamField>

<ParamField body="purpose" type="string" required>
  One of `"prize"`, `"commission"`, `"vendor_payment"`, `"other"`.
</ParamField>

<ParamField body="cost_center" type="string">
  Internal cost center code (max 64 chars). Optional — when omitted, the payout is filed against the tenant's default cost center. Sending a code that is not registered for the tenant returns `422 unknown_cost_center`.
</ParamField>

<ParamField body="contractor_reference" type="string">
  External reference (max 64 chars).
</ParamField>

<ParamField body="metadata" type="object">
  Free-form key-value (max 50 keys, value max 500 chars).
</ParamField>

<Note>
  `our_number` is **not** part of the request — SalesOS generates it on creation and returns it on the response (format: `PO-<YYYY>-<sequence>`). Store it for reconciliation.
</Note>

#### `GET /functions/v1/payments/payouts/{id}` — retrieve

#### `GET /functions/v1/payments/payouts` — list

Filters: `status`, `provider`, `cost_center`, `our_number`, `contractor_reference`, `created[gte]`, `created[lte]`. Cursor pagination as above.

#### `POST /functions/v1/payments/payouts/{id}/cancel` — cancel

Only available for international transfers in `created`/`incoming_payment_waiting` state. PIX cashout is sync-final; once accepted, it cannot be cancelled — issue a payout in the opposite direction to compensate.

***

### Balance Transactions

A unified ledger across providers. Read-only.

#### `GET /functions/v1/payments/balance-transactions` — list

Filters:

| Filter                          | Values                                  |
| ------------------------------- | --------------------------------------- |
| `account`                       | `omie`, `rinne`, `wise`                 |
| `type`                          | `charge`, `payout`, `fee`, `adjustment` |
| `cost_center`                   | any string                              |
| `our_number`                    | any string                              |
| `contractor_reference`          | any string                              |
| `currency`                      | ISO-4217 code                           |
| `created[gte]` / `created[lte]` | ISO-8601 timestamps                     |

```bash theme={null}
curl -X GET "https://api.play2sell.com/functions/v1/payments/balance-transactions?type=payout&created[gte]=2026-04-01&limit=100" \
  -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE"
```

#### `GET /functions/v1/payments/balance-transactions/{id}` — retrieve

Each row exposes:

<ResponseField name="id" type="string">
  Stable ledger-entry id (`btxn_...`).
</ResponseField>

<ResponseField name="type" type="string">
  `charge | payout | fee | adjustment`.
</ResponseField>

<ResponseField name="amount" type="integer">
  Signed minor units. Negative = money out, positive = money in.
</ResponseField>

<ResponseField name="currency" type="string">
  ISO-4217.
</ResponseField>

<ResponseField name="net" type="integer">
  `amount - fee` for outgoing; `amount + fee` ignored for incoming.
</ResponseField>

<ResponseField name="fee" type="integer">
  Provider fee in minor units, always positive.
</ResponseField>

<ResponseField name="available_on" type="string">
  ISO-8601 date the funds settle.
</ResponseField>

<ResponseField name="created" type="string">
  ISO-8601 timestamp of the entry.
</ResponseField>

<ResponseField name="source" type="object">
  `{ resource, id }` — points back to the originating invoice/payout.
</ResponseField>

<ResponseField name="cost_center" type="string">
  Inherited from the source resource.
</ResponseField>

<ResponseField name="our_number" type="string">
  Inherited from the source resource.
</ResponseField>

<ResponseField name="contractor_reference" type="string">
  Inherited from the source resource.
</ResponseField>

***

## Common Control Fields

Every Invoice, Payout and Balance Transaction carries the same control fields. They round-trip on `GET` and are filterable on `LIST`.

| Field                  | Direction        | Format                                      | Max                      | Indexed | Use                                                                                                                                                                                   |
| ---------------------- | ---------------- | ------------------------------------------- | ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cost_center`          | input (optional) | string, FK to your cost-centers table       | 64                       | yes     | File every transaction against a budget line. When omitted, falls back to the tenant's default cost center.                                                                           |
| `contractor_reference` | input (optional) | string                                      | 64                       | yes     | External customer / contract reference (PO, contract id, event id).                                                                                                                   |
| `metadata`             | input (optional) | `Record<string, string>`                    | 50 keys, value 500 chars | no      | Free-form. Use sparingly; not searchable.                                                                                                                                             |
| `our_number`           | **output only**  | string, format `<PREFIX>-<YYYY>-<sequence>` | 64                       | yes     | SalesOS-generated reference returned at creation. Distinct from `id` (ULID for API use). Stable, sequential per resource type per tenant. Use it as the bank-side reconciliation key. |

<Note>
  **Why `our_number` is server-generated.** In Brazilian banking the *cedente* assigns "nosso número" — but in this API SalesOS is the issuance gateway, so we assign the value and hand it back. If you need to carry your own internal id, put it in `contractor_reference` (top-level, indexed) or `metadata.*` (free-form).
</Note>

***

## Idempotency

All `POST` endpoints **require** an `Idempotency-Key` header — a unique string of your choosing (max 255 chars; we recommend a UUIDv4 or a deterministic key derived from your domain).

* The first call with a given key processes normally and the response is stored for 24 h.
* Replays with the same key and the same body return the **same response**, with a `Idempotent-Replay: true` response header.
* Replays with the same key but a **different body** return `409 idempotency_key_reused`.

```bash theme={null}
# First call — processes
curl -X POST https://api.play2sell.com/functions/v1/payments/invoices \
  -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
  -H "Idempotency-Key: inv-2026-04-27-001" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 12345, "currency": "BRL", "payment_method": { "type": "pix" }, "...": "..." }'

# Second call (network retry) — returns same response, no duplicate
curl -X POST https://api.play2sell.com/functions/v1/payments/invoices \
  -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
  -H "Idempotency-Key: inv-2026-04-27-001" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 12345, "currency": "BRL", "payment_method": { "type": "pix" }, "...": "..." }'
```

***

## Money & Currency

Amounts are integers in **minor units** to avoid floating-point errors:

| Currency | Minor unit | `12345` means |
| -------- | ---------- | ------------- |
| `BRL`    | centavo    | R\$ 123,45    |
| `USD`    | cent       | \$123.45      |
| `EUR`    | cent       | €123.45       |

Currencies follow ISO-4217 (3 uppercase letters). Always pair `amount` with `currency`. Reject any payload that mixes scales (e.g. sending decimals).

***

## Error Handling

All errors follow [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457):

```json theme={null}
{
  "type": "https://docs.play2sell.com/errors/validation_error",
  "title": "Invalid request",
  "status": 422,
  "detail": "amount must be a positive integer",
  "instance": "req_01HW1Z3K8C7G6Y9PQ4M5R2X0NA",
  "code": "validation_error",
  "errors": [
    { "field": "amount", "message": "must be a positive integer" }
  ]
}
```

<AccordionGroup>
  <Accordion title="400 — bad_request">
    Malformed JSON or missing required headers. Fix the request and resend.
  </Accordion>

  <Accordion title="401 — unauthorized">
    API key missing, invalid or expired. Check the `Authorization` header. Generate a new key if expired.
  </Accordion>

  <Accordion title="403 — insufficient_scope">
    Key is valid but lacks the required scope (`payments:write` or `payments:read`). Edit the key in the Dashboard.
  </Accordion>

  <Accordion title="404 — not_found">
    The resource id does not exist or belongs to another tenant.
  </Accordion>

  <Accordion title="409 — idempotency_key_reused">
    Same `Idempotency-Key` used with a different body. Either use a fresh key or send the original body.
  </Accordion>

  <Accordion title="422 — validation_error and related">
    The request was understood but cannot be processed. Inspect the `code` field to disambiguate:

    * **`validation_error`** — body validation failed; the `errors[]` array lists field-level problems. Fix the data and resend with a fresh `Idempotency-Key`.
    * **`unsupported_region`** — the customer's `country` / `currency` combination is not yet wired (today only `BR` / `BRL` is live). Wait for the region to ship, or charge a customer in a supported one.
    * **`unknown_cost_center`** — the `cost_center` code on the request is not registered for your tenant. Create it in **Admin > Integrations > Cost Centers** before resending, or omit the field to fall back to the tenant's default cost center.
    * **`merchant_not_provisioned`** — your tenant has no active payment merchant for the resolved cost center. Contact support to finish provisioning before retrying.
  </Accordion>

  <Accordion title="429 — rate_limited">
    Too many requests. The `Retry-After` response header tells you how many seconds to wait.
  </Accordion>

  <Accordion title="502 — provider_error">
    The downstream backend returned an error. The `detail` field contains a sanitized message. Retry safely with the same `Idempotency-Key`.
  </Accordion>

  <Accordion title="500 — server_error">
    Internal error. Retry with exponential backoff (2s, 4s, 8s). Contact support if it persists.
  </Accordion>
</AccordionGroup>

***

## Webhooks

Configure a webhook endpoint per environment in **Admin > Integrations > Webhooks**. SalesOS will POST signed JSON for every terminal event:

| Event                   | Resource      | Triggered when                                                 |
| ----------------------- | ------------- | -------------------------------------------------------------- |
| `invoice.paid`          | invoice       | Charge confirmed paid by the provider                          |
| `invoice.canceled`      | invoice       | Unpaid charge voided                                           |
| `invoice.failed`        | invoice       | Charge failed permanently (e.g. card declined, boleto expired) |
| `tax_document.issued`   | tax\_document | Fiscal document accepted by the municipality                   |
| `tax_document.canceled` | tax\_document | Fiscal document cancellation accepted                          |
| `tax_document.failed`   | tax\_document | Issuance failed permanently                                    |
| `payout.paid`           | payout        | Funds confirmed delivered                                      |
| `payout.failed`         | payout        | Provider rejected the transfer                                 |
| `payout.canceled`       | payout        | Cancel request accepted (international transfers only)         |

Each request includes:

* `X-Pay-Event` — event type (e.g. `payout.paid`).
* `X-Pay-Signature` — `t=<unix>,v1=<hex-hmac-sha256>`. Verify with the secret you set in the Dashboard.
* `X-Pay-Delivery` — unique delivery id, useful for deduplication.

Signature verification (Node.js):

```javascript theme={null}
import crypto from 'node:crypto';

function verifyWebhook(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(kv => kv.split('=')),
  );
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  const ok = crypto.timingSafeEqual(
    Buffer.from(parts.v1, 'hex'),
    Buffer.from(expected, 'hex'),
  );
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // 5 min
  return ok && fresh;
}
```

Sample payload:

```json theme={null}
{
  "id": "evt_01HW1Z9P7B5F2X8KQ4M5R2X0ND",
  "type": "payout.paid",
  "created": "2026-04-27T14:05:30Z",
  "data": {
    "id": "po_01HW1Z9P7B5F2X8KQ4M5R2X0NB",
    "status": "paid",
    "amount": 50000,
    "currency": "BRL",
    "cost_center": "CC-AWARDS",
    "our_number": "PO-2026-000045",
    "contractor_reference": "EVENT-Q2-WINNER-12"
  }
}
```

***

## Complete Code Examples

<CodeGroup>
  ```bash cURL theme={null}
  # Set credentials (see /api/authentication for the signing helper)
  export SALESOS_API_KEY="sk_live_YOUR_API_KEY"
  export SALESOS_API_SECRET="YOUR_API_KEY_SECRET"
  export SALESOS_URL="https://api.play2sell.com/functions/v1/payments"

  # 1. Issue an invoice (BR PIX charge with auto NFS-e)
  curl -s -X POST "$SALESOS_URL/invoices" \
    -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{
      "customer": { "country": "BR", "tax_id": "12345678000190", "name": "Acme Corp", "email": "billing@acme.com" },
      "amount": 12345, "currency": "BRL",
      "description": "Consulting — April/2026",
      "payment_method": { "type": "pix", "expires_in_seconds": 3600 },
      "tax_document": { "auto_issue": true, "type": "nfse", "service_code": "01.05" },
      "cost_center": "CC-OPERATIONS", "contractor_reference": "PO-9981"
    }' | jq .

  # 2. Pay a prize via PIX
  curl -s -X POST "$SALESOS_URL/payouts" \
    -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{
      "beneficiary": {
        "name": "Maria Santos", "document": "12345678901", "phone": "+5511999000111", "email": "maria@example.com",
        "bank_account": { "type": "pix", "key_type": "cpf", "key": "12345678901" }
      },
      "amount": 50000, "currency": "BRL", "purpose": "prize",
      "cost_center": "CC-AWARDS", "contractor_reference": "EVENT-Q2-WINNER-12"
    }' | jq .

  # 3. Read the statement for the cost center
  curl -s -X GET "$SALESOS_URL/balance-transactions?cost_center=CC-AWARDS&created[gte]=2026-04-01&limit=100" \
    -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" | jq .
  ```

  ```javascript Node.js theme={null}
  import { randomUUID } from 'node:crypto';

  const API_URL = 'https://api.play2sell.com/functions/v1/payments';
  const API_KEY = process.env.SALESOS_API_KEY;

  async function call(path, { method = 'GET', body } = {}) {
    const headers = {
      // P2S-SIGN-V1 — see signedRequest() in /api/authentication
      'Authorization': await signedHeader(method, path, body),
      'Content-Type': 'application/json',
    };
    if (method === 'POST') headers['Idempotency-Key'] = randomUUID();

    const res = await fetch(`${API_URL}${path}`, {
      method,
      headers,
      body: body ? JSON.stringify(body) : undefined,
    });

    if (!res.ok) {
      const problem = await res.json();
      throw new Error(`${problem.code}: ${problem.detail || problem.title}`);
    }
    return res.json();
  }

  // Issue an invoice (BR PIX charge with auto NFS-e), pay a prize, read the statement
  const invoice = await call('/invoices', {
    method: 'POST',
    body: {
      customer: { country: 'BR', tax_id: '12345678000190', name: 'Acme Corp', email: 'billing@acme.com' },
      amount: 12345,
      currency: 'BRL',
      description: 'Consulting — April/2026',
      payment_method: { type: 'pix', expires_in_seconds: 3600 },
      tax_document: { auto_issue: true, type: 'nfse', service_code: '01.05' },
      cost_center: 'CC-OPERATIONS',
      contractor_reference: 'PO-9981',
    },
  });
  console.log(`Invoice issued: ${invoice.id} (our_number: ${invoice.our_number}, pix qr: ${invoice.payment_method.pix.qr_code})`);

  const payout = await call('/payouts', {
    method: 'POST',
    body: {
      beneficiary: {
        name: 'Maria Santos',
        document: '12345678901',
        phone: '+5511999000111',
        email: 'maria@example.com',
        bank_account: { type: 'pix', key_type: 'cpf', key: '12345678901' },
      },
      amount: 50000,
      currency: 'BRL',
      purpose: 'prize',
      cost_center: 'CC-AWARDS',
      contractor_reference: 'EVENT-Q2-WINNER-12',
    },
  });
  console.log(`Payout sent: ${payout.id} (${payout.status}, our_number: ${payout.our_number})`);

  const statement = await call(
    '/balance-transactions?cost_center=CC-AWARDS&created[gte]=2026-04-01&limit=100',
  );
  console.log(`Statement: ${statement.data.length} entries`);
  ```

  ```python Python theme={null}
  import os
  import uuid
  import requests

  API_URL = 'https://api.play2sell.com/functions/v1/payments'
  API_KEY = os.environ['SALESOS_API_KEY']

  def call(path: str, method: str = 'GET', body: dict | None = None) -> dict:
      headers = {
          'Authorization': f'Bearer {API_KEY}',
          'Content-Type': 'application/json',
      }
      if method == 'POST':
          headers['Idempotency-Key'] = str(uuid.uuid4())

      r = requests.request(method, f'{API_URL}{path}', json=body, headers=headers, timeout=30)
      if not r.ok:
          problem = r.json()
          raise Exception(f"{problem.get('code')}: {problem.get('detail') or problem.get('title')}")
      return r.json()


  # Issue an invoice (BR PIX charge with auto NFS-e)
  invoice = call('/invoices', 'POST', {
      'customer': {'country': 'BR', 'tax_id': '12345678000190', 'name': 'Acme Corp', 'email': 'billing@acme.com'},
      'amount': 12345,
      'currency': 'BRL',
      'description': 'Consulting — April/2026',
      'payment_method': {'type': 'pix', 'expires_in_seconds': 3600},
      'tax_document': {'auto_issue': True, 'type': 'nfse', 'service_code': '01.05'},
      'cost_center': 'CC-OPERATIONS',
      'contractor_reference': 'PO-9981',
  })
  print(f"Invoice issued: {invoice['id']} (our_number: {invoice['our_number']})")
  print(f"PIX QR code: {invoice['payment_method']['pix']['qr_code']}")

  # Pay a prize
  payout = call('/payouts', 'POST', {
      'beneficiary': {
          'name': 'Maria Santos',
          'document': '12345678901',
          'phone': '+5511999000111',
          'email': 'maria@example.com',
          'bank_account': {'type': 'pix', 'key_type': 'cpf', 'key': '12345678901'},
      },
      'amount': 50000,
      'currency': 'BRL',
      'purpose': 'prize',
      'cost_center': 'CC-AWARDS',
      'contractor_reference': 'EVENT-Q2-WINNER-12',
  })
  print(f"Payout sent: {payout['id']} ({payout['status']}, our_number: {payout['our_number']})")

  # Read statement
  statement = call('/balance-transactions?cost_center=CC-AWARDS&created[gte]=2026-04-01&limit=100')
  print(f"Statement: {len(statement['data'])} entries")
  ```

  ```php PHP theme={null}
  <?php

  $apiUrl       = 'https://api.play2sell.com/functions/v1/payments';
  $apiKey       = getenv('SALESOS_API_KEY');
  $apiKeySecret = getenv('SALESOS_API_SECRET');

  // Build the P2S-SIGN-V1 Authorization header. Note: PHP's hash_hmac()
  // takes (data, key) — opposite of Node/Python — so the args look reversed.
  function p2sSignedHeader(string $method, string $fullPath, string $body): string {
      global $apiKey, $apiKeySecret;
      $ts          = (string) time();
      $payloadHash = hash('sha256', $body);
      $k1  = hash_hmac('sha256', $apiKey,      $apiKeySecret, true);
      $k2  = hash_hmac('sha256', $ts,          $k1,           true);
      $k3  = hash_hmac('sha256', $method,      $k2,           true);
      $k4  = hash_hmac('sha256', $fullPath,    $k3,           true);
      $sig = hash_hmac('sha256', $payloadHash, $k4);  // hex
      return "P2S-SIGN-V1 {$apiKey}:{$ts}:{$sig}";
  }

  function call(string $path, string $method = 'GET', ?array $body = null): array {
      global $apiUrl;
      $bodyStr  = $body ? json_encode($body) : '';
      $fullPath = parse_url($apiUrl, PHP_URL_PATH) . $path;

      $headers = [
          'Authorization: ' . p2sSignedHeader($method, $fullPath, $bodyStr),
          'Content-Type: application/json',
      ];
      if ($method === 'POST') {
          $headers[] = 'Idempotency-Key: ' . bin2hex(random_bytes(16));
      }

      $ch = curl_init($apiUrl . $path);
      curl_setopt_array($ch, [
          CURLOPT_CUSTOMREQUEST => $method,
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_HTTPHEADER => $headers,
          CURLOPT_POSTFIELDS => $bodyStr ?: null,
          CURLOPT_TIMEOUT => 30,
      ]);

      $res = curl_exec($ch);
      $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
      curl_close($ch);
      $json = json_decode($res, true);

      if ($code >= 400) {
          throw new Exception(($json['code'] ?? 'error') . ': ' . ($json['detail'] ?? $json['title'] ?? 'unknown'));
      }
      return $json;
  }

  // Issue an invoice (BR PIX charge with auto NFS-e)
  $invoice = call('/invoices', 'POST', [
      'customer' => ['country' => 'BR', 'tax_id' => '12345678000190', 'name' => 'Acme Corp', 'email' => 'billing@acme.com'],
      'amount' => 12345,
      'currency' => 'BRL',
      'description' => 'Consulting — April/2026',
      'payment_method' => ['type' => 'pix', 'expires_in_seconds' => 3600],
      'tax_document' => ['auto_issue' => true, 'type' => 'nfse', 'service_code' => '01.05'],
      'cost_center' => 'CC-OPERATIONS',
      'contractor_reference' => 'PO-9981',
  ]);
  echo "Invoice issued: {$invoice['id']} (our_number: {$invoice['our_number']})\n";
  echo "PIX QR: {$invoice['payment_method']['pix']['qr_code']}\n";

  // Pay a prize
  $payout = call('/payouts', 'POST', [
      'beneficiary' => [
          'name' => 'Maria Santos',
          'document' => '12345678901',
          'phone' => '+5511999000111',
          'email' => 'maria@example.com',
          'bank_account' => ['type' => 'pix', 'key_type' => 'cpf', 'key' => '12345678901'],
      ],
      'amount' => 50000,
      'currency' => 'BRL',
      'purpose' => 'prize',
      'cost_center' => 'CC-AWARDS',
      'contractor_reference' => 'EVENT-Q2-WINNER-12',
  ]);
  echo "Payout sent: {$payout['id']} ({$payout['status']}, our_number: {$payout['our_number']})\n";
  ```
</CodeGroup>

***

## Best Practices

### Cost-center hygiene

Pick a small, stable set of cost-center codes (≤ 50). Document them in your finance wiki. Reject API calls server-side that reference an unknown code — better to fail loudly than to silently miscategorize.

### Use both keys together for reconciliation

* **`our_number` (server-generated)** is your **bank-side key** — printed on the boleto / sent to the provider, present in your bank statement file.
* **`contractor_reference` (you provide)** is your **contract-side key** — the PO, vendor agreement id or event id you owe against; present in your ERP.

Joining the two at month-end gives you a one-click reconciliation between the bank, the SalesOS Payments API and your ERP.

### Region routing

SalesOS auto-routes by `customer.country` and `currency`. You do not need to pick a backend; trust the routing.

### Webhook reliability

Treat webhooks as the source of truth for terminal status. Polling works but burns rate-limit budget. Always verify `X-Pay-Signature` and dedupe by `X-Pay-Delivery`.

### Handling failures

* **422:** fix the data and resend with a fresh `Idempotency-Key`.
* **429:** back off as `Retry-After` says.
* **502 (provider\_error):** retry with the **same** `Idempotency-Key` — the original request did not commit, so retrying is safe.
* **5xx:** exponential backoff (2s, 4s, 8s). Contact support if it persists.

***

## Rate Limits

Each API key has a configurable limit (default: 1000 requests per hour). The counter resets every hour.

| Limit                     | Value      |
| ------------------------- | ---------- |
| Default requests per hour | 1000       |
| Max payload size          | 1 MB       |
| Max list page size        | 100        |
| Timeout per request       | 60 seconds |

Rate-limited responses include a `Retry-After` header (seconds).

***

## Security

* API keys are hashed with bcrypt — never stored in plaintext.
* Each key is scoped to a single tenant — no cross-tenant access.
* IP allowlists can be configured per key.
* All requests are logged for audit purposes (immutable, 7-year retention).
* Keys can be revoked instantly from the Dashboard.

<Warning>
  Never expose your API key in client-side code (JavaScript running in the browser, mobile apps, or public repos). The Payments API must be called only from your backend server.
</Warning>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/api/authentication">
    Create and manage API Keys
  </Card>

  <Card title="Activities Integration" icon="plug" href="/api/integrations/activities">
    Send CRM activities to SalesOS
  </Card>

  <Card title="Support" icon="headset" href="mailto:suporte@play2sell.com">
    Need help? Contact our support team
  </Card>
</CardGroup>
