Skip to content
Lapis
Esc
navigateopen⌘Jpreview
On this page

Conversion API

Send verified conversion events from your backend to Lapis and attribute them to the original ChatGPT ad click.

The Conversion API sends verified conversion events from your backend to Lapis and attributes them to the original ChatGPT ad click. Use it whenever the final conversion happens somewhere the browser pixel cannot see it: inside your app, your checkout, your CRM, or any other backend system.

1. Prerequisites

The Conversion API attributes each event using a Lapis click ID that was captured on the ad landing page. Set up this chain first:

  1. Install the Lapis Pixel on the landing page.
  2. Make sure your ad URLs include utm_source=chatgpt (or openai).
  3. Read the click ID from the lapis_sid cookie and pass it into your app, signup, or checkout flow as lapis_click_id.
  4. Store lapis_click_id on the user, lead, checkout, account, or order record.
  5. When the conversion is confirmed server-side, POST it to Lapis (see below).

If Lapis manages your landing page, you skip the cookie step: the lapis_click_id is handed to your app in the destination URL instead. Steps 4 and 5 stay exactly the same.

2. Authentication

Authenticate every request with a Lapis Conversion API key in the Authorization header:

Authorization: Bearer sk_live_...

Conversion API keys are different from public pixel keys. Public pixel keys start with pk_ and are safe to embed in a browser. Conversion API keys start with sk_live_ and must live only on your backend.

Tip: Conversion API events count as verified precisely because they are authenticated with a backend-only secret key. Keep that key out of all browser and mobile code.

3. Endpoint

POST https://api.trylapis.com/api/v1/pixel/server-events
Authorization: Bearer sk_live_...
Content-Type: application/json

Every request body must include an event_id. Lapis uses that value to handle retries safely, so the same conversion is never counted twice.

4. Request fields

Field Required Type Description Example
event_type Required string Client-defined event name. Must be lowercase snake_case. signup_completed
lapis_click_id Required string The Lapis click/session ID captured from the landing page. mpf0phjdj7jpyqf4h
event_id Required string Stable, unique ID for this event. Used to deduplicate retries. signup_completed:respan_user_123
occurred_at Optional datetime When the event happened in your system. Use ISO 8601. 2026-06-17T18:00:00Z
external_user_id Optional string Your internal user ID. Lapis stores a hash, not the raw value. respan_user_123
value Optional number Event value in dollars. Use 0 or omit for non-revenue events. 99.00
currency Optional string Three-letter currency code. Defaults to USD. USD
properties Optional object Non-PII metadata for debugging and reporting. Must be JSON serializable, up to 32 KB. {"plan":"team"}

Example request body

{
  "event_id": "signup_completed:respan_user_123",
  "event_type": "signup_completed",
  "lapis_click_id": "mpf0phjdj7jpyqf4h",
  "occurred_at": "2026-06-17T18:00:00Z",
  "external_user_id": "respan_user_123",
  "value": 0,
  "currency": "USD",
  "properties": {
    "plan": "free",
    "source": "platform.respan.ai"
  }
}

Lapis also accepts an OpenAI-style batch ({"events": [...]} with id, type, timestamp_ms, and data) and normalizes it automatically, including validate_only support. So if you already send server events to OpenAI, you can integrate by duplicating the call and adding lapis_click_id.

5. Responses

Matched event

{
  "accepted": true,
  "matched": true,
  "duplicate": false,
  "event_id": "signup_completed:respan_user_123",
  "event_type": "signup_completed",
  "session_id": "mpf0phjdj7jpyqf4h",
  "campaign": "Respan_Observability_14cluster",
  "ad_group": "RSPN_QualityReview_1410",
  "ad_id": "a_clearer_ai_review_queue"
}

Accepted but unmatched

{
  "accepted": true,
  "matched": false,
  "duplicate": false,
  "event_id": "signup_completed:respan_user_123",
  "event_type": "signup_completed",
  "reason": "unknown_lapis_click_id"
}

Status codes

Status Meaning What to do
200 The event matched an ad click session, or was a duplicate of an already accepted event. Treat as success.
202 The event was accepted, but Lapis could not match the click ID to an active attribution session. Treat as delivered, then inspect the reason field.
401 Missing or invalid Conversion API key. Check the Authorization header and key value.
403 The click ID belongs outside this Conversion API key scope. Use the key scoped to the same public pixel key.
422 Invalid payload, event name, or missing event_id. Fix the request body before retrying.
413 The properties payload is too large. Send fewer properties.

6. Idempotency

Generate a stable event_id for each event. If your backend retries the same request, Lapis returns the original attribution result with duplicate: true and does not count the event again.

Build the ID from the business object it represents:

signup_completed:<your_user_id>
subscription_started:<your_subscription_id>
purchase_completed:<your_order_id>

Warning: Do not generate a new random ID on every retry. Random retry IDs defeat deduplication and can overcount your conversions.

7. Examples

cURL

curl https://api.trylapis.com/api/v1/pixel/server-events \
  -X POST \
  -H "Authorization: Bearer $LAPIS_SERVER_EVENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event_id": "signup_completed:respan_user_123",
    "event_type": "signup_completed",
    "lapis_click_id": "mpf0phjdj7jpyqf4h",
    "external_user_id": "respan_user_123",
    "properties": {
      "plan": "free"
    }
  }'

Node.js backend

type LapisConversionEvent = {
  event_type: string;
  lapis_click_id: string;
  event_id: string;
  external_user_id?: string;
  value?: number;
  currency?: string;
  properties?: Record<string, unknown>;
};

export async function sendLapisConversionEvent(event: LapisConversionEvent) {
  const response = await fetch(
    "https://api.trylapis.com/api/v1/pixel/server-events",
    {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.LAPIS_SERVER_EVENT_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(event),
    }
  );

  const result = await response.json();

  if (!response.ok) {
    throw new Error(`Lapis Conversion API event failed: ${response.status}`);
  }

  return result;
}

Signup completed

await sendLapisConversionEvent({
  event_id: `signup_completed:${user.id}`,
  event_type: "signup_completed",
  lapis_click_id: user.lapisClickId,
  external_user_id: user.id,
  value: 0,
  currency: "USD",
  properties: {
    plan: user.plan,
    source: "platform.respan.ai",
  },
});

For business-specific event choices and payloads, see the event guides.

8. Security

  • Store sk_live_ keys in a backend secret manager or environment variable.
  • Never send Conversion API keys to browser, mobile, or any third-party frontend code.
  • Use separate keys per environment, and rotate a key immediately if it is exposed.
  • Prefer keys scoped to a specific public pixel key when you have multiple websites or clients.
  • Do not put emails, names, phone numbers, or other raw PII in properties.

FAQ

Can I send arbitrary event types?

Yes. Event names are client-defined, as long as they use lowercase snake_case. For example: signup_completed, workspace_created, or subscription_started.

Does every event count as a conversion?

No. Lapis stores all valid events, but only the event types you configure as conversions update your conversion aggregates.

What if the click ID is unknown?

Lapis returns 202 with matched: false and a reason such as unknown_lapis_click_id. The event is accepted as delivered, but it is not attributed to a campaign.

Should Lapis or my backend generate event IDs?

Your backend should, because it knows the stable business object behind the event: the user ID, order ID, subscription ID, or checkout session ID.

Can I call this endpoint from client-side code?

No. The endpoint uses a secret key, so call it only from trusted server-side code.

Was this page helpful?