# PHP SDK

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

`caravelpay/caravel-php` signs every request, verifies webhooks in constant time and checks every response before you use it. It runs on PHP 7.4 to 8.5 with only `ext-curl` and `ext-json`.

## Install

Download `caravel-php-1.0.0.zip`, unzip it into your project and load its autoloader. The SDK needs nothing else.

*Load the SDK*

```php
<?php
require 'caravel-php/autoload.php';
```

> **Available at launch.** The downloads on this page open when Caravel Pay launches.

## Get API keys

Run this once, then keep the merchant id and secret in your server's environment. **Every call creates a new merchant**, so to change your wallet or rotate the secret you connect again. See [Get your API keys](https://caravelpay.app/docs/connect.html).

*Connect once*

```php
<?php
require 'caravel-php/autoload.php';

$keys = CaravelPay\Client::connect([
    'site_url' => 'https://shop.example',
    'payout_address' => '0x52908400098527886E0F7030069857D2E4169EE7',
    'email' => 'you@shop.example', // optional
]);
// ['merchant_id' => 'mch_…', 'secret' => '…', 'status' => 'active', 'fee_rate' => '2%']
```

## Create a checkout

*Create a checkout and redirect*

```php
<?php
use CaravelPay\Amount;
use CaravelPay\Client;

$caravel = new Client(getenv('CARAVEL_MERCHANT_ID'), getenv('CARAVEL_SECRET'), ['max_retries' => 2]);

$checkout = $caravel->createCheckout([
    'order_id' => '1042',
    'amount' => Amount::normalize($order->total), // "136.41"
    'customer_email' => 'buyer@example.com',      // optional
    'return_url' => 'https://shop.example/thanks',
    'cancel_url' => 'https://shop.example/cart',
    'callback_url' => 'https://shop.example/webhooks/caravel',
]);
header('Location: ' . $checkout['checkout_url'], true, 303);
```

The same `order_id` with the same details returns the same checkout. A timeout or `5xx` can be retried safely, and with `max_retries` the SDK does it for you.

## Handle the webhook

*webhooks/caravel.php*

```php
<?php
use CaravelPay\Checkout;
use CaravelPay\Exception\SignatureVerificationException;
use CaravelPay\Webhook;

try {
    // One secret, or [merchant_id => secret] while old keys are still in use.
    $event = Webhook::verifyCurrentRequest(getenv('CARAVEL_SECRET'), 'https://shop.example/webhooks/caravel');
} catch (SignatureVerificationException $e) {
    http_response_code($e->getHttpStatus()); // 401, or 400 for a malformed event
    exit;
}
if (already_handled($event->id)) {
    http_response_code(200);
    exit;
}
// Re-read the checkout: the webhook says what to look at, the signed API says what is true.
$checkout = $caravel->getCheckout($event->checkoutId());
if (Checkout::isPaid($checkout)) {
    mark_paid($checkout['order_id'], Checkout::txHash($checkout));
}
remember($event->id);
http_response_code(200);
```

`Webhook::verify()` checks the timestamp is within 300 seconds and compares the HMAC over `POST`, the callback path with its query string, and the raw body in constant time. It then checks the event shape. Details: [Verify the signature](https://caravelpay.app/docs/webhooks.html#verify).

## Reference

| Method | Endpoint |
|---|---|
| `Client::connect($params)` | `POST /v1/connect`, unsigned, never retried |
| `$client->createCheckout($params, ['idempotency_key' => …])` | `POST /v1/checkouts` |
| `$client->getCheckout($id)` | `GET /v1/checkouts/{id}` |
| `$client->listCheckouts(['limit' => 50, 'cursor' => …])`, `allCheckouts()` | `GET /v1/checkouts` |
| `$client->getMerchant()` | `GET /v1/merchant` |
| `$client->createPaymentLink($params)`, `Client::createPaymentLinkWithoutKeys($params)` | `POST /v1/payment-links`, signed or without keys |
| `$client->getPaymentLink($id)`, `deactivatePaymentLink($id)` | `GET /v1/payment-links/{id}`, `POST …/deactivate` |
| `Webhook::verify($body, $headers, $secrets, $callbackUrl)` | Verifies a webhook and returns an `Event` |
| `Signature::compute()`, `Signature::signedPath()` | The signing rules, if you need them directly |

Errors are exceptions:

- `ApiException` carries `getErrorCode()`, `getHttpStatus()`, `getParam()`, `getRetryAfter()` and `isRetryable()`. The codes are listed under [Errors](https://caravelpay.app/docs/errors.html).
- `NetworkException` means no response arrived.
- `UnexpectedResponseException` means a malformed 2xx answer, which is never treated as paid.

To use your platform's HTTP client, implement `CaravelPay\Http\TransportInterface`.

## Coming from PayGate's PHP SDK

PayGate's SDK builds a payment URL from a wallet-forwarding address. With Caravel Pay you create a checkout per order, redirect to its `checkout_url`, verify the signed webhook and re-read the checkout before marking the order paid. A short payment comes back as `underpaid`, so there is no tolerance setting.
