Skip to content
adscapi

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 < 1 throws.
  • 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 if n exceeds bucket capacity. Default n = 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);
}));

Related