Caravel Pay Docs

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#

CredentialLooks likeWhere it lives
Merchant idmch_7Q2KZX4PRBSent in a header with every request. Not secret.
Secret64 hex charactersYour 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 hello@caravelpay.app with the old merchant id if it should be paused.

Headers#

HeaderValue
Caravel-MerchantYour merchant id.
Caravel-TimestampCurrent time in unix seconds, for example 1794233052. Must be within 300 seconds of our clock.
Caravel-SignatureLowercase hex HMAC-SHA256 of the string to sign, keyed with your secret.
Content-Typeapplication/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:

String to sign
${timestamp}\n${METHOD}\n${path}\n${rawBody}
  • timestamp: the exact value you send in Caravel-Timestamp.
  • METHOD: uppercase, POST or GET.
  • path: the path without scheme or host, such as /v1/checkouts or /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.

Sign a request
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"

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.)

Secret9b2e7c4a1f6d8e3b5a0c9d2f7e4b1a6c3d8f5e2b9a7c4d1e6f3b8a5c2d9e7f4a
Timestamp1794233052
MethodPOST
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"}
Signature34fd1584ba0f542dd68864a0a4c77df7c2a8532a4b254b4e865ad98e78ee2c4c

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, not https://api.caravelpay.app/v1/checkouts.
  • Lowercase method or uppercase hex. Method is uppercase; the signature is lowercase hex.
  • A trailing newline. echo adds one; printf '%s' doesn't.

A request that fails any of these checks gets 401 with the code invalid_signature. See Errors.