Skip to main content

Node SDK contract

Scope: the API-key contract @optomatica/optolink-sdk wraps — link CRUD + QR only. No deferred-matching surface exists on this SDK; the CLIENT-tier mobile surface is Android / iOS.

Source-checked against optolink-backend @ 29f8589519ac85445ed0598c1b9cfc125a9dd6f2, 2026-10-07 (src/link/link.controller.ts, link.service.ts, short-code.service.ts, qr-code.service.ts, src/auth/guards/api-key-auth.guard.ts, src/common/filters/http-exception.filter.ts, src/rate-limiting/rate-limit.decorator.ts) and optolink-node @ d4b5dae (src/index.ts, links.ts, errors.ts, request.ts).

Identity​

npm@optomatica/optolink-sdk 0.1.0 (published)
RuntimeNode ≥ 18, zero runtime dependencies, dual ESM + CJS
Repooptolink-node/ (own package root)
Compat line"Compatible with OptoLink backend v2.1.0+" (README)

The backend has no API versioning — the compat number is a hand-maintained platform-release line in each SDK README; backend package.json and the OpenAPI document both say 1.0. The number matches the public docs (docs/sdk/overview.md); there is no machine-readable backend version to check it against.

Auth​

Authorization: Bearer opl_api_… on every request — all endpoints are SERVER tier (@RequireApiKeyTier(SERVER)); a CLIENT key (opl_sdk_…) authenticates but gets 403 API key tier 'CLIENT' is not permitted on this endpoint (requires 'SERVER'). No global path prefix. 401 messages: Missing Authorization header / Invalid Authorization format / Invalid API key format (short) / Invalid API key (unknown) / Organization is suspended.

Endpoints (7 methods)​

MethodHTTPReturns (include matrix, verified)
createPOST /links201 full row + url — no domain/template/clickCount
getGET /links/:idfull row + url + domain {domain,status} + full template row (null when unlinked)
listGET /links{data, total, page, limit} — items add url, domain, clickCount; no template
updatePATCH /links/:idraw row only — no url/domain/template
deleteDELETE /links/:id200 {deleted: true}
qrSvg / qrPngGET /links/:id/qr.svg / qr.pngSVG markup / PNG bytes, 404 if link missing

The POST/PATCH asymmetry is why the SDK auto-refetches (GET /links/:id after both, one shared retry budget) so every method resolves the uniform full Link. :id is UUID-piped — a non-UUID is 400 Validation failed (uuid is expected) with no details[], not 404.

Create gates (spec delta): POST /links mirrors the portal's create gates — 428 APP_CONFIG_REQUIRED before the quota check, then 402 QUOTA_EXCEEDED (bodies + semantics: Entitlements enforcement). The hand-off spec predates these gates and lists neither.

Update semantics: undefined key = keep, JSON null = clear-to-inherit/empty — the SDK passes null through verbatim. Create's domainId is UUID-validated; update's is a MaxLength(100) string (deliberate asymmetry, don't "fix"). Duplicate shortCode is a clean 409 Conflict — Short code "X" is already in use in this organization (ShortCodeService.validate, pre-write check). Spec delta: the spec describes a Prisma 500 here; that path no longer exists.

Error envelope (all non-2xx, GlobalExceptionFilter)​

{"statusCode":404,"error":"Not Found","message":"Link <id> not found",
"details":[{"message":"…"}],"timestamp":"…","path":"/links/<id>","requestId":"…"}
  • details[] only on 400 validation (message text; no field names today); error is recomputed from a status→name table — 409 is missing from the table, so the conflict above renders error: "Error" (cosmetic wart, stable).
  • The three entitlement exceptions forward extra fields (code, quota/feature) on top of the envelope — shapes in Entitlements enforcement.
  • requestId is echoed only when the client sent x-request-id; every response also carries an X-Request-Id header (middleware generates one when the client didn't).
  • 429 body: Rate limit exceeded. Retry after N second(s). — no Retry-After header anywhere; the SDK regexes N out of the message.
  • Non-JSON error bodies (proxy HTML) still map to OptoLinkError with a generic message and the body text in raw; transport failures → OptoLinkConnectionError.

Pagination​

page (default 1), limit (default 20, max 100). Envelope is exactly {data, total, page, limit} — no hasNext/totalPages; listAll() walks while page * limit < total. Unknown query/body keys are rejected 400 (forbidNonWhitelisted). isActive serializes as literal "true"/"false" — the server Transform maps any other present value to false, so false must be sent, never omitted.

QR​

qr.svg → image/svg+xml, qr.png → image/png; fixed shape (256×256, margin 2, #000000 on #ffffff) — no query params or options exist; payload encodes https://{domain}/{orgKey}/{shortCode}. Cache-Control: public, max-age=86400.

Rate limits​

Keyed per API key (IP only when unauthenticated). POST/PATCH/DELETE run the creation preset (100/s), GETs and QR the resolution preset (1000/s) — generous; the limits that actually bite live on the mobile surface (Android · iOS).

Source-checked against optolink-backend @ 29f8589, 2026-10-07. The error: "Error" 409 rendering is code-read only — no live curl pass ran for this harvest (B-018); probe on next live use.

Retries (SDK-side): reads retry connection/429/5xx; writes never retry 5xx (no idempotency keys — a landed retried POST is a second link); 429 sleeps the regexed retryAfterSeconds. Defaults: 10 s timeout per attempt, 2 retries.