Skip to main content
The official PHP SDK wraps the REST API with typed models, automatic retries, a resolve-and-wait helper, a per-code exception hierarchy, and webhook verification.

Packagist

linkskipper/sdk · v0.2.1

GitHub

Source, issues, and changelog.

Install

composer require linkskipper/sdk
Requires PHP 8.1+ (the SDK uses enums and readonly properties). It works with the built-in cURL transport out of the box, or any PSR-18 HTTP client.
Use the SDK from your server, never client-side, so your API key stays secret. See Authentication.

Initialize

<?php

use LinkSkipper\LinkSkipper;

require __DIR__ . "/vendor/autoload.php";

$client = LinkSkipper::create(getenv("LINKSKIPPER_API_KEY"));
LinkSkipper::create(string $apiKey, string $baseUrl = …) is the quick constructor. For full control over timeouts, polling defaults, retry policy, transport, or clock, build a Config and pass it to new LinkSkipper($config):
<?php

use LinkSkipper\Config;
use LinkSkipper\LinkSkipper;

$client = new LinkSkipper(new Config(
    apiKey: getenv("LINKSKIPPER_API_KEY"),
    timeoutMs: 30000,
    pollIntervalMs: 2000,
    maxWaitMs: 120000,
));

Config options

apiKey
string
required
Your sk_live_… key. Throws ConfigurationException if empty.
baseUrl
string
default:"https://api.linkskipper.app"
Override the API base URL.
timeoutMs
int
default:"30000"
Per-request timeout in milliseconds.
pollIntervalMs
int
default:"2000"
Default poll interval for resolveAndWait.
maxWaitMs
int
default:"120000"
Default overall deadline for resolveAndWait.
retryPolicy
RetryPolicy
Retry tuning. Retries 429 and 5xx, honoring Retry-After, with exponential backoff.
transport
Transport
HTTP transport. Defaults to CurlTransport; pass Psr18Transport to use your own client.

Methods

resolve

resolve(string $url, ?string $idempotencyKey = null): ResolveResult Submits a URL to POST /v1/resolve. Returns immediately — a cached link comes back with status === ResolveStatus::Done, a new one queued with a jobId.
<?php

use LinkSkipper\Enum\ResolveStatus;

$result = $client->resolve("https://ouo.io/abc123", "order-42");

if ($result->status === ResolveStatus::Done) {
    echo $result->targetUrl, PHP_EOL;
    echo $result->cached ? "cached" : "fresh", PHP_EOL;
} else {
    echo "queued: ", $result->jobId, PHP_EOL;
}
ResolveResult exposes jobId, status, url, targetUrl, provider, tier, creditsCharged, cached, balance, queuePosition, pollUrl, and an isDone() helper.

resolveAndWait

resolveAndWait(
    string $url,
    ?string $idempotencyKey = null,
    ?int $pollIntervalMs = null,
    ?int $maxWaitMs = null,
): ResolvedLink
The high-level helper: submits the resolve, polls the job until terminal, and returns the resolved link. A cached hit returns without polling.
<?php

use LinkSkipper\Exception\JobFailedException;
use LinkSkipper\Exception\TimeoutException;

try {
    $link = $client->resolveAndWait(
        "https://ouo.io/abc123",
        maxWaitMs: 60000,
        pollIntervalMs: 2000,
    );

    echo $link->targetUrl, PHP_EOL;
    printf(
        "%s (%s) · %d credits · cached=%s\n",
        $link->provider,
        $link->tier->value,
        $link->creditsCharged,
        $link->cached ? "yes" : "no",
    );
} catch (JobFailedException $exception) {
    fwrite(STDERR, "Resolve failed: " . $exception->reason . PHP_EOL);
} catch (TimeoutException $exception) {
    fwrite(STDERR, "Timed out, still pending: " . $exception->jobId . PHP_EOL);
}
ResolvedLink has a guaranteed targetUrl plus jobId, provider, tier, creditsCharged, balance, and cached.

getJob

