# Accept Your First Payment in 5 Minutes

Zero to a signed `payment.succeeded` webhook, from your terminal — **on test mode, with no real money**. Four curl calls mint your account and a checkout session; one component collects the payment; the webhook (or a poll) confirms it. No dashboard, no OAuth — auth is API-key only. At the end, going live is a two-line key swap.

Base URL: `https://api.peltier.dev`. Self-hosting? The `/v1/*` merchant surface is gated behind `MERCHANT_MODE=true` on the backend.

## 1. Create your merchant account

The only no-auth call. It dual-mints **two** key pairs at once — live (`sk_live_`/`pk_live_`, mainnet) and test (`sk_test_`/`pk_test_`, testnets). **Every raw key is returned exactly once**, store all four immediately (the server keeps only hashes).

```bash
curl -X POST https://api.peltier.dev/v1/merchants \
  -H "Content-Type: application/json" \
  -d '{ "email": "ops@acme.com", "name": "Acme Inc" }'
```

```json
{
  "id": "m_a1b2...",
  "email": "ops@acme.com",
  "name": "Acme Inc",
  "created_at": "2026-07-05T12:00:00Z",
  "keys": {
    "live": { "secret_key": "sk_live_…", "publishable_key": "pk_live_…" },
    "test": { "secret_key": "sk_test_…", "publishable_key": "pk_test_…" }
  },
  "secret_key": "sk_live_…",       // back-compat mirror of keys.live — server-side ONLY
  "publishable_key": "pk_live_…"   // back-compat mirror of keys.live — browser-safe
}
```

This quickstart runs on the **test** pair — the key prefix *is* the mode, no flag anywhere:

```bash
export PELTIER_SECRET_KEY="sk_test_…"   # keys.test.secret_key
```

## 2. Pin your settlement destination

**Don't skip this.** At commit time the server verifies the buyer's signed Intent against the destination you pin here — with nothing pinned, **every checkout fails at commit**. Pin the address where USDC should land, per chain. Test keys route to testnets, so pin a testnet chain (here: Base Sepolia, `84532`):

```bash
curl -X PUT https://api.peltier.dev/v1/destinations/84532 \
  -H "Authorization: Bearer $PELTIER_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" }'
```

```json
{ "id": "...", "chain_id": 84532, "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", "created_at": "...", "updated_at": "..." }
```

## 3. Register your webhook endpoint

One endpoint **per mode**; must be **https** (SSRF-guarded — no private/internal hosts; a dev-only `http://localhost` escape exists behind `WEBHOOK_ALLOW_INSECURE`). The `whsec_` signing secret is returned **once**; re-`PUT` rotates it. Registered with your `sk_test_` key, this is your *test* endpoint — test deliveries are signed with *its* secret.

```bash
curl -X PUT https://api.peltier.dev/v1/webhook_endpoints \
  -H "Authorization: Bearer $PELTIER_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://acme.com/peltier/webhook" }'
```

```json
{ "id": "...", "url": "https://acme.com/peltier/webhook", "signing_secret": "whsec_...", "created_at": "...", "updated_at": "..." }
```

No public URL handy right now? Skip this step and reconcile by polling (step 6 shows both).

## 4. Create a payment session

From your server, with the `sk_test_`. `amount` is the expected **net** USDC in **base units, 6 decimals** — `"10000000"` is 10 USDC; a decimal string like `"10.00"` is rejected. The response's `client_secret` is what your browser page will use; the session lives 30 minutes.

```bash
curl -X POST https://api.peltier.dev/v1/payment_sessions \
  -H "Authorization: Bearer $PELTIER_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "amount": "10000000", "order_id": "ord_42" }'
```

```json
{
  "session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "client_secret": "cs_9f1c...e2",
  "expires_at": 1751719800
}
```

(Node backend? The SDK wraps this as `createPaymentSession(apiUrl, secretKey, { amount, order_id })` from `@peltier/node` — same rule there: `amount` must be integer base units, and the wire field is snake_case `order_id`.)

