# Get your API keys

Source: https://caravelpay.app/docs/connect.html · Caravel Pay docs · Last updated 2026-11-09

One call gives you a merchant id and a secret. Send your site and your payout wallet, and you can create checkouts straight away. Using WooCommerce? The plugin makes this call for you when you save your wallet.

## Make the call

`POST/v1/connect`

This is the only call you don't sign: it's the one that gives you the keys. Make it from your server.

| Parameter | Description |
|---|---|
| `site_url` · string · required | Your site's HTTPS address, such as `https://shop.example`. It sets where your return, cancel and callback URLs may point. See [below](https://caravelpay.app/docs/connect.html#site-url). |
| `payout_address` · string · required | The Polygon wallet that receives your payouts, starting with `0x`. See [your payout wallet](https://caravelpay.app/docs/connect.html#wallet). |
| `platform` · string · required | `"api"`. (The WooCommerce plugin sends `"woocommerce"`.) |
| `email` · string · optional | Where we send service notices about this merchant, if ever needed. |

*POST /v1/connect · curl*

```sh
curl https://api.caravelpay.app/v1/connect \
  -H "Content-Type: application/json" \
  --data-binary '{
    "site_url": "https://shop.example",
    "payout_address": "0x52908400098527886E0F7030069857D2E4169EE7",
    "email": "you@shop.example",
    "platform": "api"
  }'
```

*POST /v1/connect · Node.js*

```js
// Node.js 18+ (built-in fetch). Run once, on your server.
import { writeFile } from "node:fs/promises";

const res = await fetch("https://api.caravelpay.app/v1/connect", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    site_url: "https://shop.example",
    payout_address: "0x52908400098527886E0F7030069857D2E4169EE7",
    email: "you@shop.example", // optional: service notices only
    platform: "api",
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);

// The secret is in this response only. Save it outside your web root.
await writeFile(
  ".env.caravel",
  `CARAVEL_MERCHANT=${data.merchant_id}\nCARAVEL_SECRET=${data.secret}\n`,
  { mode: 0o600 },
);
console.log(`Connected as ${data.merchant_id}`);
```

*POST /v1/connect · PHP*

```php
<?php
// PHP 7.4+ with the curl extension. Run once, on your server.
$ch = curl_init('https://api.caravelpay.app/v1/connect');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 15,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS     => json_encode([
        'site_url'       => 'https://shop.example',
        'payout_address' => '0x52908400098527886E0F7030069857D2E4169EE7',
        'email'          => 'you@shop.example', // optional: service notices only
        'platform'       => 'api',
    ], JSON_UNESCAPED_SLASHES),
]);
$data = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status !== 201) {
    throw new RuntimeException(($data['error']['code'] ?? 'http_' . $status) . ': ' . ($data['error']['message'] ?? ''));
}

// The secret is in this response only. Save it outside your web root.
file_put_contents('.env.caravel', "CARAVEL_MERCHANT={$data['merchant_id']}\nCARAVEL_SECRET={$data['secret']}\n");
chmod('.env.caravel', 0600);
echo "Connected as {$data['merchant_id']}\n";
```

*POST /v1/connect · Python*

```py
# Python 3.8+ with requests (pip install requests). Run once, on your server.
import os
import requests

res = requests.post("https://api.caravelpay.app/v1/connect", timeout=15, json={
    "site_url": "https://shop.example",
    "payout_address": "0x52908400098527886E0F7030069857D2E4169EE7",
    "email": "you@shop.example",  # optional: service notices only
    "platform": "api",
})
data = res.json()
if res.status_code != 201:
    raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")

# The secret is in this response only. Save it outside your web root.
fd = os.open(".env.caravel", os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w") as f:
    f.write(f"CARAVEL_MERCHANT={data['merchant_id']}\nCARAVEL_SECRET={data['secret']}\n")
print(f"Connected as {data['merchant_id']}")
```

*201 Created*

```json
{
  "merchant_id": "mch_7Q2KZX4PRB",
  "secret": "9b2e7c4a1f6d8e3b5a0c9d2f7e4b1a6c3d8f5e2b9a7c4d1e6f3b8a5c2d9e7f4a",
  "status": "active",
  "fee_rate": "2%"
}
```

> **The secret is shown once.** Keep it on your server, in an environment variable or a secrets store, and never in browser or app code. Lost it? [Get new keys.](https://caravelpay.app/docs/connect.html#reconnect)

That's it. Put the two values in `CARAVEL_MERCHANT` and `CARAVEL_SECRET` and [create your first checkout](https://caravelpay.app/docs/#create). You pay 2% Caravel fee and nothing else.

## What site_url does

`site_url` decides where your checkouts may send buyers and webhooks. Your `return_url`, `cancel_url` and `callback_url` must be HTTPS on that site, with or without `www`:

| site_url | Allowed URLs start with |
|---|---|
| `https://shop.example` | `https://shop.example/` or `https://www.shop.example/` |

Buyers never see it. The hosted checkout shows Caravel Pay, the amount and the ways to pay, and no store name, logo or address.

## Your payout wallet

- Use an ordinary Polygon wallet that you control, such as MetaMask, Rabby or a hardware wallet. Contracts, token addresses and exchange deposit contracts are refused.
- Payouts arrive in the token the buyer paid with, never swapped. Card payments arrive as USDC.
- One wallet can receive payouts for several sites.

[Payouts and fees](https://caravelpay.app/docs/payouts.html) has the details.

## Change your wallet or secret

Call `POST /v1/connect` again with the new details. Each call creates a new merchant with its own id and secret; existing keys are never sent out again. Then:

1. **Switch new checkouts to the new keys.** Their payouts go to the wallet you just sent.
2. **Keep the old keys for a day.** Checkouts you already created finish on the old merchant: they pay out to the old wallet, and their webhooks are signed with the old secret. Webhooks are retried for up to 24 hours, so keep the old secret at least 24 hours after your last checkout on it.

Every webhook carries `Caravel-Merchant`, so verify each one with the secret that belongs to that merchant:

*Two sets of keys during a switch*

```js
const secrets = {
  [process.env.CARAVEL_MERCHANT]: process.env.CARAVEL_SECRET,         // new keys
  [process.env.CARAVEL_OLD_MERCHANT]: process.env.CARAVEL_OLD_SECRET, // until the old checkouts finish
};
const secret = secrets[req.get("Caravel-Merchant")];
if (!secret) return res.sendStatus(401);
```

If an old secret may have leaked, get new keys the same way and email [hello@caravelpay.app](mailto:hello@caravelpay.app) with the old merchant id so we can pause it.

The WooCommerce plugin does all of this for you when you save a new wallet or click **Reconnect**.

## Status

`status` is `active`: you can create checkouts right away. If it is `pending_review`, checkouts open once the merchant is approved. [Merchant status](https://caravelpay.app/docs/merchant.html#status) lists every value.

## Errors

| Code | Status | What to do |
|---|---|---|
| `invalid_request` | 400 | A field is missing or unknown, or the payout wallet can't receive payouts. `param` names the field. |
| `invalid_url` | 400 | Send `site_url` as a plain HTTPS address, like `https://shop.example`. |
| `limit_exceeded` | 409 | This wallet already receives payouts for the maximum number of merchants. Use another wallet. |
| `rate_limited` | 429 | Too many calls from your server. Wait `Retry-After` seconds. |
| `service_unavailable` | 503 | New merchants are briefly paused. Retry after `Retry-After` seconds. |

All codes are on the [Errors](https://caravelpay.app/docs/errors.html) page.
