Skip to main content
Send conversion or marketing events for attribution and optimization. Supports batch submissions, test events, and partial failure reporting. Response Time: ~1s (events are queued for processing) Request Schema: /schemas/v2/media-buy/log-event-request.json Response Schema: /schemas/v2/media-buy/log-event-response.json

Quick Start

Log a purchase event:
import { testAgent } from "@adcp/client/testing";
import { LogEventResponseSchema } from "@adcp/client";

const result = await testAgent.logEvent({
  event_source_id: "website_pixel",
  events: [
    {
      event_id: "evt_purchase_12345",
      event_type: "purchase",
      event_time: "2026-01-15T14:30:00Z",
      action_source: "website",
      event_source_url: "https://www.example.com/checkout/confirm",
      user_match: {
        click_id: "abc123def456",
        click_id_type: "gclid",
      },
      custom_data: {
        value: 149.99,
        currency: "USD",
        order_id: "order_98765",
        num_items: 3,
      },
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

// Validate response against schema
const validated = LogEventResponseSchema.parse(result.data);

// Check for operation-level errors first (discriminated union)
if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

if ("events_received" in validated) {
  console.log(`Received: ${validated.events_received}, Processed: ${validated.events_processed}`);
  if (validated.match_quality !== undefined) {
    console.log(`Match quality: ${(validated.match_quality * 100).toFixed(0)}%`);
  }
}

Request Parameters

ParameterTypeRequiredDescription
event_source_idstringYesEvent source configured on the account via sync_event_sources
eventsEvent[]YesEvents to log (min 1, max 10,000)
test_event_codestringNoTest event code for validation without affecting production data

Event Object

FieldTypeRequiredDescription
event_idstringYesUnique identifier for deduplication (scoped to event_type + event_source_id). Max 256 chars.
event_typeEventTypeYesStandard event type (e.g. purchase, lead, add_to_cart)
event_timedate-timeYesISO 8601 timestamp when the event occurred
user_matchUserMatchNoUser identifiers for attribution matching
custom_dataCustomDataNoEvent-specific data (value, currency, items)
action_sourceActionSourceNoWhere the event occurred (website, app, in_store, etc.)
event_source_urluriNoURL where the event occurred (required when action_source is website)
custom_event_namestringNoName for custom events (used when event_type is custom)

User Match Object

At least one of uids, hashed_email, hashed_phone, click_id, or client_ip + client_user_agent is required:
FieldTypeDescription
uidsUID[]Universal ID values (rampid, id5, uid2, euid, pairid, maid)
hashed_emailstringSHA-256 hash of lowercase, trimmed email address (64-char hex)
hashed_phonestringSHA-256 hash of E.164-formatted phone number (64-char hex)
click_idstringPlatform click identifier (fbclid, gclid, ttclid, etc.)
click_id_typestringType of click identifier
client_ipstringClient IP address for probabilistic matching
client_user_agentstringClient user agent string for probabilistic matching
Hashing: Hashed identifiers must be SHA-256 hex strings (64 characters, lowercase). Normalize before hashing: emails to lowercase with whitespace trimmed, phone numbers to E.164 format (e.g. +12065551234).

Custom Data Object

FieldTypeDescription
valuenumberMonetary value of the event
currencystringISO 4217 currency code (e.g. USD, EUR, GBP)
order_idstringUnique order or transaction identifier
content_idsstring[]Product or content identifiers
content_typestringCategory of content (product, service, etc.)
num_itemsintegerNumber of items in the event
contentsContent[]Per-item details (id, quantity, price, brand)

Response

Success Response:
  • events_received - Number of events received
  • events_processed - Number of events successfully queued
  • partial_failures - Events that failed validation (with event_id, code, message)
  • warnings - Non-fatal issues (low match quality, missing fields)
  • match_quality - Overall match quality score (0.0 to 1.0)
Error Response:
  • errors - Array of operation-level errors (invalid event source, auth failure)
Note: Responses use discriminated unions - you get either success fields OR errors, never both. Partial failures are reported per-event within the success response.

Common Scenarios

Batch Events

Send multiple events in a single request:
import { testAgent } from "@adcp/client/testing";
import { LogEventResponseSchema } from "@adcp/client";

const result = await testAgent.logEvent({
  event_source_id: "website_pixel",
  events: [
    {
      event_id: "evt_purchase_001",
      event_type: "purchase",
      event_time: "2026-01-15T10:00:00Z",
      action_source: "website",
      user_match: {
        hashed_email: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
        uids: [{ type: "uid2", value: "AbC123XyZ..." }],
      },
      custom_data: {
        value: 89.99,
        currency: "USD",
        order_id: "order_001",
      },
    },
    {
      event_id: "evt_lead_002",
      event_type: "lead",
      event_time: "2026-01-15T11:30:00Z",
      action_source: "website",
      user_match: {
        hashed_email: "f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5",
        click_id: "abc123def456",
        click_id_type: "fbclid",
      },
    },
    {
      event_id: "evt_cart_003",
      event_type: "add_to_cart",
      event_time: "2026-01-15T12:15:00Z",
      action_source: "app",
      user_match: {
        uids: [{ type: "rampid", value: "Def456Ghi..." }],
      },
      custom_data: {
        content_ids: ["SKU-1234", "SKU-5678"],
        num_items: 2,
        value: 45.00,
        currency: "USD",
      },
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = LogEventResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

if ("events_received" in validated) {
  console.log(`${validated.events_processed}/${validated.events_received} events processed`);
  if (validated.partial_failures?.length) {
    for (const failure of validated.partial_failures) {
      console.log(`  Failed: ${failure.event_id} - ${failure.message}`);
    }
  }
}

Test Events

Validate event integration without affecting production data:
import { testAgent } from "@adcp/client/testing";
import { LogEventResponseSchema } from "@adcp/client";

const result = await testAgent.logEvent({
  event_source_id: "website_pixel",
  test_event_code: "TEST_12345",
  events: [
    {
      event_id: "test_evt_001",
      event_type: "purchase",
      event_time: new Date().toISOString(),
      action_source: "website",
      event_source_url: "https://www.example.com/checkout",
      user_match: {
        click_id: "test_click_abc",
        click_id_type: "gclid",
      },
      custom_data: {
        value: 99.99,
        currency: "USD",
      },
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = LogEventResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

if ("events_received" in validated) {
  console.log("Test event sent successfully");
  if (validated.warnings?.length) {
    console.log("Warnings:", validated.warnings);
  }
}
Test events appear in the seller’s test events UI but do not affect production attribution or reporting.

In-Store Conversions

Report offline conversions using CRM data:
import { testAgent } from "@adcp/client/testing";
import { LogEventResponseSchema } from "@adcp/client";

const result = await testAgent.logEvent({
  event_source_id: "crm_import",
  events: [
    {
      event_id: "store_txn_20260115_001",
      event_type: "purchase",
      event_time: "2026-01-15T16:45:00Z",
      action_source: "in_store",
      user_match: {
        uids: [{ type: "rampid", value: "XyZ789AbC..." }],
      },
      custom_data: {
        value: 250.0,
        currency: "USD",
        order_id: "POS-2026-0115-001",
        contents: [
          { id: "SKU-JACKET-L", quantity: 1, price: 189.0 },
          { id: "SKU-SCARF-01", quantity: 1, price: 61.0 },
        ],
      },
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = LogEventResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

if ("events_received" in validated) {
  console.log(`In-store events processed: ${validated.events_processed}`);
}

Event Deduplication

Events are deduplicated by the combination of event_id + event_type + event_source_id. Sending the same event multiple times is safe - duplicates are silently ignored. Choose event_id values that are stable across retries:
  • Transaction IDs: "order_98765"
  • Composite keys: "purchase_user123_20260115"
  • UUIDs: "550e8400-e29b-41d4-a716-446655440000"

Error Handling

Error CodeDescriptionResolution
EVENT_SOURCE_NOT_FOUNDEvent source not configuredRun sync_event_sources first
INVALID_EVENT_TYPEUnrecognized or disallowed event typeCheck event source’s event_types configuration
INVALID_EVENT_TIMEEvent time too far in the past/futureUse timestamps within the seller’s attribution window
MISSING_USER_MATCHNo user identifiers providedInclude at least one of: uids, hashed_email, hashed_phone, click_id, or client_ip + client_user_agent
BATCH_TOO_LARGEMore than 10,000 eventsSplit into smaller batches
RATE_LIMIT_EXCEEDEDToo many requestsWait and retry with exponential backoff

Best Practices

  1. Configure sources first - Always run sync_event_sources before sending events. Events sent to unconfigured sources are rejected.
  2. Include user_match - Events without user identifiers cannot be attributed. Provide the strongest identifiers available: hashed email/phone > UIDs > click IDs > IP/UA. Send multiple identifier types when available to maximize match rates.
  3. Use test events first - Set test_event_code during integration to validate events appear correctly without affecting production data.
  4. Batch when possible - Send up to 10,000 events per request to reduce API calls. Events within a batch are processed independently.
  5. Include value and currency - For purchase events, always include custom_data.value and custom_data.currency to enable ROAS reporting and optimization.
  6. Stable event IDs - Use deterministic event IDs (order numbers, transaction IDs) rather than random UUIDs. This ensures safe retries without duplicate counting.
  7. Send events promptly - Log events as close to real-time as possible. Events outside the seller’s attribution window may not be matched.

Next Steps