@syllecta/sdk-js
v0.1.0
Published
Universal TypeScript SDK for the Syllecta Cloud API
Readme
@syllecta/sdk-js
Official TypeScript SDK for the Syllecta platform. It wraps every public REST and webhook endpoint with safe defaults so product teams can send idempotent writes, forward third‑party webhooks, and monitor retries without re‑implementing middleware.
Why this SDK exists
Building reliable event ingestion on your own is tedious. Common issues:
- Users double‑submit checkout forms and your API creates duplicate payments.
- Webhooks arrive twice or out of order because Stripe/GitHub retries.
- Each team builds its own retry + verification logic and it drifts out of sync.
@syllecta/sdk-js centralizes those patterns so that any Node.js service can:
- Call
/v1/recordwith an idempotency key – duplicate requests are automatically deduped. - Verify and forward webhooks to Syllecta – we keep the audit trail, you keep the business logic.
- Use typed HTTP calls with retry defaults without wiring your own client stack.
The SDK supports both ESM and CommonJS projects, works with plain fetch or Express, and exposes only the surface area customers need (no backoffice or admin APIs).
Prerequisites
Before coding, make sure you have:
- A Syllecta API key (created in the dashboard).
- Optional webhook signing secrets from Stripe/Shopify/etc. if you plan to verify signatures locally.
- Node.js ≥ 20 and TypeScript ≥ 5.6 (JavaScript projects work too, but types give you extra safety).
Installation
npm install @syllecta/sdk-js
# or
pnpm add @syllecta/sdk-jsIf you want the built‑in Express middleware for webhooks:
npm install express body-parserConfiguring the client
import { CloudSDK, BearerAuth } from "@syllecta/sdk-js";
const sdk = new CloudSDK({
baseUrl: "https://api.syllecta.com/v1",
auth: BearerAuth(process.env.SYLLECTA_API_KEY!),
timeoutMs: 30_000,
maxRetries: 2
});Available auth strategies:
| Helper | When to use |
| ------------ | ------------------------------------------------------------------------- |
| BearerAuth | Standard Syllecta API key |
| ApiKeyAuth | Generic custom-header helper for non-standard/internal integrations |
| NoAuth | Local/testing calls where auth is intentionally disabled |
For Syllecta tenant API keys, use Authorization: Bearer <ck_...> via BearerAuth(...). ApiKeyAuth(...) remains available only for custom gateways or internal compatibility layers.
Making idempotent REST calls
Idempotent writes guarantee that retries do not double‑book inventory, charge customers twice, or create duplicate database rows.
- Decide on a scope (usually
METHOD:path). - Generate a deterministic idempotency key (
order_456_payment_attempt). - Send the request with the helper or plain client.
const result = await sdk.idempotency.checkOrLock({
key: "order_456_payment_attempt",
scope: "POST:/payments",
ttlSeconds: 60
});
if (result.cached) {
// Another worker already processed this payment.
return result.record;
}
if (!result.locked) {
return { retryAfterMs: result.retryAfterMs ?? 250 };
}
// Your business operation (DB write, external API call, etc.)
const payment = { id: "pay_01", status: "accepted" };
await sdk.idempotency.save({
key: "order_456_payment_attempt",
scope: "POST:/payments",
status: 201,
headers: { "content-type": "application/json" },
body: payment
});Most teams use sdk.idempotency.* directly for lock/save flows; sdk.client.request remains available when you need low-level control.
Plain HTTP request example:
import { SyllectaApiError } from "@syllecta/sdk-js";
try {
const res = await sdk.client.request<{ cached: boolean }>({
method: "GET",
path: "/v1/record",
query: { key: "order_456_payment_attempt", scope: "POST:/payments" }
});
console.log(res.data.cached);
} catch (err) {
if (err instanceof SyllectaApiError) {
throw new Error(`Syllecta request failed with status ${err.status}`);
}
throw err;
}How Syllecta responds
| Status | Meaning | Action |
| ----------------------------- | ---------------------------------------------- | -------------------------- |
| 201 | First successful write | Continue |
| 200 with { cached: true } | Duplicate request | Safe to ignore |
| 400 | Missing key / invalid body | Fix payload |
| 401 | API key invalid | Rotate credentials |
| 425 | Another request with same key is still running | Retry after retryAfterMs |
| 429 | Usage quota exceeded | Check dashboard usage tab |
Chargeback simulation API
Use this endpoint when you need realistic dispute lifecycle events in sandbox/QA flows.
const simulation = await sdk.simulations.createChargeback({
provider: "stripe",
amount: 2500, // cents
currency: "usd",
transactionId: "txn_123",
simulateLifecycle: true,
finalStatus: "won" // optional: "won" | "lost"
});
console.log(simulation.simulation.id, simulation.simulation.status);Request body fields:
provider:"stripe" | "braintree" | "adyen"amount: integer in centscurrency: ISO currency code (usd,eur, ...)transactionId: optional external transaction IDsimulateLifecycle: optional (trueby default)finalStatus: optional forced final state (wonorlost)
Webhook processing playbook
Most customers integrate Syllecta in front of Stripe, Shopify, GitHub, or custom HMAC providers. The SDK exposes two levels of abstraction:
1. Raw forwarding
import { forwardWebhookEvent } from "@syllecta/sdk-js";
app.post("/shopify/webhook", async (req, res) => {
const forwarded = await forwardWebhookEvent({
provider: "shopify",
apiKey: process.env.SYLLECTA_API_KEY!,
body: req.body,
headers: {
"x-shopify-hmac-sha256": req.get("x-shopify-hmac-sha256") ?? undefined
},
rawBody: req.rawBody // strongly recommended
});
if (forwarded.cached) {
return res.status(200).json({ ok: true, cached: true });
}
res.json({ ok: true });
});Use this when you only need deduplication and centralized logging.
forwardWebhookEvent and handleWebhookEvent send tenant API keys as Authorization: Bearer <ck_...> by default. Pass apiKeyHeader only for explicit legacy/custom compatibility layers.
2. Full verification + processing
import { handleWebhookEvent } from "@syllecta/sdk-js";
app.post("/stripe/webhook", async (req, res) => {
const { skipped } = await handleWebhookEvent({
provider: "stripe",
apiKey: process.env.SYLLECTA_API_KEY!,
signingSecret: process.env.STRIPE_SIGNING_SECRET!,
signatureHeader: "stripe-signature",
headers: { "stripe-signature": req.get("stripe-signature") ?? undefined },
rawBody: req.rawBody,
body: req.body,
onProcess: async () => {
await processStripeEvent(req.body); // only runs once per event
}
});
res.json({ ok: true, cached: skipped });
});handleWebhookEvent returns early when the payload is already known to Syllecta, so your onProcess callback only executes for fresh events.
Pagination pattern
The SDK does not include an auto-paginator helper. For cursor endpoints, iterate manually:
import { SyllectaApiError } from "@syllecta/sdk-js";
type AuditLog = { id: string; occurredAt: string; actor: string };
let cursor: string | undefined;
do {
try {
const res = await sdk.client.request<{ items: AuditLog[]; nextCursor?: string }>({
method: "GET",
path: "/v1/audit/logs",
query: { tenantId: "tenant_1", pageSize: 100, cursor }
});
for (const row of res.data.items) {
console.log(row.id, row.actor);
}
cursor = res.data.nextCursor;
} catch (err) {
if (err instanceof SyllectaApiError) {
throw new Error(`Failed to load audit logs (${err.status})`);
}
throw err;
}
} while (cursor);Working with errors
sdk.client.request returns { status, headers, data } only for successful responses. Any non‑2xx response throws SyllectaApiError, which includes status, headers, and body. Transport/runtime failures (network, timeout abort, etc.) still throw their native error.
import { SyllectaApiError } from "@syllecta/sdk-js";
try {
const res = await sdk.client.request({
method: "POST",
path: "/v1/record",
body: { key: "order_456_payment_attempt", scope: "POST:/payments" }
});
console.log("record status", res.status);
} catch (err) {
if (err instanceof SyllectaApiError) {
console.error("Syllecta API returned error", err.status, err.body);
} else {
console.error("Transport error", err);
}
}If you want a reusable type guard:
import { isSyllectaApiError } from "@syllecta/sdk-js";
try {
await sdk.simulations.createChargeback({
provider: "stripe",
amount: 2500,
currency: "usd"
});
} catch (err) {
if (isSyllectaApiError(err)) {
console.error("status", err.status);
console.error("body", err.body);
throw err;
}
throw err;
}Common statuses:
| Status | When it happens | What to do |
| ------ | ------------------------------ | ------------------------------------- |
| 400 | Header missing or JSON invalid | Regenerate request |
| 403 | Tenant disabled | Visit dashboard to re‑enable |
| 409 | Same key but different payload | Ensure you retry with identical body |
| 425 | Another worker holds the lock | Retry after retryAfterMs |
| 500 | Temporary platform issue | SDK auto‑retries (up to maxRetries) |
Local testing tips
- Raw body: when using Express, configure
bodyParser.json({ verify })to stash the raw buffer onreq.rawBody. Syllecta needs it to recompute signatures exactly as the provider sent them. - Replay protection: set the
SYLLECTA_API_KEYfrom a sandbox tenant so you can replay events without affecting production dashboards. - Timeouts: adjust
timeoutMsandmaxRetriesin the client constructor if you are testing over high‑latency tunnels.
Frequently used helpers
| Helper | Description |
| --------------------- | ---------------------------------------------------------------- |
| CloudSDK | High-level API (client, idempotency, webhooks) |
| HttpClient | Low-level typed HTTP client with retry + typed API errors |
| SyllectaApiError | Non‑2xx API error with status, headers, and parsed body |
| forwardWebhookEvent | Sends a webhook payload straight to Syllecta |
| handleWebhookEvent | Verifies + forwards + conditionally runs business logic |
| verifySignature | Validates timestamped HMAC headers for inbound webhooks |
| webhookVerifier | Express middleware for validating signatures before JSON parsing |
Each helper ships with TypeScript definitions so your IDE can autocomplete parameters and responses.
Example integration blueprint
Putting everything together for a typical SaaS backend:
import express from "express";
import bodyParser from "body-parser";
import { CloudSDK, BearerAuth, handleWebhookEvent } from "@syllecta/sdk-js";
const sdk = new CloudSDK({
baseUrl: "https://api.syllecta.com/v1",
auth: BearerAuth(process.env.SYLLECTA_API_KEY!)
});
const app = express();
app.use(bodyParser.json({ verify: (req: any, _, buf) => (req.rawBody = buf) }));
app.post("/orders", async (req, res) => {
const key = `order_${req.body.orderId}`;
const scope = "POST:/orders";
const lock = await sdk.idempotency.checkOrLock({
key,
scope,
ttlSeconds: 120
});
if (lock.cached && lock.record) {
return res.status(lock.record.status).json(lock.record.body);
}
if (!lock.locked) {
return res.status(425).json({ retryAfterMs: lock.retryAfterMs ?? 250 });
}
// Your domain operation
const order = { orderId: req.body.orderId, accepted: true };
await sdk.idempotency.save({
key,
scope,
status: 201,
headers: { "content-type": "application/json" },
body: order
});
res.status(201).json(order);
});
app.post("/stripe/webhook", async (req, res) => {
await handleWebhookEvent({
provider: "stripe",
apiKey: process.env.SYLLECTA_API_KEY!,
signingSecret: process.env.STRIPE_SIGNING_SECRET!,
signatureHeader: "stripe-signature",
headers: { "stripe-signature": req.get("stripe-signature") ?? undefined },
rawBody: req.rawBody,
body: req.body,
onProcess: () => processStripeEvent(req.body)
});
res.json({ ok: true });
});
app.listen(3000, () => console.log("Syllecta integration ready on :3000"));This blueprint:
- Accepts REST writes with idempotency keys.
- Forwards Stripe webhooks and only processes them once.
- Keeps all errors observable via structured logs.
Development workflow (for contributors)
pnpm install
pnpm run dev # watch mode with tsup
pnpm test # vitest
pnpm run build # produces ESM + CJS bundles in dist/Publishing:
pnpm version patch # or minor/major
pnpm run build
pnpm publish --access publicRequirements
- Node.js >= 20
- TypeScript >= 5.6 (optional but recommended)
- Express >= 4 if you use the provided middleware
License
MIT © 2026 Syllecta
