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 publishable wk_ key — never send a secret mch_ 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):

  1. Protection reference number — the CLM-YYYY-XXXXXXXX value returned in the confirm response (Crockford Base32 format, regex ^CLM-\d{4}-[0-9A-HJKMNP-TV-Z]{8}$).

  2. "Not insurance" disclosure — include this exact phrase verbatim:

    This is not insurance. This is a contractual refund guarantee provided by Claimful Inc.

  3. 48-hour free-look cancellation notice — inform the consumer they may request a full fee refund within 48 hours of purchase by visiting claimful.claims with their protection reference number.

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