<!--
  SINGLE-SOURCE SYNC (ADR-0021): this file is the canonical prose for the /docs/getting-started page
  and its LLM export (served raw at /docs/getting-started.md, indexed by /llms.txt). The React page at
  resources/js/pages/public/docs/getting-started.tsx is hand-structured to mirror it 1:1 — edit BOTH in
  the same change (engineering-rules P4). Technical facts are sourced from docs/01-architecture/10-api-design.md
  and docs/01-architecture/13-brokeret-crm-connector.md; those engineering docs win on any conflict.
-->

# Getting started

PayXiro is a payment orchestration platform: your application makes one API call, and PayXiro handles
provider selection, the hosted Cashier, status tracking, risk and routing, the ledger, and the signed
webhook back to you. This guide covers everything you need to make your first call.

## Base URL

```
https://payxiro.com/api/v1
```

The host is provisional. There is no environment in the URL or a header — the environment is selected
by your API key prefix (see below).

## Authentication

Every request authenticates with a bearer secret key scoped to one **(merchant, environment)** pair:

```
Authorization: Bearer sk_live_9f2Kj...
```

| Key type        | Prefix     | Environment |
| --------------- | ---------- | ----------- |
| Live secret key | `sk_live_` | live        |
| Test secret key | `sk_test_` | sandbox     |

- Keys are created and revoked in the Merchant Portal; the full key is shown exactly once at creation.
- Only a SHA-256 hash of the key is stored — there is no "reveal key" feature.
- A `sk_test_` key can never read or write live data, and vice versa.
- A merchant may hold up to two active keys per environment (create the new key, deploy it, then
  revoke the old one).

## Idempotency

Every POST that creates money movement (`/payments`, `/checkout_sessions`) **requires** an
`Idempotency-Key` header. A request without it is rejected with `invalid_request`.

- Use a stable reference from your own database (e.g. your deposit row ID) as the key, so a retry
  after a network timeout is naturally safe.
- Replay: the same key with the same body within the window returns the **stored original response**
  (same status and body) with header `Idempotency-Replayed: true` — no second Payment is created.
- Conflict: the same key with a **different** body returns `409` (`idempotency_key_reuse`).
- In flight: the same key while the first request is still executing returns `409`
  (`idempotency_key_in_flight`) — retry with backoff.

Keys are stored for at least 30 days, scoped to (merchant, environment, endpoint).

## Conventions

- **Amounts are strings** in JSON (`"500.00"`), never numbers, rendered to the asset's display
  decimals. `"500.001"` is rejected for a 2-decimal asset.
- **Asset codes** are `CODE` or `CODE.NETWORK` (`MYR`, `USDT.TRC20`). `USDT.TRC20` and `USDT.ERC20`
  are distinct assets.
- **Timestamps** are ISO-8601 UTC with a `Z` suffix (`2026-07-05T09:41:22Z`); field names end in `_at`.
- **Public IDs** are `<prefix>_<ULID>` (`pay_`, `cust_`, `cs_`, `pacc_`, `evt_`, ...).
- **v1 evolves additively only** — new fields, endpoints, and enum values are added, never renamed or
  removed. Clients MUST ignore unknown fields and tolerate unknown enum values on read.

## The error envelope

Errors always return this shape:

```json
{
    "error": {
        "type": "invalid_request",
        "code": "amount_below_minimum",
        "message": "Amount 5.00 MYR is below the minimum of 10.00 MYR for this payment method.",
        "param": "amount",
        "request_id": "req_01J9ZTW4C8"
    }
}
```

Branch on `type` and `code`, never on `message` text.

| Type              | HTTP      | Meaning                                                                                  |
| ----------------- | --------- | ---------------------------------------------------------------------------------------- |
| `invalid_request` | 400 / 422 | Malformed body, missing/invalid parameter, missing Idempotency-Key                       |
| `authentication`  | 401       | Missing, revoked, or malformed API key                                                   |
| `permission`      | 403       | Valid key, but the operation is not allowed                                              |
| `not_found`       | 404       | Resource does not exist in this merchant + environment (no cross-tenant existence leaks) |
| `conflict`        | 409       | Idempotency conflicts, invalid state transitions                                         |
| `rate_limit`      | 429       | Too many requests; `Retry-After` header set                                              |
| `provider`        | 402       | The selected Provider rejected the operation after routing/fallback was exhausted        |
| `internal`        | 500       | PayXiro fault; safe to retry with the same Idempotency-Key                               |

## The four-step integration flow

1. **Create a Checkout Session** server-side, with an `Idempotency-Key` set to your own deposit ID, so
   a retry never creates a second Payment.
2. **Present the Cashier** using one of the four modes (redirect, popup, embedded, or headless).
3. **Credit only on the signed webhook** (or a verified server-side `GET`) — never on the browser
   redirect or a client event. The webhook and the redirect race; either can arrive first.
4. **Optionally poll status** as a reconciliation fallback for a missed webhook.

The one rule that matters: credit the customer **only** in the webhook handler (or a verified
server-side `GET`), **never** on the redirect. Continue with [Checkout Sessions](/docs/checkout-sessions).
