Skip to content
adscapi

Offline conversions

Upload a CSV of offline conversions to one platform. Raw email/phone in, SHA-256 out — the platform never sees unhashed PII. One bad batch never blocks the rest.

Signature

ts
import { uploadOffline } from 'adscapi';

const result: OfflineResult = await uploadOffline({
  platform: 'bing' | 'google' | 'meta',
  csv: string,                 // header row + one conversion per line
  secrets?: AdscapiSecrets,    // default: process.env
  dryRun?: boolean,            // parse + hash + build, do not POST
});

Options

FieldTypeNotes
platform'bing' | 'google' | 'meta'Platforms with a documented offline-conversion import endpoint
csvstringRaw CSV text (header + rows). See columns below
secretsAdscapiSecrets?Defaults to process.env
dryRunboolean?Parse + hash + build payloads, but do not POST

CSV columns

Header is case-insensitive. Missing columns are fine; unknown columns are ignored; blank lines are dropped.

csv
email,phone,event,value,currency,timestamp,gclid,msclkid,fbclid
jane@example.com,,purchase,49.00,USD,1710000000,Cj0KCQ...,,
bob@acme.com,+14155550100,lead,,,2024-03-15T12:00:00Z,,abc123,
ColumnNotes
email / phoneRaw. Hashed before upload
eventCanonical name; defaults to purchase
value / currencyOptional conversion value
timestampUnix seconds, milliseconds, or ISO/RFC date string → unix seconds
gclid / msclkid / fbclidClick ids — required by some platforms (see below)

Per-platform requirements

  • Bing — needs MSADS_ACCESS_TOKEN, MSADS_DEV_TOKEN, MSADS_CUSTOMER_ID, MSADS_ACCOUNT_ID. A row needs at least one of msclkid / email / phone. Batches of 1,000.
  • Google — needs GOOGLE_ACCESS_TOKEN, GADS_CUSTOMER_ID, GADS_DEVELOPER_TOKEN, GADS_CONVERSION_ACTION_ID. Rows without a gclid are skipped. Batches of 2,000.
  • Meta — needs META_PIXEL_ID (dataset id) + META_CAPI_TOKEN. A row needs email or phone. action_source: physical_store; events older than 62 days are rejected. Batches of 1,000.

Missing secrets return { uploaded: 0, failed: 0, skipped: 'secrets absent' } — nothing throws.

Return — OfflineResult

ts
type OfflineResult = {
  platform: 'bing' | 'google' | 'meta';
  uploaded: number;          // rows POSTed inside a successful batch
  failed: number;            // rows inside a batch the platform rejected
  skipped: number | string;  // row count skipped, or a string reason when nothing ran
  error?: string;            // first batch failure detail, when failed > 0
  dryRun?: boolean;
};

Example

ts
import { uploadOffline } from 'adscapi';
import { readFileSync } from 'node:fs';

const csv = readFileSync('./conversions.csv', 'utf8');
// email,phone,event,value,currency,timestamp,gclid,msclkid,fbclid
// jane@example.com,,purchase,49,USD,1710000000,Cj0...,,
// bob@acme.com,+14155550100,purchase,120,USD,1710001000,,abc123,

// Dry-run first — counts what would upload
const preview = await uploadOffline({ platform: 'google', csv, dryRun: true });
console.log(preview);
// → { platform: 'google', uploaded: 1, failed: 0, skipped: 1, dryRun: true }

const result = await uploadOffline({ platform: 'google', csv });
if (result.error) console.error(result.failed, 'failed:', result.error);
console.log('uploaded', result.uploaded, 'skipped', result.skipped);
shell
# Same path via the CLI
npx adscapi offline --platform google --file ./conversions.csv
npx adscapi offline --platform meta --file ./conversions.csv --dry-run

Related