TypeScript
A typed fetch wrapper around the real ingestion contract — no package to install, because none is published yet.
This is not an installable package
See SDKs overview — there’s no published npm package for Retidal today. Everything below
is a small TypeScript module you add to your own project, built directly from the documented
event-ingestion and identify contracts. It
compiles with fetch, which is available natively in Node 18+, browsers, and edge runtimes — no HTTP
library dependency either.
Add the module
Section titled “Add the module”Create retidal.ts in your project:
// Mirrors the documented IngestEvent / IngestAsyncResult / IngestSyncResult// shapes returned by POST /api/v1/t (Ingestion & Decisioning API) — see// /docs/sending-data/events.
export interface RetidalEvent { eventName: string; visitorId?: string; userId?: string; sessionId?: string; properties?: Record<string, unknown>; clientTimestamp?: string; eventId?: string; shortLinkId?: string; shortLinkCode?: string; clickIds?: Record<string, string>;}
export interface IngestAsyncResult { accepted: number; failed: number; queued: number; processed: false; invalid?: Array<{ index: number; rawSample: string; reason: string }>;}
export interface IngestSyncResult { accepted: number; failed: number; queued: number; processed: true; errors?: Array<{ index: number; eventName: string; errors: Array<{ field: string; rule: string; expected?: unknown; actual?: unknown; message: string }>; }>; enforceRejected?: number;}
export interface RetidalIdentifyTraits { email?: string | null; phone?: string | null; name?: string | null; locale?: string | null; timezone?: string | null; [key: string]: unknown;}
export interface RetidalClientConfig { /** Ingestion base URL. Production: https://api.retidal.com */ apiUrl: string; apiKey: string;}
export class RetidalApiError extends Error { constructor( message: string, public readonly status: number, public readonly body: unknown ) { super(message); this.name = "RetidalApiError"; }}
export class RetidalClient { constructor(private readonly config: RetidalClientConfig) {}
/** Fire-and-forget style: default async 202 path. Returns accepted/failed/queued counts. */ async track(event: RetidalEvent): Promise<IngestAsyncResult> { return this.post<IngestAsyncResult>("/api/v1/t", event); }
/** Batch variant of track() — up to 100 events per call. */ async trackBatch(events: RetidalEvent[]): Promise<IngestAsyncResult> { if (events.length > 100) { throw new Error("Retidal accepts at most 100 events per batch request."); } return this.post<IngestAsyncResult>("/api/v1/t", { events }); }
/** * Synchronous verification path (?sync=1). `processed: true` is the only * proof a non-metering event reached storage. Use for health checks and * integration tests, not production traffic (it trades latency for that * guarantee) — see /docs/sending-data/events. */ async trackSync(event: RetidalEvent): Promise<IngestSyncResult> { return this.post<IngestSyncResult>("/api/v1/t?sync=1", event); }
/** Link an anonymous visitor to a logged-in user. See /docs/sending-data/identify. */ async identify( visitorId: string, userId: string, traits?: RetidalIdentifyTraits ): Promise<{ ok: true; dropped_keys: string[] }> { return this.post("/api/v1/identify", { visitorId, userId, ...(traits ? { traits } : {}) }); }
private async post<T>(path: string, body: unknown): Promise<T> { const response = await fetch(`${this.config.apiUrl}${path}`, { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": this.config.apiKey, }, body: JSON.stringify(body), });
let data: unknown; try { data = await response.json(); } catch { data = null; }
// 200 (sync) and 202 (async) are both success. Everything else is an error, // including 422 (enforce mode rejected every event) — see error handling below. if (response.status !== 200 && response.status !== 202) { const message = (data as { error?: string } | null)?.error ?? `Retidal request failed with ${response.status}`; throw new RetidalApiError(message, response.status, data); }
return data as T; }}import { RetidalClient } from "./retidal";
const retidal = new RetidalClient({ apiUrl: "https://api.retidal.com", apiKey: process.env.RETIDAL_API_KEY!,});
// Fire-and-forget eventconst result = await retidal.track({ eventName: "user_paid", visitorId: "vid_abc123", userId: "uid_456", properties: { amount: 9900, currency: "CNY" }, // amount is in minor units — 9900 = ¥99.00});console.log(result); // { accepted: 1, failed: 0, queued: 0, processed: false }
// Link the visitor to the logged-in user, with a trait that unlocks event-triggered emailawait retidal.identify("vid_abc123", "uid_456", { email: "user@example.com" });202 is not proof of persistence
track() returns as soon as the event passes validation and is dispatched to background processing — it
does not prove the event reached storage. Use trackSync() only to verify a specific integration path
works end to end; keep production traffic on track(). Full explanation in
Sending events § The response contract.
Error handling
Section titled “Error handling”post() throws a RetidalApiError (carrying status and the parsed body) for anything POST /api/v1/t
returns that isn’t 200 or 202. Handle the documented status codes explicitly rather than treating every
throw the same way:
import { RetidalApiError } from "./retidal";
// Status codes below are POST /api/v1/t's documented responses.try { await retidal.track({ eventName: "user_paid", visitorId: "vid_abc123" });} catch (err) { if (err instanceof RetidalApiError) { switch (err.status) { case 400: // Malformed JSON, or batch over 100 events. console.error("Bad request:", err.body); break; case 401: // Missing or invalid X-API-Key. console.error("Check your API key."); break; case 422: // Enforce-mode validation rejected every event in the batch. console.error("Validation rejected:", err.body); break; case 503: { // Transient: SELECTOR_LOOKUP_FAILED or QUEUE_UNAVAILABLE — both carry retryAfter. const retryAfter = (err.body as { retryAfter?: number })?.retryAfter ?? 30; console.error(`Retidal temporarily unavailable, retry after ${retryAfter}s`); break; } default: throw err; } } else { throw err; }}See Errors & status codes for the full status-code reference across every endpoint, not just ingestion.
Next steps
Section titled “Next steps”Every endpoint, generated request/response schemas, and multi-language examples.
Trait rules, PII handling, and why identify has to run before the event you want to trigger email from.