## 5. Embed the checkout

Hand `{ session_id, client_secret }` plus your `pk_test_` to the browser — **never the `sk_`**. `destinationAddress` / `destinationChainId` must be the exact pin from step 2 (the commit is rejected on mismatch, so a tampering buyer only breaks their own checkout). Because the key is a test key, the widget offers testnet chains automatically.

```bash
npm install @peltier/react
```

```typescript
import { PeltierProvider, PeltierCheckout } from '@peltier/react'
import '@peltier/react/styles.css'

<PeltierProvider
  apiUrl="https://api.peltier.dev"
  publishableKey="pk_test_…"            // keys.test — pk_test_ ⇒ testnet chains
>
  <PeltierCheckout
    sessionId={sessionId}                 // from step 4
    clientSecret={clientSecret}           // from step 4
    destinationAddress="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"  // the step-2 pin
    destinationChainId={84532}            // Base Sepolia — the step-2 chain
    onComplete={(sessionId) => console.log('paid (test)', sessionId)}
  />
</PeltierProvider>
```

Pay it with **testnet USDC** from a wallet funded for free at [faucet.circle.com](https://faucet.circle.com), on the same testnet as your destination (Base Sepolia here). Same-chain testnet USDC needs no swap or bridge venue, so it settles out of the box; cross-token / cross-chain on testnet needs extra venue wiring — see [Test mode](/docs/test-mode.md).

## 6. Receive your first webhook

When the payment settles, your endpoint gets a signed `payment.succeeded` (or `payment.overpaid` / `payment.underpaid` — see [amount classification](/docs/amount-classification.md)). Test deliveries carry `livemode: false`:

```json
{
  "session_id": "a1b2c3d4-...",
  "event_type": "payment.succeeded",
  "order_id": "ord_42",
  "amount": "10000000",
  "settled_amount": "10000000",
  "dest_chain_id": 84532,
  "livemode": false,
  "mode": "test"
}
```

Verify the `Peltier-Signature` header before trusting the body — pass the **raw** request bytes, never re-encoded JSON. **`verifyWebhookSignature` is async**: forgetting the `await` makes the check always pass (a Promise is truthy) and accepts every forged webhook.

```typescript
import { verifyWebhookSignature } from '@peltier/node'

const ok = await verifyWebhookSignature(
  req.header('Peltier-Signature'),
  rawBody,
  process.env.PELTIER_WEBHOOK_SECRET,  // the whsec_ from step 3
)
if (!ok) return res.status(400).end()
```

(Debugging a failing signature? `verifyWebhookSignatureDetailed` returns `{ ok, reason }` — see [Webhooks](/docs/webhooks.md).)

Skipped webhooks, or just double-checking? Poll with your `sk_test_`:

```bash
curl https://api.peltier.dev/v1/payment_sessions/a1b2c3d4-... \
  -H "Authorization: Bearer $PELTIER_SECRET_KEY"
# → { "status": "swept", "payment_outcome": "succeeded", "settled_amount": "10000000", ... }
```

## Going live

Everything above is mode-symmetric, so going live is a key swap, not a rewrite: take the **live** pair from the same step-1 response (`keys.live`), re-run steps 2–3 with the `sk_live_` key (pin a **mainnet** destination — e.g. Base, `8453` — and register your live webhook endpoint, which gets its own `whsec_`), then create sessions with `sk_live_` and hand the browser `pk_live_`. The two modes are fully isolated server-side — nothing from your test run leaks into production.

That's it — you've accepted your first payment. Next: the full [checkout flow](/docs/flow-a-checkout.md) (statuses, TTL, the pk_ + client_secret guard), [webhooks](/docs/webhooks.md) (event taxonomy, retries, idempotency), [accounts & API keys](/docs/merchant-accounts.md) (key rotation, extra keys), and [amount classification](/docs/amount-classification.md) (over/underpayment semantics).
