Search by

sendclipper / sendclipper-php

sendclipper

Official PHP SDK for the SendClipper email API: send mail, manage domains, templates, sandboxes, webhooks, contacts and suppressions.

v0.1.0 2026-10-16 20:28 UTC

This package is auto-updated.

Last update: 2026-10-16 20:36:09 UTC


README

The official PHP library for the SendClipper email API.

composer require sendclipper/sendclipper-php

PHP 8.1 or newer, with the curl and json extensions. No other dependencies.

Setup

Create an API key in the dashboard under API keys.

use SendClipper\SendClipper;

$sc = new SendClipper('sc_live_xxxxxxxxxxxxxxxx');
// or: new SendClipper()  reads SENDCLIPPER_API_KEY

Send an email

[$data, $error] = $sc->emails->send([
    'from' => 'hello@send.acme.io',
    'to' => 'ada@lumen.io',
    'subject' => 'Your receipt',
    'html' => '<p>Thanks for your order.</p>',
    'text' => 'Thanks for your order.',
]);

if ($error) {
    echo $error->name, ': ', $error->message; // e.g. domain_not_verified
} else {
    echo "Email {$data['id']} accepted";
}

Parameters and responses are plain arrays whose keys are the API's own field names (templateId, sendClass, ...), exactly as the API reference writes them.

from must be a bare address on a domain you verified in SendClipper. Each message has one recipient in to; for several, use batch->send.

Send with a template

Design and publish a template in the dashboard, then send it by id or by its send name:

$sc->emails->send([
    'from' => 'hello@send.acme.io',
    'to' => 'ada@lumen.io',
    'templateId' => 'tpl_9f2b7c10ad42', // or 'template' => 'welcome'
    'variables' => ['first_name' => 'Ada'],
]);

A tag with no value and no {{tag|fallback}} is refused by name (template_variables_missing), never sent blank.

Send a batch

Up to 500 messages in one request. Each item is accepted or refused on its own:

[$data] = $sc->batch->send([
    ['from' => 'hello@send.acme.io', 'to' => 'ada@lumen.io', 'subject' => 'Hi Ada', 'html' => '<p>Hi</p>'],
    ['from' => 'hello@send.acme.io', 'to' => 'alan@lumen.io', 'templateId' => 'tpl_9f2b7c10ad42', 'variables' => ['first_name' => 'Alan']],
]);

foreach ($data['data'] as $item) {
    if ($item['status'] === 'failed') {
        echo $item['index'], ' ', $item['error']['code'], PHP_EOL;
    }
}

Errors

API methods never throw for an API refusal or a network failure. They return a SendClipper\Result with data, error and headers, and exactly one of data and error is set. A Result destructures to [$data, $error]:

$result = $sc->domains->list();
$result->data;     // the decoded response, or null
$result->error;    // a SendClipper\ErrorResponse, or null
$result->headers;  // response headers (lower-case names), or null when no answer arrived
$result->ok();     // error === null

$domains = $sc->domains->list()->unwrap(); // data, or throws SendClipper\Exception\ApiException

ErrorResponse has name (SendClipper's code: validation_failed, rate_limited, network_error, ...), message (a sentence for a person), statusCode (null when no answer arrived) and detail (code-specific facts).

The iterators (emails->iterate() and friends) throw an ApiException instead, since a loop has no error to hand back. $e->apiCode is the API's code and $e->status the HTTP status.

Retries and idempotency

Network errors, 429 and 5xx answers are retried twice with backoff, honouring Retry-After. Every JSON write carries an Idempotency-Key, and the API answers a repeated key with the first result for 24 hours. A retried send therefore never goes out twice.

To make your own retries safe as well (a queued job that runs again after a crash), pass a key that names the logical operation:

$sc->emails->send($params, ['idempotencyKey' => "order-{$order->id}-receipt"]);

Request options

Every method takes request options as its last argument:

$sc->emails->list(['status' => 'bounced'], ['timeout' => 5, 'headers' => ['X-Trace-Id' => $traceId]]);
Option Meaning
idempotencyKey Your own key for a write
timeout Per-attempt timeout in seconds (default 30)
maxRetries Retries for this call (default 2)
headers Extra request headers

Client options: new SendClipper($key, ['baseUrl' => ..., 'timeout' => ..., 'maxRetries' => ..., 'defaultHeaders' => [...], 'transport' => ...]). baseUrl also reads SENDCLIPPER_BASE_URL. transport replaces the curl sender (a proxy, a test double): implement SendClipper\Transport\Transport.

Verify webhooks

SendClipper signs each delivery. Verify it against the raw body, before decoding:

use SendClipper\Exception\WebhookVerificationException;
use SendClipper\Webhook;

try {
    $event = Webhook::verify(
        file_get_contents('php://input'), // the raw body, not json_decode()d
        getallheaders(),
        (string) getenv('SENDCLIPPER_WEBHOOK_SECRET'),
    );
} catch (WebhookVerificationException $e) {
    http_response_code(400);
    exit($e->reason);
}
// $event['id'] is stable across retries: deduplicate on it
error_log($event['event'] . ' ' . $event['data']['to']);
http_response_code(200);

In Laravel or Symfony pass $request->getContent() and $request->headers; a PSR-7 request works as the headers argument too. During a secret rotation a delivery carries two signatures and either one is accepted. A delivery signed more than five minutes away from now is refused. $sc->webhooks->verify(...) is the same function.

To test your handler locally, Webhook::signatureHeader($secret, $rawBody) builds a valid header.

Sandboxes

A sandbox traps mail while you develop: nothing is delivered.

$box = $sc->sandboxes->create(['name' => 'my-app-dev'])->unwrap();
$creds = $sc->sandboxes->credentials($box['id'])->unwrap(); // SMTP host, port, username, password

// Or send through the sandbox's JSON door with its own token, using the same fields as emails->send():
$token = $sc->sandboxes->resetCredentials($box['id'])->unwrap();
$sc->sandboxes->send($token['apiToken'], ['from' => 'app@example.com', 'to' => 'test@example.com', 'subject' => 'Hi', 'html' => '<p>Hi</p>']);

// In a test: wait for the message to arrive
[$msg, $error] = $sc->sandboxes->messages->waitFor($box['id'], fn (array $m) => $m['subject'] === 'Hi');

Everything else

Resource Methods
emails send, get, list, iterate, stats
batch send
domains list, create, records, verify, enableReceiving, disableReceiving, setRegion, deletionPreview, remove
templates list, get, html, create, update, remove, duplicate, publish, unpublish, preview, setTags, projects
templates->tags list, create, update, remove
templates->assets list, upload
sandboxes list, get, create, update, remove, credentials, resetCredentials, send
sandboxes->messages list, get, raw, remove, clear, markAllRead, waitFor
webhooks list, create, update, remove, rotateSecret, deliveries, verify
webhooks->dispatches list, iterate, get, replay, replayAll
contacts list, iterate, create, import, importPreview, export
audiences list
suppressions list, iterate, create, remove, export
mailboxes list, get, create, update, archive
mailboxes->messages list, iterate, get, attachment
automations enroll
workspace get
apiKeys list

contacts->import() takes the file's contents (file_get_contents('contacts.csv')), and templates->assets->upload() the image's bytes. Management routes need a key with Full access. A sending-only key can call emails->send and batch->send.

Full API reference: https://sendclipper.com/docs.

License

MIT