API reference
Authentication and request signing
Every API request carries your merchant id, a timestamp and an HMAC-SHA256 signature made with your secret. Webhooks we send you are signed the same way. The one unsigned call is POST /v1/connect, which gives you the keys.
Credentials#
| Credential | Looks like | Where it lives |
|---|---|---|
| Merchant id | mch_7Q2KZX4PRB | Sent in a header with every request. Not secret. |
| Secret | 64 hex characters | Your server only. Never in browser, app or plugin code that buyers can see. |
You get both from one call to POST /v1/connect: see Get your API keys. The WooCommerce plugin makes that call and stores them for you. Use the secret as a plain string: don't hex-decode it.
If your secret leaks or gets lost, get new keys with the same call and switch to them. Email [email protected] with the old merchant id if it should be paused.
Headers#
| Header | Value |
|---|---|
Caravel-Merchant | Your merchant id. |
Caravel-Timestamp | Current time in unix seconds, for example 1794233052. Must be within 300 seconds of our clock. |
Caravel-Signature | Lowercase hex HMAC-SHA256 of the string to sign, keyed with your secret. |
Content-Type | application/json on requests with a body. |
Build the signature#
Join four values with a single newline (\n) between each, with no newline at the end:
${timestamp}\n${METHOD}\n${path}\n${rawBody}
- timestamp: the exact value you send in
Caravel-Timestamp. - METHOD: uppercase,
POSTorGET. - path: the path without scheme or host, such as
/v1/checkoutsor/v1/checkouts/chk_8Qe2TnV4kR. Include the query string if you send one, exactly as sent:/v1/checkouts?limit=20. - rawBody: the exact bytes you send as the body. Use an empty string for
GET.
Then compute HMAC-SHA256 of that string with your secret as the key, and send it as lowercase hex.
TS=$(date +%s)
BODY='{"order_id":"1042","amount":"136.41","currency":"USD"}'
SIG=$(printf '%s\n%s\n%s\n%s' "$TS" POST /v1/checkouts "$BODY" \
| openssl dgst -sha256 -hmac "$CARAVEL_SECRET" | awk '{print $NF}')
echo "$SIG"import crypto from "node:crypto";
function sign(secret, timestamp, method, path, rawBody = "") {
return crypto
.createHmac("sha256", secret)
.update(`${timestamp}\n${method}\n${path}\n${rawBody}`)
.digest("hex");
}<?php
function caravel_sign(string $secret, string $timestamp, string $method, string $path, string $rawBody = ''): string
{
return hash_hmac('sha256', "$timestamp\n$method\n$path\n$rawBody", $secret);
}import hashlib, hmac
def sign(secret: str, timestamp: str, method: str, path: str, raw_body: bytes = b"") -> str:
message = f"{timestamp}\n{method}\n{path}\n".encode() + raw_body
return hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()The Quickstart has a complete request helper in each language that signs and sends in one call.
Check your implementation#
Sign these example values. If your code prints the signature below, it matches ours. (This secret is only an example; it doesn't work against the API.)
| Secret | 9b2e7c4a1f6d8e3b5a0c9d2f7e4b1a6c3d8f5e2b9a7c4d1e6f3b8a5c2d9e7f4a |
|---|---|
| Timestamp | 1794233052 |
| Method | POST |
| Path | /v1/checkouts |
| Body | {"order_id":"1042","amount":"136.41","currency":"USD","return_url":"https://shop.example/thanks","cancel_url":"https://shop.example/cart","callback_url":"https://shop.example/webhooks/caravel"} |
| Signature | 34fd1584ba0f542dd68864a0a4c77df7c2a8532a4b254b4e865ad98e78ee2c4c |
Timestamps and clock skew#
Requests whose Caravel-Timestamp is more than 300 seconds away from our clock are rejected with timestamp_expired. This stops an old, captured request from being replayed. Keep your server clock synced with NTP and create the timestamp right before you send.
Common mistakes#
- Signing one body and sending another. Serialise the JSON once, sign that string, and send the same string. Don't let your HTTP client re-encode it.
- Signing the full URL. Sign
/v1/checkouts, nothttps://api.caravelpay.app/v1/checkouts. - Lowercase method or uppercase hex. Method is uppercase; the signature is lowercase hex.
- A trailing newline.
echoadds one;printf '%s'doesn't.
A request that fails any of these checks gets 401 with the code invalid_signature. See Errors.