SUQODocs
User GuideAPI ReferenceSDKs

Handle errors

The SuqoError hierarchy in the SUQO PHP SDK.

Every failure the SDK raises derives from Suqo\Exception\SuqoError, which extends \RuntimeException. A single catch (SuqoError) is total — configuration faults included. Check the type, never the message string or the raw response shape; neither is part of the contract.

use Suqo\Exception\KycRequiredError;
use Suqo\Exception\RateLimitError;
use Suqo\Exception\SuqoError;
use Suqo\Exception\ValidationError;

try {
    $suqo->subscriptions->create($params);
} catch (ValidationError $e) {
    foreach ($e->fieldErrors as $field => $messages) {
        echo $field, ': ', implode(', ', $messages), PHP_EOL;
    }
} catch (KycRequiredError $e) {
    echo $e->kycStatus;                 // wire `status_code`
} catch (RateLimitError $e) {
    echo $e->retryAfter;                // seconds, from Retry-After
} catch (SuqoError $e) {
    error_log("suqo {$e->status} req={$e->requestId}: {$e->getMessage()}");
}

Order matters: catch the specific types first, then SuqoError as the backstop.

The hierarchy

ClassWhen
AuthenticationError401 — a bad, revoked or wrong-environment key
KycRequiredError403 with a KYC-shaped body; carries kycStatus
PermissionDeniedError403 without one — the key is valid, the account may not use this endpoint
ValidationError400 — the only type that populates fieldErrors
NotFoundError404 — including an id from the other environment
RateLimitError429 — retryAfter when the server sent one
ServerError5xx, and any status the table does not name
NetworkErrortransport failure, DNS, TLS or timeout (status 0)
CancelledErroryour Cancellation token was tripped (status 0)
SuqoConfigErrorbad configuration, before any request exists (status 0)

The Error suffix is kept rather than PHP's Exception idiom so the type names read the same across SUQO's SDKs. Every one is still a \Throwable.

What's on every error

PropertyTypeNotes
statusintHTTP status, or 0 for a non-HTTP failure. Also getCode().
requestIdstringThe X-Request-Id actually sent on the failing attempt — quote it in a support ticket. Empty when the error predates a request.
rawBodymixedThe parsed body, wire names preserved. Null when there was none or it was not JSON.
fieldErrorsarray<string, list<string>>Always present, empty when not applicable.
retryAfter?floatSeconds, from the Retry-After header.

getMessage() carries the server's message when the body supplied one, else a per-status default.

ValidationError — keys are wire names

fieldErrors is populated only for a 400 with a field-shaped body, and its keys are wire names (pbp_id, billing_email), not the SDK's surface names — the body is reported as it arrived.

$emailProblems = $e->fieldErrors['email'] ?? [];

getMessage() is the first message in the body's own key order.

Two kinds of 403

KYC is the narrower case and is checked first, so a genuine KYC response raises KycRequiredError with kycStatus populated; anything else 403 raises PermissionDeniedError. Catch both if you want to handle every 403.

Configuration errors are inside the tree

SuqoConfigError is raised by new SuqoClient(...) and Config::resolve(...), and sits inside the SuqoError tree so one catch-all covers startup and runtime. status is 0, requestId is '' and rawBody is null, because no request exists — it is a deployment mistake rather than an API outcome.

Writes aren't automatically retried

GET requests are retried up to maxRetries times on a network failure, a timeout, a 429 or a 5xx. Writes — create, cancel, updateBillingCycle — are not, pending idempotency keys, because a blindly-retried write could double-act.

So a NetworkError on a write means the request may or may not have landed. Reconcile — list the subscription, check the webhook — rather than resending blindly. Not yet implemented covers what changes when idempotency keys ship.

CancelledError is never retried, and is deliberately distinct from the NetworkError a timeout produces: "I stopped it" and "the network stopped it" stay separable.

On this page