Framework-agnostic PHP SDK for the Takealot Marketplace API — read seller sales, offers, transactions, balances, returns, shipments, facilities, and account information from your Takealot Seller account.
- Framework-agnostic. Plain PSR-18 (HTTP client), PSR-17 (factories), and PSR-7 (messages). No Laravel, no Symfony, no global state — drop it into any modern PHP project.
- Read-only and safe. The SDK wraps the Marketplace API's
GETendpoints only. It never creates, updates, or disables anything on your account — so there is no risk of an accidental price change or a disabled offer. - Type-safe DTOs. Readonly response objects with strict shape validation. Malformed upstream payloads fail loudly instead of leaking untyped arrays through your code.
- Continuation-token pagination, two ways. Fetch a single
Pageand follow the token yourself, oriterate()a lazyGeneratorthat walks every page transparently. - Automatic rate-limit handling. HTTP 429 responses are retried
automatically, honouring the
Retry-Afterheader, with exponential backoff as a fallback. Configurable viamaxRetries. - Typed exception hierarchy.
400,401/403,404, and429each map to a distinct exception subclass — no need togrepon status codes. - Hardened by default. 8 MiB response body cap, JSON depth limit of 64, and
PSR-18 transport-error wrapping that scrubs the
X-API-Keyheader from leaked messages. - PHPStan level 9. Strict types throughout, zero static-analysis errors.
Takealot does not publish an official PHP SDK at the time of writing. This
package is built against the current public
Takealot Marketplace API at
https://marketplace-api.takealot.com/v1, targets modern PHP (8.2+), and stays
out of your dependency-injection container by design.
It is the integration layer SONNENGLAS uses to pull our South African marketplace sales and settlement data into our own systems.
- PHP 8.2 or newer.
- A PSR-18 HTTP client. Guzzle 7 is the
recommended default and is automatically discovered via
php-http/discovery.
composer require sonnenglas/takealot-php-sdkIf you do not already have a PSR-18 client installed:
composer require guzzlehttp/guzzlephp-http/discovery auto-wires any installed PSR-18 client and PSR-17
factories — you only need to inject them manually if you want to override
defaults (timeouts, retry middleware, a mock client in tests, etc.).
Generate an API key in the Takealot Seller Portal under Settings → API Access, then construct the client with it. Everything else is optional.
use Sonnenglas\Takealot\Client;
$client = new Client(apiKey: getenv('TAKEALOT_API_KEY'));The key is sent as the X-API-Key header on every request. Only one API key
can be active per seller account at a time.
$page = $client->sales()->list([
'order_date__gte' => '2026-06-01', // inclusive, YYYY-MM-DD, SAST
'order_date__lte' => '2026-06-30',
'limit' => 100, // 1–100, default 20
'include_count' => true,
]);
foreach ($page->items as $sale) {
printf(
"Order %d · %s · R%d · %s\n",
$sale->orderId,
$sale->sku,
$sale->sellingPrice,
$sale->salesRegion ?? 'unknown',
);
}
echo "Matching sales: " . ($page->count ?? 'n/a') . "\n";
echo $page->hasMore() ? "More pages available.\n" : "Last page.\n";iterate() follows the continuation_token for you, so you never have to
manage paging by hand:
$totalUnits = 0;
foreach ($client->sales()->iterate(['order_date__gte' => '2026-06-01']) as $sale) {
$totalUnits += $sale->quantity;
}
echo "Units sold in June: {$totalUnits}\n";$balances = $client->balances()->get()->balances; // nested ?Balances breakdown
printf(
"Available for payout: R%.2f (held back R%.2f)\n",
$balances?->available ?? 0.0,
$balances?->heldBack ?? 0.0,
);- Documentation home
- Guides
- API reference
- Runnable examples — see
examples/
The SDK targets the seller-facing read surface of the Marketplace API.
Every collection endpoint supports continuation-token pagination, fields
selection, and (where applicable) expands.
| Resource | Endpoint(s) | Accessor | Operations |
|---|---|---|---|
| Sales | GET /sales |
$client->sales() |
list(), iterate() |
| Offers | GET /offers, GET /offers/{id}, GET /offers/by_sku/{sku}, GET /offers/by_barcode/{barcode} |
$client->offers() |
list(), iterate(), getById(), getBySku(), getByBarcode() |
| Transactions | GET /transactions |
$client->transactions() |
list(), iterate() |
| Balances | GET /balances |
$client->balances() |
get() |
| Returns | GET /returns, GET /returns/{id} |
$client->returns() |
list(), iterate(), getById() |
| Shipments | GET /shipments, GET /shipments/{id} |
$client->shipments() |
list(), iterate(), getById() |
| Facilities | GET /facilities/get_enabled_regions |
$client->facilities() |
enabledRegions() |
| Seller | GET /seller |
$client->seller() |
get() |
Single-item lookups (getById(), getBySku(), getByBarcode(),
returns()->getById(), shipments()->getById()) return the DTO, or null when
the item does not exist (HTTP 404) — they do not throw on a missing item.
Write operations are out of scope. The Marketplace API exposes offer-management endpoints (
POST /offers,PATCH /offers/{id},POST /offers/batch). This SDK deliberately does not wrap them — it is a read-only client. Call them directly if you need to mutate offers.
Collection endpoints return a Page:
$page = $client->sales()->list(['limit' => 50]);
$page->items; // list<Sale>
$page->continuationToken; // string|null — the token for the next page
$page->count; // int|null — total matches (only with include_count=true)
$page->hasMore(); // bool — true when continuationToken !== nullTo walk every page manually, feed the token back in. When a continuation token is supplied, all other query parameters are ignored by the API — pass the token alone:
$params = ['order_date__gte' => '2026-06-01', 'limit' => 100];
do {
$page = $client->sales()->list($params);
foreach ($page->items as $sale) {
// ...
}
$params = ['continuation_token' => $page->continuationToken];
} while ($page->hasMore());Or let the SDK do it for you with iterate() — see the
pagination guide.
The client retries HTTP 429 responses automatically:
- It reads the
Retry-Afterheader (integer seconds or an HTTP-date) and waits that long before retrying, capped at 60 seconds per attempt. - If no usable
Retry-Afteris present, it falls back to exponential backoff (1s, 2s, 4s, …, capped at 60s). - After
maxRetries(default3) exhausted attempts, it throwsRateLimitException, which exposes$retryAfterfor your own scheduling.
Tune or disable retries via the constructor:
// More patient: up to 5 retries.
$client = new Client(apiKey: $key, maxRetries: 5);
// Fail fast: no automatic retries.
$client = new Client(apiKey: $key, maxRetries: 0);See the rate-limiting guide for details and for how to inject a no-op sleeper in tests.
All SDK exceptions extend Sonnenglas\Takealot\Exceptions\TakealotException.
| HTTP | Exception | When |
|---|---|---|
| 400 | ValidationException |
Invalid query parameters or filters. |
| 401 / 403 | AuthenticationException |
Missing, invalid, or deactivated API key. |
| 404 | NotFoundException |
A missing resource. Single-item lookups catch this and return null instead. |
| 429 | RateLimitException ($retryAfter) |
Rate limit hit after automatic retries were exhausted. |
| other | ApiException |
Any other 4xx/5xx, oversized body, or malformed JSON. |
| — | TransportException |
The PSR-18 client failed (DNS / TCP / TLS / timeout). statusCode is 0. |
ApiException (and therefore every subclass) carries $statusCode and the
decoded $responseBody:
use Sonnenglas\Takealot\Exceptions\AuthenticationException;
use Sonnenglas\Takealot\Exceptions\RateLimitException;
use Sonnenglas\Takealot\Exceptions\TakealotException;
try {
$page = $client->sales()->list(['order_date__gte' => '2026-06-01']);
} catch (AuthenticationException $e) {
// Check that TAKEALOT_API_KEY is set and still active in the Seller Portal.
throw $e;
} catch (RateLimitException $e) {
sleep($e->retryAfter ?? 5);
// ...retry...
} catch (TakealotException $e) {
error_log("Takealot API error ({$e->getCode()}): {$e->getMessage()}");
throw $e;
}See the error-handling guide for the full decision matrix.
The SDK is built test-first against php-http/mock-client, so you can exercise
every code path without touching the live API. Swap in a mock PSR-18 client and
a no-op Sleeper to make retry tests instant:
use GuzzleHttp\Psr7\HttpFactory;
use GuzzleHttp\Psr7\Response;
use Http\Mock\Client as MockClient;
use Sonnenglas\Takealot\Client;
$mock = new MockClient();
$mock->addResponse(new Response(200, [], json_encode([
'items' => [/* ...sale rows... */],
'limit' => 20,
], JSON_THROW_ON_ERROR)));
$factory = new HttpFactory();
$client = new Client(
apiKey: 'key_test',
httpClient: $mock,
requestFactory: $factory,
);
$page = $client->sales()->list(['limit' => 20]);Run the SDK's own suite with:
composer test # PHPUnit
composer phpstan # PHPStan level 9
composer check # phpstan + test togetherThis project follows Semantic Versioning 2.0.0. See CHANGELOG.md for the release history.
Pull requests, issues, and discussions are welcome. Please read CONTRIBUTING.md before opening a PR — it covers the dev setup, coding standards, and the test-first workflow used throughout the codebase.
Security vulnerabilities should be reported privately — see SECURITY.md.
Released under the MIT License.
Built and maintained by Przemek Peron.
If this SDK saves you time, please consider starring the GitHub repository.