Skip to content

linkskipper/sdk-js

Repository files navigation

@linkskipper/sdk

Official TypeScript/JavaScript client for the Link Skipper link-resolving API. It turns the asynchronous resolve flow into a single await.

  • Zero runtime dependencies (native fetch, Node 18+ or any modern browser).
  • ESM + CommonJS + bundled type declarations.
  • Typed errors for every API problem code, plus built-in retry with exponential backoff.

Install

npm install @linkskipper/sdk

Quick start — resolveAndWait

resolveAndWait submits the URL, then transparently polls the job until it finishes, returning the destination. A cache hit returns instantly; a cache miss is polled for you.

import { LinkSkipper, JobFailedError, TimeoutError } from "@linkskipper/sdk";

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

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

  console.log(link.targetUrl);
  console.log(`${link.provider} (${link.tier}) · ${link.creditsCharged} credit(s) · cached=${link.cached}`);
} catch (error) {
  if (error instanceof JobFailedError) {
    console.error("Could not resolve:", error.reason);
  } else if (error instanceof TimeoutError) {
    console.error("Still pending after the deadline:", error.jobId);
  } else {
    throw error;
  }
}

Configuration

const client = new LinkSkipper({
  apiKey: "sk_live_...",
  baseUrl: "https://linkskipper.app",
  timeoutMs: 30_000,
  pollIntervalMs: 2_000,
  maxWaitMs: 120_000,
  retry: {
    maxAttempts: 3,
    initialDelayMs: 500,
    maxDelayMs: 8_000,
    backoffFactor: 2,
  },
});
Option Default Description
apiKey (required) Sent as Authorization: Bearer <apiKey>.
baseUrl https://linkskipper.app API origin.
timeoutMs 30000 Per-request timeout.
pollIntervalMs 2000 Delay between job polls in resolveAndWait.
maxWaitMs 120000 Total budget for resolveAndWait before it throws TimeoutError.
retry 3 attempts Backoff for network errors, 5xx, and 429.
fetch globalThis.fetch Inject a custom fetch (e.g. for older runtimes).

API

resolve(url, { idempotencyKey? })

One-shot, non-blocking. On a cache hit it returns status: "done" with the targetUrl inline; otherwise it returns status: "queued" with a jobId and pollUrl.

const result = await client.resolve("https://cuty.io/xyz", { idempotencyKey: "order-42" });
if (result.status === "done") {
  console.log(result.targetUrl);
} else {
  console.log("queued at position", result.queuePosition, "->", result.pollUrl);
}

getJob(jobId)

Fetch a single job's current state.

const job = await client.getJob(result.jobId!);
console.log(job.status);

resolveAndWait(url, opts)

Resolve, then poll until the job is done, failed, or invalid. Returns the resolved link on success, throws JobFailedError on failed/invalid, and TimeoutError once maxWaitMs elapses.

account() / providers()

const account = await client.account();
console.log(account.balance, account.subscriptionUntil);

const providers = await client.providers();
console.log(providers.map((p) => `${p.label} (${p.tier})`));

Errors

Every non-2xx application/problem+json response is mapped to a typed exception. All extend ApiError, which exposes status, code, detail, title, type, balance, and retryAfter.

Code Class HTTP
invalid_request InvalidRequestError 400
invalid_key InvalidKeyError 401
out_of_credits OutOfCreditsError 402
forbidden_scope ForbiddenScopeError 403
not_found NotFoundError 404
link_removed LinkRemovedError 410
unsupported_link UnsupportedLinkError 422
rate_limited RateLimitedError 429
quota_exceeded QuotaExceededError 429
resolve_failed ResolveFailedError 502
provider_down ProviderDownError 503

Non-HTTP outcomes use NetworkError (transport failure after retries), TimeoutError (poll deadline), and JobFailedError (terminal failed/invalid job).

import { RateLimitedError, OutOfCreditsError } from "@linkskipper/sdk";

try {
  await client.resolveAndWait("https://exe.io/AbCdEf");
} catch (error) {
  if (error instanceof RateLimitedError) {
    console.log("retry after", error.retryAfter, "seconds");
  } else if (error instanceof OutOfCreditsError) {
    console.log("balance:", error.balance);
  }
}

Webhooks

Pass webhook_url on a resolve and Link Skipper POSTs the result to your endpoint, signed with X-LinkSkipper-Signature: t=<unixSeconds>,v1=<hmacSha256>. Verify every delivery against your key's webhook secret with verifyWebhook, which checks the HMAC in constant time, enforces a freshness window (default 300s), and returns the typed WebhookEvent. Pass the raw request body string — not a re-serialized object.

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") {
      console.log(`Job ${event.job_id} -> ${event.target_url}`);
    }
    res.sendStatus(204);
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      res.sendStatus(400);
    } else {
      throw error;
    }
  }
});

Retries

Network errors, 5xx, and 429 are retried with bounded exponential backoff. A Retry-After header is always honored. Other 4xx responses are never retried.

License

MIT © Link Skipper

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors