Download the PHP package sparkcrm/spark-sdk without Composer
On this page you can find all versions of the php package sparkcrm/spark-sdk. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Informations about the package spark-sdk
SparkCRM Checkout SDK (PHP)
A standalone PHP SDK for the SparkCRM DTC Checkout API. It takes you from
init() → lead → order → upsell → complete, plus tax quotes, decline salvage,
refunds, and the PayPal redirect/capture flow — without you ever touching the
raw HTTP API.
The base URL is fixed to https://api.sparkcrm.io.
- PHP 8.1+
- PSR-18 / PSR-17 HTTP (no Guzzle or Laravel hard dependency)
- A normalized result object (declines are results, not exceptions) and typed exceptions for real failures
- Tracking auto-capture + param normalization, card-expiry normalization, retry/backoff
- Pluggable state store that survives the PayPal redirect (the default keeps state in the PHP session — call
session_start()first)
Install
With Composer (recommended)
The SDK speaks HTTP through any PSR-18 client. Unless your app already ships one, you must also install an implementation + PSR-7 factories — they are then auto-discovered (the first real API call fails without them):
Download a ready-to-use bundle (no Composer)
Don't use Composer? Each GitHub Release
ships a spark-sdk-<version>.zip with everything already included — the SDK, its
dependencies, and a working HTTP client. Download it, unzip it into your
project, and include the bundled autoloader:
That's it — no Composer, no extra HTTP-client install.
From source without Packagist
Want to vendor a specific copy, or it isn't on Packagist yet? Point Composer at the repo, or clone it and let Composer build the autoloader:
Dropping
src/in on its own is not enough: the SDK needs a few PSR packages (psr/http-*,php-http/discovery,psr/log,psr/simple-cache) plus a PSR-18 client at runtime. The downloadable bundle and both Composer paths above all bring those along for you.
Before you start — read this once
Four things silently break a first integration. Get these right and the rest just works:
- API token. Pass your SparkCRM DTC API token to the constructor. It is sent as
a Bearer token; a missing/invalid one throws
AuthExceptionon the first call. - Start the session. The default store keeps funnel state in the PHP session, so
your app must call
session_start()before any SDK call — otherwise nothing persists between requests and the order is silently lost. (Or pick another state store.) - One fresh client per request. The SDK is stateless between requests: build a new
SparkCheckouton each HTTP request — it recalls the buyer's order from the store (session/cookie/cache), so later requests continue the same funnel automatically. - Production base URL. The base URL is fixed to
https://api.sparkcrm.io— calls charge real money. Use a test campaign / your gateway's test cards while wiring up.
Quick start
A checkout spans two separate HTTP requests — the landing-page load and the checkout submit. Build a fresh client in each; state carries over via the store.
Need a live tax quote before charging? See Tax, subscriptions, refunds, search — it is never auto-called.
init() — call once on page load
init() does two things and stores the result for the rest of the funnel:
- Captures tracking + device context from the incoming request/URL:
ip_address,user_agent,utm_*,affiliate,sub1–sub5, and any other query params — all normalized automatically (see Param normalization). - Opens a session (
POST /checkout/sessions) with that context + the campaign id, storing the returnedsession_id.
Everything captured here is auto-merged into createLead() / createOrder() so
you never re-pass ip/user_agent/utm/affiliate.
campaign is an integer and is required. currency defaults to USD.
Browser requests
Pass the incoming PSR-7 ServerRequest so the SDK can read the query string,
client IP and User-Agent. If you don't pass one, it falls back to $_GET /
$_SERVER.
Behind a proxy or load balancer
Out of the box the SDK reads the real customer IP from the forwarded headers
(X-Forwarded-For → X-Real-IP → CF-Connecting-IP), falling back to
REMOTE_ADDR for direct connections — so a checkout behind a CDN or load
balancer records the customer's IP, not the proxy's, with no configuration.
To harden against header spoofing in a known topology, list your trusted proxies explicitly; forwarded headers are then honored only when the connection comes from one of them, and the client is the right-most non-proxy hop:
Pure server-to-server
Callers with no browser request can pass ip_address / user_agent /
affiliate explicitly; auto-capture is then skipped.
Choosing a state store
The SDK remembers the buyer context + session_id and the live
order_number / transaction_number between requests. Pick the store that
matches your topology:
| Store | Use when | Notes |
|---|---|---|
SessionStore (default) |
single server | the session cookie is the correlation key |
CacheStore (PSR-16) |
multi-server / load-balanced | keyed by a small opaque cookie (e.g. spark_ref); any node recalls state |
SignedCookieStore |
stateless nodes, no session/cache | authenticated-encrypted cookie (libsodium, requires ext-sodium); contents are confidential and tamper-evident |
ArrayStore |
pure server-to-server, one process | does not survive across requests |
LayeredStore |
want a fallback chain | e.g. session → cache → signed cookie |
PayPal note: the correlation cookie is set
Secure; HttpOnly; SameSite=Lax.Lax(notStrict) is required so the cookie is still sent when the customer returns from the PayPal redirect (a cross-site top-level navigation).
With the default SessionStore, the correlation key is the PHP session cookie.
The SDK hardens it to Secure; HttpOnly; SameSite=Lax on construction (a no-op
once the session has started — construct the client before session_start()).
For local HTTP development, disable the Secure flag:
createOrder() — one smart method
createOrder() picks the endpoint based on SDK state:
- A lead exists (
order_numberstored) →POST /checkout/orders/payment - No lead →
POST /checkout/orders(full order)
The full-order path additionally requires customer.first_name +
customer.last_name and attaches ip / user_agent / utm / affiliate / products /
campaign to the call. After it returns, the state is identical for both paths,
so processUpsell() / completeOrder() are unchanged.
Need to force a full order even when a lead exists? Use createFullOrder().
Reading the result
A decline is a result, not an exception:
Decline salvage
When the per-decline retry cap is reached
(max_reprocess_attempts_per_decline / max_upsell_declines), the API returns
a declined result, not an exception — ->isDeclined() is true and
->isCapped() tells you the cap (rather than the gateway) ended the retries, so
you can stop retrying and message the customer.
reprocessPayment()charges the order already in the store and injects thatorder_numberfor you — but it does not check one exists. If the funnel state was lost (a fresh client with noresume()/init()), it sends a null order and the API rejects the call. Run it on the same funnel wherecreateOrder()ran.
PayPal (redirect + capture)
The initial PayPal order is a two-phase flow; upsells are seamless.
If the customer cancels instead, they come back to your cancel_url — the SDK
appends ?spark_canceled=1 to it so you can detect and clean up the abandoned
order. The order is simply left uncaptured.
How spark_order is appended
A PayPal return_url can only carry ?spark_order=<order> if the order exists
before the redirect. createOrder() guarantees that:
- Lead path (
createLead()thencreateOrder()): the order already exists, sospark_orderis appended directly. - Full-order path (no prior lead) with
paypal_wallet:createOrder()transparently creates a lead first to mint the order, then processes the PayPal payment on it — sospark_orderis appended here too. This costs one extraPOST /checkout/leadscall and creates a real lead (subject to the usual throttle/dedupe).
On top of the URL marker, the persistent store also carries the order across the
redirect via the first-party SameSite=Lax cookie, so resume() recovers it
even if the marker is lost.
createFullOrder()is the explicit escape hatch that always hits/checkout/ordersand never creates a lead; it therefore does not appendspark_order. Use it only when you don't need redirect-marker survival.
Tax, subscriptions, refunds, search
Param normalization
init() reads every query param off the landing-page URL and maps it to what
Spark accepts. Matching is case-insensitive and ignores separators
(- _ space), so affId, aff_id, AFFID, Aff-Id all resolve to the same
target.
- Affiliate ←
affiliate, affiliate_id, affId, affid, aff_id, aff, ref, a(first present wins) - Sub-affiliates
sub1–sub5←sub{n}, sub_{n}, c{n}, subaff{n}, sub_affiliate_id[_{n}] - UTMs (
utm_source/medium/campaign/term/content) pass through unchanged cohortpasses through unchanged (a first-class Spark field, not a custom field)- Everything else →
custom_fields, capped to the API's limits (max 50 keys, keys sanitized to[a-zA-Z0-9_-]{1,64}, values truncated to 1000 chars). Pass a PSR-3 logger viaConfig(logger: $logger)to get a warning when keys are dropped over the cap.
Extend the alias map for edge cases via Config:
Card expiry is normalized automatically: 12/26, 12/2026, or split
card_exp_month + card_exp_year all become a canonical card_exp of
MM/YYYY (set Config(splitCardExpiry: true) to emit the split fields instead).
Malformed input — an unparseable card_exp, a month outside 01–12, or a year
that isn't 2 or 4 digits — throws ValidationException locally, before any
request is sent (not a server-side 422, not a declined result).
payment.method is validated against the supported set
(card, onfile, external, paypal_wallet); an unknown method throws a
ValidationException before any request is sent.
Errors: results vs exceptions
| Outcome | How it surfaces |
|---|---|
| Decline (gateway or pre-gateway) | OrderResult::isDeclined() |
| Duplicate order | OrderResult::isDuplicate() |
| PayPal redirect needed | OrderResult::needsRedirect() |
| Upsell reprocess already running (409) | OrderResult::inProgress() |
| 401 invalid/missing token | AuthException |
| 403 token lacks ability | PermissionException |
| 404 not found | NotFoundException |
422 field validation (has an errors map) |
ValidationException (->errors()) |
| 422 processing failure (no field errors, e.g. a failed capture/refund) | a non-approved OrderResult — inspect ->message() (not thrown) |
| 429 rate limited (after retries) | RateLimitException (->retryAfterMs()) |
| 402 account past due | AccountSuspendedException |
| 5xx / transport (after retries) | ApiException |
All exceptions extend SparkCheckout\Exception\SparkException and expose
->httpStatus() and ->raw(). Requests are retried with exponential backoff on
429 and 5xx (honoring the X-RateLimit-Reset delay).
Test mode
Enable test mode to tag every charge/session request as a test. Whether a tagged request actually routes to a sandbox gateway is governed server-side by the campaign's gateway configuration — the SDK only marks the request.
Laravel (optional)
The package ships an optional Laravel bridge. The service provider is auto-discovered — no manual registration needed. Set your token and (optionally) publish the config:
Resolve the client from the container or use the Spark facade. The store is
backed by the Laravel cache, keyed per visitor (the session id when a session is
started, otherwise an opaque spark_ref cookie); bindings are scoped, so they
reset cleanly per request under Octane.
The Laravel bridge requires illuminate/contracts (already present in any
Laravel app); the core SDK has no framework dependency.
License
MIT.
All versions of spark-sdk with dependencies
ext-sodium Version *
psr/http-client Version ^1.0
psr/http-factory Version ^1.0
psr/http-message Version ^1.1 || ^2.0
php-http/discovery Version ^1.19
psr/log Version ^1.1 || ^2.0 || ^3.0
psr/simple-cache Version ^2.0 || ^3.0