Skip to content
adscapi

Dedup & retries

Two layers of protection: share one eventId so platforms merge browser pixel + server CAPI into a single conversion, and optionally plug in a DedupeStore so your own process doesn't re-send the same eventId:platform pair. Transient HTTP failures retry automatically.

eventId — pixel + CAPI dedup

Platforms that support dual setup (pixel + Conversions API) dedupe on a shared event id. Set the same stable string on the browser pixel and on ConversionEvent.eventId. You get full coverage without double-counting.

ts
// Browser pixel (Meta example) — same eventID the server will send
fbq('track', 'Purchase', { value: 49, currency: 'USD' }, { eventID: orderId });

// Server CAPI — same stable id
await ads.conversions.track({
  name: 'purchase',
  value: 49,
  currency: 'USD',
  eventId: orderId,            // ← shared with the pixel
  transactionId: orderId,
  user: { email: buyer.email },
  consent: { adUserData: true, adPersonalization: true },
});
ts
// When eventId is absent, the client derives:
//   `${name}-${eventTime ?? ''}-${transactionId ?? user.email ?? ''}`
// Override whenever you have a better stable id (order id, checkout id).

Prefer an order id or checkout id over the auto-derived form whenever you have one — it’s stable across retries and across pixel/server.

DedupeStore — skip already-sent pairs

Optional client hook. Key is ${eventId}:${platform}. After a successful send the client calls add(key); on the next run seen(key) short-circuits dispatch and the result comes back with deduped: true.

ts
import { createAdscapi, type DedupeStore } from 'adscapi';

// You own the store — Redis, KV, Postgres, in-memory for tests.
const dedupe: DedupeStore = {
  async seen(key) { return redis.sismember('adscapi:dedupe', key); },
  async add(key)  { await redis.sadd('adscapi:dedupe', key); },
};

const ads = createAdscapi(process.env, { dedupe });

// key = `${eventId}:${platform}`
// Already-sent keys return { ok: true, deduped: true } and skip the HTTP call.
const results = await ads.conversions.track(event);

This is process-level / store-level idempotency — separate from the platform’s own pixel/CAPI dedup. Use both.

Retries

ts
// Built into every platform dispatch (src/retry.ts):
// - 3 tries
// - retry on 5xx, 429, and network errors
// - 4xx is terminal (bad payload won't fix itself)
// - backoff: 200ms, 400ms, 800ms (2 ** attempt * 200)

Adapters throw on failure; the client wraps with withRetry and reports per-destination. One platform failing never blocks the rest — see Errors.

DestinationResult flags

ts
type DestinationResult = {
  platform: string;
  ok: boolean;
  error?: string;
  dryRun?: boolean;   // opts.dryRun
  deduped?: boolean;  // DedupeStore saw this eventId:platform
};

Related