Skip to main content
The official JavaScript / TypeScript SDK wraps the REST API with a typed client, automatic retries, a resolve-and-wait helper, typed error classes, and webhook verification.

npm

@linkskipper/sdk · v0.2.1

GitHub

Source, issues, and changelog.

Install

npm install @linkskipper/sdk
Requires Node 18+ (for global fetch), a modern browser, or a custom fetch passed in. Ships ESM with TypeScript types.
Use the SDK from a server, not the browser, so your API key stays secret. See Authentication.

Initialize

import { LinkSkipper } from "@linkskipper/sdk";

const client = new LinkSkipper({
  apiKey: process.env.LINKSKIPPER_API_KEY!,
});

Client options

apiKey
string
required
Your sk_live_… key. Throws LinkSkipperError if missing.
baseUrl
string
default:"https://api.linkskipper.app"
Override the API base URL.
timeoutMs
number
default:"30000"
Per-request timeout in milliseconds.
pollIntervalMs
number
default:"2000"
Default poll interval for resolveAndWait.
maxWaitMs
number
default:"120000"
Default overall deadline for resolveAndWait.
retry
Partial<RetryConfig>
Retry tuning: { maxAttempts: 3, initialDelayMs: 500, maxDelayMs: 8000, backoffFactor: 2 }. Retries 429 and 5xx, honoring Retry-After, with exponential backoff and jitter.
fetch
typeof fetch
Custom fetch implementation (for older Node or testing).
userAgent
string
Override the default User-Agent.

Methods

resolve

resolve(url, options?) => Promise<ResolveResult> Submits a URL to POST /v1/resolve. Returns immediately — a cached link comes back with status: "done", a new one with status: "queued" and a jobId.
url
string
required
The shortener URL to resolve.
options.idempotencyKey
string
Sent as the Idempotency-Key header to de-duplicate retries.
const result = await client.resolve("https://ouo.io/abc123", {
  idempotencyKey: "order-42",
});

if (result.status === "done") {
  console.log(result.targetUrl, result.cached);
} else {
  console.log("queued:", result.jobId);
}
The ResolveResult exposes jobId, status, url, targetUrl, provider, tier, creditsCharged, cached, balance, queuePosition, and pollUrl (camelCase mappings of the resolve response).

resolveAndWait

resolveAndWait(url, options?) => Promise<ResolvedLink> The high-level helper: submits the resolve, then polls the job until it is terminal and returns the resolved link. A cached hit returns without polling.
url
string
required
The shortener URL to resolve.
options.idempotencyKey
string
De-duplicate retries.
options.pollIntervalMs
number
default:"2000"
How often to poll the job.
options.maxWaitMs
number
default:"120000"
Overall deadline. Exceeding it throws TimeoutError.
options.signal
AbortSignal
Cancel the wait loop.
import { LinkSkipper, JobFailedError, TimeoutError } from "@linkskipper/sdk";

const client = new LinkSkipper({ apiKey: process.env.LINKSKIPPER_API_KEY! });

try {
  const link = await client.resolveAndWait("https://ouo.io/abc123", {
    maxWaitMs: 60_000,
    pollIntervalMs: 2_000,
  });

  console.log(link.targetUrl);
  console.log(
    `${link.provider} (${link.tier}) · ${link.creditsCharged} credits · cached=${link.cached}`,
  );
} catch (error) {
  if (error instanceof JobFailedError) {
    console.error("Resolve failed:", error.reason);
  } else if (error instanceof TimeoutError) {
    console.error("Timed out, still pending:", error.jobId);
  } else {
    throw error;
  }
}
The ResolvedLink has a guaranteed targetUrl plus jobId, provider, tier, creditsCharged, balance, and cached.

getJob

getJob(jobId, signal?) => Promise<Job> Reads GET /v1/jobs/{jobId} once. Use it to poll manually or check a job from a webhook.
const job = await client.getJob("9b1d7c0e-2f3a-4b5c-8d6e-1a2b3c4d5e6f");
if (job.status === "done") {
  console.log(job.targetUrl);
}

account

account() => Promise<Account> Reads GET /v1/account: telegramId, balance, subscriptionUntil, and providers.
const account = await client.account();
console.log(`${account.balance} credits left`);

providers

providers() => Promise<ProviderEntry[]> Reads GET /v1/providers. Each entry has provider, label, hosts, tier, and latency.
const providers = await client.providers();
for (const p of providers) {
  console.log(`${p.label}: ${p.hosts.join(", ")}`);
}

Errors

Every failed API call rejects with an ApiError subclass keyed to the response error code. JobFailedError, TimeoutError, NetworkError, and WebhookVerificationError cover the non-HTTP cases. All extend LinkSkipperError.
ClassTriggered by
InvalidRequestErrorinvalid_request (400)
InvalidKeyErrorinvalid_key (401)
OutOfCreditsErrorout_of_credits (402) — .balance
ForbiddenScopeErrorforbidden_scope (403)
NotFoundErrornot_found (404)
LinkRemovedErrorlink_removed (410)
UnsupportedLinkErrorunsupported_link (422)
RateLimitedErrorrate_limited (429) — .retryAfter
QuotaExceededErrorquota_exceeded (429) — .retryAfter
ResolveFailedErrorresolve_failed (502)
ProviderDownErrorprovider_down (503)
JobFailedErrorA polled job ended failed/invalid.reason, .job
TimeoutErrorresolveAndWait deadline passed — .jobId, .waitedMs
NetworkErrorTransport failure after retries — .cause
WebhookVerificationErrorverifyWebhook rejected a payload
Every ApiError carries status, code, title, detail, type, balance, retryAfter, and the raw problem object.
import {
  ApiError,
  OutOfCreditsError,
  RateLimitedError,
} from "@linkskipper/sdk";

try {
  await client.resolveAndWait("https://ouo.io/abc123");
} catch (error) {
  if (error instanceof OutOfCreditsError) {
    console.error("Top up — balance:", error.balance);
  } else if (error instanceof RateLimitedError) {
    console.error("Back off for", error.retryAfter, "seconds");
  } else if (error instanceof ApiError) {
    console.error(error.code, error.status, error.detail);
  } else {
    throw error;
  }
}

Webhook verification

verifyWebhook(payload, signatureHeader, secret, toleranceSeconds?) checks the X-LinkSkipper-Signature header, the timestamp tolerance (default 300s), and the HMAC, then returns the typed WebhookEvent. It throws WebhookVerificationError on any failure.
import express from "express";
import { verifyWebhook, WebhookVerificationError } from "@linkskipper/sdk";

app.post(
  "/webhooks/linkskipper",
  express.raw({ type: "application/json" }),
  (req, res) => {
    try {
      const event = verifyWebhook(
        req.body.toString("utf8"),
        req.header("X-LinkSkipper-Signature"),
        process.env.LINKSKIPPER_WEBHOOK_SECRET,
      );
      if (event.event === "resolve.done") {
        saveTarget(event.job_id, event.target_url);
      }
      res.sendStatus(204);
    } catch (error) {
      if (error instanceof WebhookVerificationError) {
        res.sendStatus(400);
      } else {
        throw error;
      }
    }
  },
);
Pass the raw request body (e.g. via express.raw), not a re-serialized object, or the signature won’t match. See Webhooks.

Exported types

The package exports Account, AccountProvider, Job, JobStatus, ProviderEntry, ProviderTier, ResolveResult, ResolveStatus, ResolvedLink, ProblemDetails, WebhookEvent/WebhookPayload, WebhookEventName, ClientOptions, ResolveOptions, ResolveAndWaitOptions, RetryConfig, ErrorCode, and the ERROR_CODES constant.