Webhooks & rate limits
Signed webhook delivery for your own systems, plus two small helpers for pacing bulk work. WebCrypto only — Workers-safe.
signPayload / sendWebhook
ts
import { signPayload, sendWebhook } from 'adscapi';
// Hex HMAC-SHA256 of `body` with `secret` (exported so receivers can verify)
const hex: string = await signPayload(secret: string, body: string);
// POST JSON to a caller URL with X-Adscapi-Signature: sha256=<hex>
await sendWebhook({
url: string,
secret: string,
payload: unknown,
fetchImpl?: typeof fetch, // inject for tests / custom runtimes
});
// Throws on non-2xx: Error('Webhook delivery failed: HTTP <status>')Header format: X-Adscapi-Signature: sha256=<hexhmac> over the exact JSON body string. Uses crypto.subtle (WebCrypto) — works in Node 18+, Bun, Deno, and Cloudflare Workers.
Rate-limit helpers
ts
import { boundedConcurrency, TokenBucket } from 'adscapi';
// Run tasks with at most `limit` in flight. Results in input order.
// A rejected task rejects the whole call (like Promise.all).
const results = await boundedConcurrency(limit: number, tasks: Array<() => Promise<T>>);
// In-memory token bucket for pacing calls to a rate-limited API.
const bucket = new TokenBucket({
tokensPerInterval: number, // capacity + refill amount
intervalMs: number, // refill period
now?: () => number, // injectable clock (tests)
});
await bucket.removeTokens(1); // waits until a token is available
bucket.tokens; // current fill (refills lazily)boundedConcurrency— cap in-flight work.limit < 1throws.TokenBucket— in-memory only. Fine in a long-running Node process. On serverless/Workers (no shared memory across requests) use a Durable Object or KV counter instead.removeTokens(n)rejects ifnexceeds bucket capacity. Defaultn = 1.
Example
ts
import { createAdscapi, sendWebhook, signPayload, boundedConcurrency, TokenBucket } from 'adscapi';
const ads = createAdscapi();
const WEBHOOK_SECRET = process.env.ADSCAPI_WEBHOOK_SECRET!;
// Notify your own system after every track() fan-out
const results = await ads.conversions.track(event);
await sendWebhook({
url: 'https://hooks.example.com/adscapi',
secret: WEBHOOK_SECRET,
payload: { eventId: event.eventId, results },
});
// Receiver side — verify the signature
export async function POST(req: Request) {
const body = await req.text(); // exact bytes
const expected = await signPayload(WEBHOOK_SECRET, body);
const got = req.headers.get('x-adscapi-signature')?.replace(/^sha256=/, '');
if (got !== expected) return new Response('bad signature', { status: 401 });
// …handle JSON.parse(body)
return new Response('ok');
}
// Pace a bulk backfill against a picky API
const bucket = new TokenBucket({ tokensPerInterval: 10, intervalMs: 1000 });
await boundedConcurrency(5, rows.map((row) => async () => {
await bucket.removeTokens(1);
return ads.conversions.track(row);
}));