PHP SDK Reference
Authoritative reference for the official Claimful PHP SDK (claimful/sdk,
source at packages/sdk-php). Pair this document with the
SDK semantic-versioning covenant — breaking-change rules and the
90-day deprecation-overlap window live there.
Document marker: CLAIMFUL_SDK_PHP_SENTINEL
1. Install
composer require claimful/sdk
Requirements (packages/sdk-php/composer.json):
- PHP
^8.4. guzzlehttp/guzzle ^7.9(default HTTP client; any PSR-18 client works).guzzlehttp/psr7 ^2.8,psr/http-client ^1.0,psr/http-message ^2.0,psr/log ^3.0.- A merchant secret API key for server-side calls (
mch_live_.../mch_test_...). The browser widget uses a separate publishablewk_key — never send a secretmch_key from the browser.
PSR-4 root: Claimful\Sdk\ → src/.
2. Client construction
use Claimful\Sdk\Client;
$client = new Client(
apiKey: getenv('CLAIMFUL_API_KEY'),
http: null, // optional PSR-18 client
baseUri: 'https://api.claimful.ai/api/v1', // optional override
sleep: null, // optional milliseconds sleeper
retry: [
'baseDelayMs' => 250,
'jitter' => true,
'maxAttempts' => 3,
'multiplier' => 2.0,
'retryAfterCeilingSeconds' => 600,
],
);
Construction throws ClaimfulValidationException for an empty apiKey.
Idempotency keys auto-inject on POST/PUT/PATCH/DELETE; a caller-supplied
key MUST match ^[A-Za-z0-9_-]{16,255}$.
Idempotency contract (Table 34 / ADR 0018):
- Keys and their cached responses are valid for 24 hours. Within that window, resending the same key replays the original response with no side-effects.
- Reusing the same key with a different request body returns
409 idempotency_conflict. Generate a new key for a distinct request. - A missing or invalid key returns
400 idempotency_key_required. The SDK auto-injects UUIDv4 keys so raw-HTTP callers are the primary audience for this error.
Retries fire on HTTP 408, 429, 500, 502, 503, 504. Retry-After is
honoured up to retryAfterCeilingSeconds.
Public properties (all readonly):
| Property | Type | Purpose |
| --- | --- | --- |
| $client->offers | OffersResource | Universal-mode offer endpoints. |
| $client->quotes | OffersResource | Alias kept for legacy callers. |
| $client->claims | ClaimsResource | Read-only claim helpers. |
| $client->refunds | RefundsResource | Refund creation. |
| $client->webhooks | Webhooks\SignatureVerifier | Wire-format verifier. |
| $client->headless | HeadlessResource | Tier-1.5 headless helpers. |
Low-level escape hatches: $client->post($path, $payload, $idempotencyKey),
$client->get($path, $query), $client->postRaw($path, $rawJsonBody, $idempotencyKey).
3. Resources
OffersResource
| Method | HTTP |
| --- | --- |
| quote(array $payload, ?string $idempotencyKey = null): array | POST /offers/quote |
| create(array $payload, ?string $idempotencyKey = null): array | alias of quote() |
| confirm(array $payload, ?string $idempotencyKey = null): array | POST /offers/confirm |
| decline(array $payload, ?string $idempotencyKey = null): array | POST /offers/decline |
Quote payload accepts either camelCase or snake_case keys; the resource
normalises to snake_case on the wire. Required: order_amount_cents,
order_id, protection_program_id. Optional: currency (default 'USD'),
customer_email.
ClaimsResource
| Method | HTTP |
| --- | --- |
| list(array $query = []): array | GET /merchant/claims |
The bare GET /claims path is portal/consumer-side only; the merchant-key
claim list lives under /merchant/claims (FER-189).
RefundsResource
| Method | HTTP |
| --- | --- |
| create(string $orderId, array $payload = [], ?string $idempotencyKey = null): array | POST /orders/{orderId}/refund |
The refund is keyed by $orderId in the path; the payload bag is reserved for
future API versions.
HeadlessResource
Tier-1.5 helpers (ADR 0055 proposed). Bodies are canonicalised via
CanonicalJson::stringify() and sent through Client::postRaw() so the wire
bytes match the PHP API server AND the TypeScript SDK byte-for-byte.
| Method | HTTP |
| --- | --- |
| quote(array $payload, ?string $idempotencyKey = null): array | POST /headless/quote |
| confirm(array $payload, ?string $idempotencyKey = null): array | POST /headless/confirm |
| decline(array $payload, ?string $idempotencyKey = null): array | POST /headless/decline |
Quote payload keys: currency, customer_email / customerEmail,
order_amount_cents / orderAmountCents, order_id / orderId,
product_category / productCategory, protection_program_id /
protectionProgramId. Confirm/decline accept quote_id / quoteId; decline
also accepts an optional reason.
Public constant:
HeadlessResource::DISCLOSURE_PHRASE === 'This is not insurance';
The server response disclosure_text always equals this byte-string. Use the
constant only for byte-equality assertions — do not use it as visible
disclosure copy. Any consumer-facing UI must render the full two-sentence phrase
verbatim:
This is not insurance. This is a contractual refund guarantee provided by Claimful Inc.
4. Webhook verification
use Claimful\Sdk\Webhooks\SignatureVerifier;
$verifier = new SignatureVerifier();
$ok = $verifier->verify(
timestamp: (string) $request->header('X-Claimful-Timestamp'),
body: $request->getContent(),
signature: (string) $request->header('X-Claimful-Signature'),
secret: getenv('CLAIMFUL_WEBHOOK_SECRET'),
tolerance: 300, // optional, default 300 seconds
);
Internally verify() parses the t=<unix>,v1=<hex> header (ADR 0009 §5),
enforces the tolerance window against time(), computes
hash_hmac('sha256', $timestamp.'.'.$body, $secret), and compares each parsed
signature in constant time with hash_equals(). Multiple keyed signatures
(v1, v2, v1n) are accepted — any match passes.
5. Exception hierarchy
\InvalidArgumentException
└── ClaimfulValidationException
\RuntimeException
└── ClaimfulHttpException
└── ClaimfulRateLimitException
| Class | Carries | Thrown when |
| --- | --- | --- |
| ClaimfulValidationException | message only | Empty apiKey, malformed idempotencyKey. |
| ClaimfulHttpException | httpStatus, requestId, idempotencyKey, message, previous | Non-2xx response (not 429) or transport failure. |
| ClaimfulRateLimitException | retryAfter (seconds) + inherited fields | HTTP 429. |
All three live under Claimful\Sdk\Exceptions.
6. End-to-end example
use Claimful\Sdk\Client;
use Claimful\Sdk\HeadlessResource;
use Claimful\Sdk\Exceptions\ClaimfulHttpException;
use Claimful\Sdk\Exceptions\ClaimfulRateLimitException;
use Claimful\Sdk\Exceptions\ClaimfulValidationException;
use Claimful\Sdk\Webhooks\SignatureVerifier;
$client = new Client(apiKey: getenv('CLAIMFUL_API_KEY'));
$offer = $client->offers->quote([
'currency' => 'USD',
'order_amount_cents' => 12_900,
'order_id' => 'order_1234',
'protection_program_id' => 'pp_abc',
]);
try {
// The quote response is camelCase; `quote_token` arrives in the
// `X-Claimful-Quote-Token` response header, never the JSON body. Every
// confirm ALSO requires the versioned, snake_case `order_context` object
// describing the binding order — a missing/malformed context fails closed
// with a 422 `order_context_invalid` before any protection is created.
$client->offers->confirm(
[
'quoteId' => $offer['quoteId'],
'quoteToken' => '<server-token>',
'order_context' => [
'schema_version' => '2026-07-01',
'order' => [
'id' => 'order_1234',
'purchased_at' => '2026-07-10T14:18:00Z',
'currency' => 'USD',
'covered_amount_cents' => 12_900,
],
'participants' => ['mode' => 'unnamed'],
],
],
idempotencyKey: bin2hex(random_bytes(16)),
);
} catch (ClaimfulRateLimitException $e) {
sleep($e->retryAfter ?? 5);
} catch (ClaimfulHttpException $e) {
error_log("claimful http {$e->httpStatus} request={$e->requestId}");
throw $e;
} catch (ClaimfulValidationException $e) {
throw $e;
}
$headless = $client->headless->quote([
'order_amount_cents' => 12_900,
'protection_program_id' => 'pp_abc',
]);
if ($headless['disclosure_text'] !== HeadlessResource::DISCLOSURE_PHRASE) {
throw new RuntimeException('disclosure drift');
}
$ok = (new SignatureVerifier())->verify(
timestamp: (string) $request->header('X-Claimful-Timestamp'),
body: $request->getContent(),
signature: (string) $request->header('X-Claimful-Signature'),
secret: getenv('CLAIMFUL_WEBHOOK_SECRET'),
);
7. Confirmation email requirements [iii-3-8]
After a consumer opts in at checkout, your order confirmation email must include the following four items (contractual requirement — Merchant Agreement §3.2):
-
Protection reference number — the
CLM-YYYY-XXXXXXXXvalue returned in the confirm response (Crockford Base32 format, regex^CLM-\d{4}-[0-9A-HJKMNP-TV-Z]{8}$). -
"Not insurance" disclosure — include this exact phrase verbatim:
This is not insurance. This is a contractual refund guarantee provided by Claimful Inc.
-
48-hour free-look cancellation notice — inform the consumer they may request a full fee refund within 48 hours of purchase by visiting
claimful.claimswith their protection reference number. -
Covered-reasons summary or link — either list the 12 covered reasons or link to the consumer's coverage details page at
claimful.claims.
Note: The claimful.claims coverage deep-link is delivered by Claimful
directly via its own welcome email (welcome_protected). Your confirmation email
does not need to reproduce the tokenized link — a plain claimful.claims
reference is sufficient.
Sample template
Subject: Your order is confirmed + purchase protection is active
Hi [Customer Name],
Thank you for your order. You've added Claimful purchase protection
(reference: CLM-2026-7H2KQ9PM).
If you cannot attend for a covered reason, request a refund at claimful.claims
within 30 days of your event.
Free-look cancellation: You may cancel this protection for a full fee refund
within 48 hours of purchase at claimful.claims.
Disclosure: This is not insurance. This is a contractual refund guarantee
provided by Claimful Inc.
8. Cross-references
- SDK semver covenant
packages/sdk-php/CHANGELOG.md- TypeScript SDK reference