getJob(string $jobId): Job Reads GET /v1/jobs/{jobId} once. The returned Job has a JobStatus enum status with an isTerminal() helper.
<?php

use LinkSkipper\Enum\JobStatus;

$job = $client->getJob("9b1d7c0e-2f3a-4b5c-8d6e-1a2b3c4d5e6f");
if ($job->status === JobStatus::Done) {
    echo $job->targetUrl, PHP_EOL;
}

account

account(): Account Reads GET /v1/account: telegramId, balance, subscriptionUntil, and providers.
<?php

$account = $client->account();
printf("%d credits left\n", $account->balance);

providers

providers(): array — a list<ProviderEntry> Reads GET /v1/providers. Each ProviderEntry has provider, label, hosts, tier (enum), and latency.
<?php

foreach ($client->providers() as $p) {
    printf("%s: %s\n", $p->label, implode(", ", $p->hosts));
}

Errors

Every failed API call throws an ApiException subclass keyed to the response error code, under the LinkSkipper\Exception namespace. All extend LinkSkipperException.
ExceptionTriggered by
InvalidRequestExceptioninvalid_request (400)
InvalidKeyExceptioninvalid_key (401)
OutOfCreditsExceptionout_of_credits (402) — balance()
ForbiddenScopeExceptionforbidden_scope (403)
NotFoundExceptionnot_found (404)
LinkRemovedExceptionlink_removed (410)
UnsupportedLinkExceptionunsupported_link (422)
RateLimitedExceptionrate_limited (429) — retryAfter()
QuotaExceededExceptionquota_exceeded (429) — retryAfter()
ResolveFailedExceptionresolve_failed (502)
ProviderDownExceptionprovider_down (503)
JobFailedExceptionA polled job ended failed/invalid$reason, $job
TimeoutExceptionresolveAndWait deadline passed — $jobId
NetworkExceptionTransport failure after retries
ConfigurationExceptionBad client configuration (e.g. empty key)
WebhookVerificationExceptionWebhook::verify rejected a payload
Every ApiException exposes the parsed problem via methods: status(), errorCode() (an ErrorCode enum), detail(), retryAfter(), balance(), and the raw $exception->problem.
<?php

use LinkSkipper\Exception\ApiException;
use LinkSkipper\Exception\OutOfCreditsException;
use LinkSkipper\Exception\RateLimitedException;

try {
    $client->resolveAndWait("https://ouo.io/abc123");
} catch (OutOfCreditsException $e) {
    fwrite(STDERR, "Top up — balance: " . $e->balance() . PHP_EOL);
} catch (RateLimitedException $e) {
    fwrite(STDERR, "Back off for " . $e->retryAfter() . " seconds" . PHP_EOL);
} catch (ApiException $e) {
    fwrite(STDERR, $e->errorCode()->value . " " . $e->status() . " " . $e->detail() . PHP_EOL);
}

Webhook verification

Webhook::verify(string $payload, string $signatureHeader, string $secret, int $toleranceSeconds = 300): WebhookEvent checks the X-LinkSkipper-Signature header, the timestamp tolerance, and the HMAC, then returns a typed WebhookEvent. It throws WebhookVerificationException on any failure.
<?php

use LinkSkipper\Webhook;
use LinkSkipper\Enum\WebhookEventName;
use LinkSkipper\Exception\WebhookVerificationException;

$payload   = file_get_contents("php://input");
$signature = $_SERVER["HTTP_X_LINKSKIPPER_SIGNATURE"] ?? "";

try {
    $event = Webhook::verify($payload, $signature, getenv("LINKSKIPPER_WEBHOOK_SECRET"));
    if ($event->event === WebhookEventName::ResolveDone) {
        save_target($event->jobId, $event->targetUrl);
    }
    http_response_code(204);
} catch (WebhookVerificationException $exception) {
    http_response_code(400);
}
The WebhookEvent exposes event (a WebhookEventName enum), createdAt, jobId, status (a JobStatus enum), targetUrl, provider, tier, error, creditsCharged, and balance.
Read the raw request body with php://input before any JSON parsing, or the signature won’t match. See Webhooks.