Verified conversions & revenue
Record authoritative lifecycle and revenue events from a trusted server
The verified Conversion API records CRM, checkout, billing, or backend events that should not be trusted to a public browser key. It validates the complete payload, stores verification provenance, and computes attribution when analytics consent and an active attribution definition are available.
Endpoint and authentication
POST $CONVEX_SITE_URL/v1/conversions
Authorization: Bearer cvk_live_<key-id>_<secret>
Content-Type: application/jsonThe route is registered on the configured Realanalytics Convex HTTP-actions host. Authenticate with a dedicated conversion key issued by a project owner or admin. Its plaintext is returned once. Rotation permits two active keys to overlap until the old key is explicitly revoked. The key is server-only: store it in a secret manager and never expose it through NEXT_PUBLIC_*, HTML, browser JavaScript, logs, or source control.
Realanalytics stores only a verifier hash. Project owners and admins can list the key's prefix, last four characters, creation time, last-used time, and revocation state; the complete key cannot be retrieved again.
Requests must be JSON and no larger than 64 KiB. The endpoint rejects unknown fields instead of silently ignoring contract mistakes.
Node and Bun client
npm install @realanalytics/serverimport { createServerClient } from '@realanalytics/server'
const analytics = createServerClient({
conversionKey: process.env.REALANALYTICS_CONVERSION_KEY!,
endpoint: `${process.env.CONVEX_SITE_URL}/v1/conversions`,
})
await analytics.trackConversion({
conversionId: 'purchase_1001',
transactionId: 'order_1001',
stage: 'customer',
kind: 'purchase',
acquisitionOrigin: 'website_form',
metaEventContext: 'system_generated',
occurredAt: '2026-07-10T10:00:00Z',
personId: 'person_123',
value: 199.5,
currency: 'GBP',
consent: {
analytics: 'granted',
adUserData: 'denied',
adPersonalization: 'denied',
recordedAt: '2026-07-10T09:30:00Z',
source: 'cmp:your-provider',
},
})The server client enforces the same payload rules before sending, applies a per-attempt timeout, and retries only network failures, HTTP 429, and HTTP 5xx responses with bounded backoff. It is not part of the browser SDK and rejects public project keys.
Server client lifetime
Create the server client for the lifetime your server runtime owns. Callanalytics.shutdown() only during its real worker, process, or test teardown—not after each conversion. Shutdown aborts active requests and retry waits, is idempotent, and permanently stops that client instance. An affected or later trackConversion() call rejects with a non-retryable RealanalyticsServerError whose code isCLIENT_SHUTDOWN.
Lifecycle stage and conversion kind
stage answers where the person is in the customer lifecycle.kind answers what happened financially or operationally. Keep them separate.
| Field | Allowed values | Money rules |
|---|---|---|
stage | lead, qualified_lead, booked_call, customer | Purchase, renewal, and refund require customer. |
kind | lifecycle, free_trial, purchase, renewal, refund | Lifecycle and free trial reject money. Revenue kinds require value and currency. |
If eventName is omitted, it is derived deterministically. Lifecycle stages become lead_created, lead_qualified,call_booked, or customer_created. Other kinds becomefree_trial_started, purchase_completed,subscription_renewed, or payment_refunded.
Core request fields
| Field | Requirement |
|---|---|
conversionId | Required idempotency identifier, up to 200 characters. |
transactionId | Required source transaction identifier, up to 200 characters. |
stage / kind | Both are required and must use the values above. |
acquisitionOrigin | Required acquisition source: website_form, meta_instant_form, crm_manual, or other. |
metaEventContext | Required Meta action source: website, system_generated, email, phone_call, chat, physical_store, or other. Choose where the event actually occurred; it is never inferred from request transport. |
occurredAt | Required RFC 3339 timestamp with a timezone. |
personId / anonymousId | At least one opaque identity is required. |
consent | Required complete event-time consent snapshot. |
eventName | Optional override, up to 100 characters. |
conversionId and transactionId must begin with an alphanumeric character and may otherwise contain letters, numbers, dots, underscores, colons, or hyphens.
Required identity and consent
Every request requires personId or anonymousId. These must be opaque internal identifiers without whitespace or @; do not put an email address or phone number in either field. Supplying both lets the service record an identity edge when analytics consent is granted.
"consent": {
"analytics": "granted",
"adUserData": "denied",
"adPersonalization": "denied",
"recordedAt": "2026-07-10T09:30:00Z",
"source": "cmp:your-provider"
}All three consent values are required and accept granted,denied, or unknown. The API still records a verified conversion when analytics is denied, but attribution is skipped. SendingclickIds or userData requiresadUserData: granted. Google receives the explicit advertising personalization status if that optional destination is later used.
For a Meta Instant Form lead, set acquisitionOrigin tometa_instant_form and pass the leadgen identifier asuserData.metaLeadId. It must be an exact 15–17 digit string; Realanalytics never converts it to a number.
Lifecycle example
{
"conversionId": "qualified_lead_1001",
"transactionId": "lead_1001_qualified",
"stage": "qualified_lead",
"kind": "lifecycle",
"acquisitionOrigin": "website_form",
"metaEventContext": "system_generated",
"occurredAt": "2026-07-10T10:00:00Z",
"personId": "person_123",
"consent": {
"analytics": "granted",
"adUserData": "denied",
"adPersonalization": "denied",
"recordedAt": "2026-07-10T09:30:00Z",
"source": "cmp:your-provider"
}
}Lifecycle events cannot include value or currency.
Purchase example
curl -X POST "$CONVEX_SITE_URL/v1/conversions" \
-H "Authorization: Bearer $REALANALYTICS_CONVERSION_KEY" \
-H "Content-Type: application/json" \
-d '{
"conversionId": "purchase_1001",
"transactionId": "order_1001",
"stage": "customer",
"kind": "purchase",
"acquisitionOrigin": "website_form",
"metaEventContext": "system_generated",
"occurredAt": "2026-07-10T10:00:00Z",
"personId": "person_123",
"value": 199.5,
"currency": "GBP",
"sourceUrl": "https://example.com/checkout/complete",
"consent": {
"analytics": "granted",
"adUserData": "denied",
"adPersonalization": "denied",
"recordedAt": "2026-07-10T09:30:00Z",
"source": "cmp:your-provider"
},
"metadata": {
"plan": "pro",
"seats": 3
}
}'Values use major currency units and must be positive finite numbers. Currency is normalized to uppercase and must be a recognized ISO 4217 code.
Renewal example
{
"conversionId": "renewal_invoice_2001",
"transactionId": "invoice_2001",
"stage": "customer",
"kind": "renewal",
"acquisitionOrigin": "website_form",
"metaEventContext": "system_generated",
"occurredAt": "2026-08-10T10:00:00Z",
"personId": "person_123",
"value": 99,
"currency": "GBP",
"consent": {
"analytics": "granted",
"adUserData": "denied",
"adPersonalization": "denied",
"recordedAt": "2026-07-10T09:30:00Z",
"source": "cmp:your-provider"
}
}Free-trial example
{
"conversionId": "trial_3001",
"transactionId": "trial_3001",
"stage": "lead",
"kind": "free_trial",
"acquisitionOrigin": "website_form",
"metaEventContext": "website",
"occurredAt": "2026-07-10T10:00:00Z",
"anonymousId": "anon_456",
"consent": {
"analytics": "granted",
"adUserData": "unknown",
"adPersonalization": "unknown",
"recordedAt": "2026-07-10T09:30:00Z",
"source": "cmp:your-provider"
}
}Free trials cannot include value or currency.
Refund example
{
"conversionId": "refund_4001",
"transactionId": "refund_tx_4001",
"originalTransactionId": "order_1001",
"stage": "customer",
"kind": "refund",
"acquisitionOrigin": "website_form",
"metaEventContext": "system_generated",
"occurredAt": "2026-07-11T09:00:00Z",
"personId": "person_123",
"value": 25,
"currency": "GBP",
"consent": {
"analytics": "granted",
"adUserData": "denied",
"adPersonalization": "denied",
"recordedAt": "2026-07-10T09:30:00Z",
"source": "cmp:your-provider"
}
}Send the refund amount as a positive value. It is stored as a negative attribution adjustment. originalTransactionId must reference thetransactionId of an active verified purchase or renewal with the same identity and currency. Cumulative refunds cannot exceed the original transaction value, and the refund's own transactionId must differ.
Idempotency and retries
conversionId is the idempotency key; there is no separate Idempotency-Key header. Re-send the same complete payload with the sameconversionId after a timeout or retryable server error. The API fingerprints the canonical payload, so object key order does not matter.
- An exact retry returns HTTP 200 with
status: duplicate. - A new conversion returns HTTP 201 with
status: created. - Reusing a conversion ID for different data returns HTTP 409.
- A transaction and lifecycle stage can be recorded only once per project.
Success response
{
"conversionId": "purchase_1001",
"transactionId": "order_1001",
"kind": "purchase",
"environment": "live",
"status": "created",
"attribution": {
"status": "created",
"configId": "paid-acquisition",
"configVersion": 3
}
}environment is derived from the conversion key and is alwayslive for cvk_live_ or test forcvk_test_. The server client rejects a success response whose environment does not match the key used for the request.
Attribution may instead be skipped with a reason when analytics consent is not granted or there is no active attribution config. A refund may return no_original_attribution when the original transaction has no attribution result to adjust.
Success does not mean ad-platform delivery
This response confirms verified ingestion and attribution computation. It does not confirm a Meta or Google upload. Both destinations are optional and off by default. Attio is also optional and is only an alternative CRM source; the direct API does not require it or enable it.
Error behavior
| Status | Meaning | Retry? |
|---|---|---|
| 400 | Invalid JSON or payload validation error | Fix the request |
| 401 | Missing, malformed, revoked, or unknown conversion key | Fix authentication |
| 402 | Monthly project conversion quota exceeded | Upgrade or wait for the next UTC month |
| 405 / 415 | Wrong method or content type | Fix the request |
| 409 | Idempotency or transaction-stage collision | Do not retry with changed data |
| 413 | Body exceeds 64 KiB | Reduce the body |
| 422 | Refund conflicts with the original conversion | Fix refund linkage, identity, currency, or value |
| 429 | Project conversion rate limit exceeded | Retry after the response's Retry-After delay |
| 500 | { "error": "Conversion ingestion failed", "retryable": true } | Retry the identical payload |
Optional fields
sourceUrl: HTTP(S) source URL without embedded credentials.clickIds:fbclid,fbc,fbp,gclid,gbraid,wbraid, ordclid.userData: email, phone, names, external ID, client IP, or client user agent; requires granted ad-user-data consent.metadata: up to 50 string, finite-number, boolean, or null values.