PHPackages                             sparkcrm/spark-sdk - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. sparkcrm/spark-sdk

ActiveLibrary[Payment Processing](/categories/payments)

sparkcrm/spark-sdk
==================

PHP SDK for the SparkCRM DTC Checkout API (init, lead, order, upsell, complete, PayPal, refunds).

v1.0.0(today)00MITPHPPHP ^8.1CI passing

Since Jun 18Pushed todayCompare

[ Source](https://github.com/Spark-Public-SDKs/php-sdk)[ Packagist](https://packagist.org/packages/sparkcrm/spark-sdk)[ RSS](/packages/sparkcrm-spark-sdk/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (12)Versions (2)Used By (0)

SparkCRM Checkout SDK (PHP)
===========================

[](#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
-------

[](#install)

### With Composer (recommended)

[](#with-composer-recommended)

```
composer require sparkcrm/spark-sdk
```

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):

```
composer require nyholm/psr7 symfony/http-client
# or: composer require php-http/guzzle7-adapter guzzlehttp/guzzle
```

### Download a ready-to-use bundle (no Composer)

[](#download-a-ready-to-use-bundle-no-composer)

Don't use Composer? Each [GitHub Release](https://github.com/Spark-Public-SDKs/php-sdk/releases)ships a `spark-sdk-.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:

```
require __DIR__.'/spark-sdk/vendor/autoload.php';

use SparkCheckout\SparkCheckout;

$spark = new SparkCheckout($apiToken);
```

That's it — no Composer, no extra HTTP-client install.

### From source without Packagist

[](#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:

```
# Option 1 — install straight from the Git repo
composer config repositories.spark vcs https://github.com/Spark-Public-SDKs/php-sdk.git
composer require sparkcrm/spark-sdk:dev-main

# Option 2 — clone anywhere, resolve dependencies once, then include its autoloader
git clone https://github.com/Spark-Public-SDKs/php-sdk.git
cd php-sdk && composer install   # creates vendor/ with the autoloader + dependencies
```

```
require '/path/to/spark-sdk/vendor/autoload.php';
```

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

[](#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 `AuthException` on 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](#choosing-a-state-store).)
- **One fresh client per request.** The SDK is stateless between requests: build a new `SparkCheckout` on 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
-----------

[](#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.

```
use SparkCheckout\SparkCheckout;
use SparkCheckout\Exception\SparkException;

$apiToken = getenv('SPARK_CHECKOUT_TOKEN'); // your SparkCRM DTC API token

// ── Request 1: initial landing-page load ──────────────────────────────────
session_start();                        // default store lives in $_SESSION
$spark = new SparkCheckout($apiToken);

// Opens a checkout session AND captures ip, user_agent, utm_*, affiliate,
// sub1-5 and any other query params, then stores session_id + context so every
// later call reuses it automatically.
$spark->init(['campaign' => 1042]);     // integer campaign id (required); currency defaults to USD

// ── Request 2: checkout submit (a SEPARATE request, same browser/session) ──
session_start();
$spark = new SparkCheckout($apiToken);  // fresh client; state recalled from the session

try {
    // Create the lead first. If you skip it, createOrder() below becomes a
    // FULL order and then ALSO requires customer.first_name + last_name.
    $spark->createLead([
        'customer' => ['email' => $email, 'first_name' => $first, 'last_name' => $last],
        'shipping' => ['address' => '...', 'city' => '...', 'state' => 'CA', 'zip' => '90001', 'country' => 'US'],
        'products' => [['offer_id' => 'OFF-1', 'quantity' => 1]],
    ]); // auto-uses the stored session + context

    $result = $spark->createOrder([
        'payment' => ['method' => 'card', 'card_number' => '...', 'card_exp' => '12/26', 'card_cvv' => '123'],
        'billing' => ['address' => '...', 'zip' => '90001', 'country' => 'US'],
    ]); // a lead exists -> charges the existing order (/orders/payment)

    // Branch in this order — declines, redirects and duplicates are RESULTS, not exceptions:
    if ($result->needsRedirect()) {
        header('Location: '.$result->redirectUrl());  // PayPal etc.
        exit;
    } elseif ($result->isDeclined()) {
        echo 'Declined: '.$result->declineCode();
    } elseif ($result->isApproved()) {
        $spark->processUpsell(['upsell_id' => 'UP-1', 'quantity' => 1]); // optional
        $spark->completeOrder();          // fires autoresponders/webhooks, then clears state
        echo 'Approved! Order '.$result->orderNumber();
    }
} catch (SparkException $exception) {     // 401/403/404/422-field/429/402/5xx (see Errors)
    echo 'Checkout error ('.$exception->httpStatus().'): '.$exception->getMessage();
}
```

> Need a live tax quote before charging? See [Tax, subscriptions, refunds, search](#tax-subscriptions-refunds-search) — it is never auto-called.

---

`init()` — call once on page load
---------------------------------

[](#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](#param-normalization)).
- **Opens a session** (`POST /checkout/sessions`) with that context + the campaign id, storing the returned `session_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

[](#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`.

```
$spark = new SparkCheckout($apiToken, request: $psrServerRequest);
$spark->init(['campaign' => 1042]);
```

#### Behind a proxy or load balancer

[](#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:

```
use SparkCheckout\Config;

// Hardened: only trust these proxies' forwarded headers.
$config = new Config($apiToken, trustedProxies: ['10.0.0.0/8', '192.168.1.5']);

// Strict: ignore forwarded headers entirely, use REMOTE_ADDR only.
$config = new Config($apiToken, trustedProxies: []);

// Override the header priority (defaults shown) — e.g. put CF-Connecting-IP first behind Cloudflare:
$config = new Config($apiToken, trustedProxyHeaders: ['X-Forwarded-For', 'X-Real-IP', 'CF-Connecting-IP']);

$spark = new SparkCheckout($apiToken, config: $config, request: $psrServerRequest);
```

### Pure server-to-server

[](#pure-server-to-server)

Callers with no browser request can pass `ip_address` / `user_agent` / `affiliate` explicitly; auto-capture is then skipped.

```
$spark->init([
    'campaign'   => 1042,
    'ip_address' => $buyerIp,
    'user_agent' => $buyerUserAgent,
    'affiliate'  => 'AFF-123',
]);
```

---

Choosing a state store
----------------------

[](#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:

StoreUse whenNotes`SessionStore` *(default)*single serverthe session cookie is the correlation key`CacheStore` (PSR-16)multi-server / load-balancedkeyed by a small opaque cookie (e.g. `spark_ref`); any node recalls state`SignedCookieStore`stateless nodes, no session/cacheauthenticated-encrypted cookie (libsodium, requires `ext-sodium`); contents are confidential and tamper-evident`ArrayStore`pure server-to-server, one processdoes not survive across requests`LayeredStore`want a fallback chaine.g. session → cache → signed cookie```
use SparkCheckout\SparkCheckout;
use SparkCheckout\Store\CacheStore;

$store = new CacheStore($psr16Cache, $correlationToken);
$spark = new SparkCheckout($apiToken, $store);
```

> **PayPal note:** the correlation cookie is set `Secure; HttpOnly; SameSite=Lax`. `Lax` (not `Strict`) 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:

```
use SparkCheckout\Store\SessionStore;

$spark = new SparkCheckout($apiToken, new SessionStore(secure: false));
// or leave the host's session config untouched: new SessionStore(configureCookie: false)
```

---

`createOrder()` — one smart method
----------------------------------

[](#createorder--one-smart-method)

`createOrder()` picks the endpoint based on SDK state:

- **A lead exists** (`order_number` stored) → `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

[](#reading-the-result)

A decline is a **result**, not an exception:

```
$result = $spark->createOrder([...]);

$result->isApproved();        // 2xx + success, not declined/redirect (incl. "processing")
$result->isDeclined();        // gateway decline or pre-gateway decline
$result->declineCode();       // e.g. "05"
$result->isDuplicate();       // full-order dedupe; ->orderNumber() is the existing order
$result->needsRedirect();     // PayPal
$result->redirectUrl();
$result->orderNumber();
$result->transactionNumber();
$result->message();
$result->raw();               // the untouched response body
```

---

Decline salvage
---------------

[](#decline-salvage)

```
// Retry a declined MAIN charge (order_number auto-injected).
// Omit 'payment' to retry the existing method, or pass a fresh card.
if ($result->isDeclined()) {
    $result = $spark->reprocessPayment([
        'payment' => ['method' => 'card', 'card_number' => '...', 'card_cvv' => '...'],
        // optional: 'amount', 'gateway_id', 'affiliate', 'sub1'..'sub5'
    ]);
}

// Retry one declined upsell transaction.
$upsell = $spark->processUpsell(['upsell_id' => 'UP-1', 'quantity' => 1]);
if ($upsell->isDeclined()) {
    $retry = $spark->reprocessUpsell($upsell->transactionNumber());
    if ($retry->inProgress()) {
        // another reprocess for this transaction is already running (409)
    }
}
```

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 that `order_number` for you — but it does not check one exists. If the funnel state was lost (a fresh client with no `resume()` / `init()`), it sends a null order and the API rejects the call. Run it on the same funnel where `createOrder()` ran.

---

PayPal (redirect + capture)
---------------------------

[](#paypal-redirect--capture)

The initial PayPal order is a two-phase flow; upsells are seamless.

```
// Phase 1 — create the order. return_url + cancel_url are required.
$result = $spark->createOrder([
    // The full-order path (and this no-prior-lead paypal_wallet path) require
    // customer first/last. On the lead path createLead() only required the email.
    'customer' => ['email' => $email, 'first_name' => $first, 'last_name' => $last],
    'products' => [['offer_id' => 'OFF-1', 'quantity' => 1]],
    'payment' => [
        'method'     => 'paypal_wallet',
        'return_url' => 'https://shop.test/checkout/return',
        'cancel_url' => 'https://shop.test/checkout/cancel',
        // optional: 'external_payment_id' => 1234,
    ],
]);

if ($result->needsRedirect()) {
    header('Location: '.$result->redirectUrl());
    exit;
}
```

```
// Phase 2 — the customer returns to return_url (a new request).
// The persistent store (cookie/cache) survived the redirect.
$spark->resume();                 // or: $spark->resume($_GET['spark_order'] ?? null);
$capture = $spark->capture();     // POST /checkout/orders/capture (idempotent)

$capture->isApproved();
$capture->payerEmail();
$capture->subscriptionCreated();
$capture->subscriptionNumber();
```

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.

```
// On your cancel_url page:
if ($spark->wasCanceled()) {
    $spark->abandon(); // clears the stored funnel state so the next order starts clean
}
```

### How `spark_order` is appended

[](#how-spark_order-is-appended)

A PayPal `return_url` can only carry `?spark_order=` if the order exists *before* the redirect. `createOrder()` guarantees that:

- **Lead path** (`createLead()` then `createOrder()`): the order already exists, so `spark_order` is 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 — so `spark_order` is appended here too. This costs one extra `POST /checkout/leads` call 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/orders` and never creates a lead; it therefore does **not** append `spark_order`. Use it only when you don't need redirect-marker survival.

---

Tax, subscriptions, refunds, search
-----------------------------------

[](#tax-subscriptions-refunds-search)

```
// Tax quote (explicit; createOrder never auto-calls it).
$tax = $spark->calculateTax([
    'to_address' => ['country' => 'US', 'state' => 'CA', 'zip' => '90001'],
    'line_items' => [['quantity' => 1, 'unit_price' => 49.00]],
]);
$tax->enabled(); // false when the campaign has no tax provider (zeros, not an error)

// Subscription status — by the current order, or an explicit customer number.
$subscription = $spark->checkSubscription();          // uses the stored order
$spark->checkSubscription('CUST-123456-78901');       // explicit
$subscription->isActive();
$subscription->subscriptionNumbers();

// Refund a transaction. Omit `amount` for a full refund; pass isExternal: true
// to record an external (admin-recorded, off-gateway) refund.
$spark->refund('TXN-123456-789012', amount: 9.99, reason: 'customer request');
$spark->refund('TXN-123456-789012');                       // full refund
$spark->searchTransactions(['order_number' => 'ORD-1']);   // transaction search

// Search helpers (paginated).
$orders = $spark->searchOrders(['customer_email' => 'a@b.c']);
$orders->items();
$orders->pagination(); // total / per_page / current_page / last_page
```

---

Param normalization
-------------------

[](#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
- **`cohort`** passes 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 via `Config(logger: $logger)` to get a warning when keys are dropped over the cap.

Extend the alias map for edge cases via `Config`:

```
use SparkCheckout\Config;
use SparkCheckout\Normalization\AliasMap;

$config = new Config($apiToken, aliasMap: new AliasMap(['affiliate' => ['my_aff']]));
$spark = new SparkCheckout($apiToken, config: $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
-----------------------------

[](#errors-results-vs-exceptions)

OutcomeHow it surfacesDecline (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
---------

[](#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.

```
use SparkCheckout\Config;
use SparkCheckout\Testing\TestCard;

$spark = new SparkCheckout($apiToken, config: new Config($apiToken, testMode: true));
$spark->isTestMode(); // true

// Canonical Luhn-valid test cards + a ready-to-use payment block:
$spark->createOrder(['payment' => TestCard::approved()]);   // sandbox-approved card
$spark->createOrder(['payment' => TestCard::declined()]);   // sandbox-declined card
$spark->createOrder(['payment' => TestCard::payment(TestCard::AMEX)]);
```

---

Laravel (optional)
------------------

[](#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:

```
# .env
SPARK_CHECKOUT_TOKEN=your-token

php artisan vendor:publish --tag=spark-checkout-config   # optional
```

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.

```
use SparkCheckout\Laravel\SparkCheckoutFacade as Spark;
use SparkCheckout\SparkCheckout;

// Facade
Spark::init(['campaign' => 1042]);

// or resolve directly
app(SparkCheckout::class)->createLead([...]);
```

The Laravel bridge requires `illuminate/contracts` (already present in any Laravel app); the core SDK has no framework dependency.

---

License
-------

[](#license)

MIT.

###  Health Score

39

—

LowBetter than 85% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/294290145?v=4)[sparkdevsupport](/maintainers/sparkdevsupport)[@sparkdevsupport](https://github.com/sparkdevsupport)

---

Top Contributors

[![sparkdevsupport](https://avatars.githubusercontent.com/u/294290145?v=4)](https://github.com/sparkdevsupport "sparkdevsupport (2 commits)")

---

Tags

sdkpaymentscheckoutsparkcrmdtc

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/sparkcrm-spark-sdk/health.svg)

```
[![Health](https://phpackages.com/badges/sparkcrm-spark-sdk/health.svg)](https://phpackages.com/packages/sparkcrm-spark-sdk)
```

###  Alternatives

[cakephp/cakephp

The CakePHP framework

8.8k19.1M1.7k](/packages/cakephp-cakephp)[tempest/framework

The PHP framework that gets out of your way.

2.2k31.1k12](/packages/tempest-framework)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

84735.1k](/packages/flow-php-flow)[telnyx/telnyx-php

Official Telnyx PHP SDK — APIs for Voice, SMS, MMS, WhatsApp, Fax, SIP Trunking, Wireless IoT, Call Control, and more. Build global communications on Telnyx's private carrier-grade network.

35729.6k2](/packages/telnyx-telnyx-php)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.4M511](/packages/shopware-core)[chargebee/chargebee-php

ChargeBee API client implementation for PHP

758.3M9](/packages/chargebee-chargebee-php)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
