Get started
Get your API keys
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#
/v1/connectThis 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_urlstringrequired | Your site's HTTPS address, such as https://shop.example. It sets where your return, cancel and callback URLs may point. See below. |
payout_addressstringrequired | The Polygon wallet that receives your payouts, starting with 0x. See your payout wallet. |
platformstringrequired | "api". (The WooCommerce plugin sends "woocommerce".) |
emailstringoptional | Where we send service notices about this merchant, if ever needed. |
curl https://api.caravelpay.app/v1/connect \
-H "Content-Type: application/json" \
--data-binary '{
"site_url": "https://shop.example",
"payout_address": "0x52908400098527886E0F7030069857D2E4169EE7",
"email": "[email protected]",
"platform": "api"
}'// 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: "[email protected]", // 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}`);<?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' => '[email protected]', // 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";# 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": "[email protected]", # 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']}"){
"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.
That's it. Put the two values in CARAVEL_MERCHANT and CARAVEL_SECRET and create your first checkout. 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 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:
Switch new checkouts to the new keys. Their payouts go to the wallet you just sent.
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:
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 [email protected] 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 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 page.