OrcLink API v1.0.0
Read outcomes, journeys, calls, report rows and spend; write conversions, revenue and opportunity events; subscribe to outcome events.
- Authentication: Authorization: Bearer orc_live_... (an API key from Data out > API). Keys are never accepted in the URL: a request with a key parameter, or any parameter value that starts with orc_live_, is refused (400 key_in_url).
- Scopes: read (the GET endpoints and REST hook subscriptions), write (POST /api/v1/conversions, PUT /api/v1/revenue/{externalId} and PUT /api/v1/opportunities/{externalId}).
- Errors: {"error": {"code": "...", "message": "..."}} (POST /api/v1/conversions and the two PUT endpoints keep {"error": "..."}). Messages never echo submitted values.
- Rate limits per key: 120 requests per minute for reads, 30 per minute for reports, 60 per minute for hooks, 600 per minute for conversions, revenue and opportunities together. Per workspace, for all its keys together: 600 per minute for reads, 60 per minute for reports, 600 per minute for hooks (429 rate_limited). One performance report per key runs at a time (429 report_running while one runs).
- Paging: pass next_cursor as cursor until it is null.
- Privacy: no response carries an email, phone, IP address, caller number, page path, hash, olid, click id, session id or visitor key.
- Outcomes, touchpoints and calls are person-level: in HIPAA mode they need the workspace BAA, and stage names are generic labels.
- HIPAA mode: campaign, campaign group, ad group, ad content, keyword, source, medium, ad account, ad account id and custom channel names (free text) are omitted from every response and webhook body (a custom channel leaves as "Custom channel"; built-in channel names always leave) unless the workspace owner turns on "Include campaign names in data out", confirming: "I confirm our campaign, campaign group, ad group, ad content, keyword, source, medium, ad account, ad account id and custom channel names contain no patient information." A confirmation of an older text releases nothing until it is confirmed again.
- OrcLink numbers are claims: the source system owns the outcome and its value.
Your own record id (external_id) is included in outcomes and webhook events; do not put a name, email or phone in it. An id is shown as an id only when the ad platform made it: a CSV upload's campaign and ad group text follows the names rule.
Endpoints
/api/v1/conversionsscope: writeSend one conversion or up to 100
Server-to-server conversions (quiz complete, intake, checkout, a lead from your backend). Email and phone are hashed on receipt and never stored or echoed. An event opts in to a canonical outcome with stage, kind or status, or an event_name that is a dictionary stage. external_id must be an opaque id (not an email address or a formatted phone number).
Body: ConversionEvent | { events: ConversionEvent[] }
200OK{ results: ConversionResult[] }400Invalid body (paths and messages only).{ error: string, issues: { path: string, message: string }[] }401Invalid or missing API key.{ error: string }429Rate limited.{ error: string }
/api/v1/revenue/{externalId}scope: writeRecord revenue from your system of record
Records one revenue event for the record `externalId` (an invoice, a sale, a closed deal: its id in your system). Your system owns the number: a later PUT for the same id with another value replaces the gross value and is kept as a revision (value history). Status never moves backward (a lost or cancelled record is not reopened by a late event). The record is matched to a click by `olid`, then by the hashed email or phone, then by `lead_id` (the id your page sent with `orc.identify`; lower confidence, never sent to ad platforms). The path id is never used to match. Idempotent: the same `event_id` with the same body is recorded once (200, status `duplicate`); the same `event_id` with another body is refused (409). Email and phone are hashed on receipt and never stored, logged or returned. Validation errors name the field, never the value. One id space for revenue and opportunities: a revenue write closes the opportunity with the same id when it carries the same person or no email / phone. A record made by the other type with another email or phone is refused (409, code identity_conflict) unless allow_identity_change is true. A write of the same type with a new email or phone replaces it.
| Parameter | In | Description |
|---|---|---|
| externalId * | path | The record's id in your system, 1 to 128 characters. Must be an opaque id: an email address or a formatted phone number (+ or punctuation, 7 to 15 digits) is refused. Digits only is accepted (an invoice number). Must not start with orc_test_. |
Body: RevenueEvent
200Updated an existing outcome, or a duplicate of an event already recorded (status duplicate).DataInWriteResult201Created a new outcome.DataInWriteResult400Invalid body: not JSON, an unknown field, or a field that fails its rule. Each issue names a field; values are never echoed.DataInError401Missing or invalid API key.DataInError403The API key has no write scope.DataInError409The event_id was already used with a different body (no code). Or this id already belongs to a record of another type and another person (code identity_conflict): use a different id, or send allow_identity_change: true. Or the same event_id is being written by another request right now: then the answer carries Retry-After (30 seconds), retry the same request.DataInError413Body larger than 64 KB.DataInError429Rate limited: 600 requests per minute per API key, shared with the Conversions API. See Retry-After.DataInError500The event could not be recorded; retry with the same event_id.DataInError
/api/v1/opportunities/{externalId}scope: writeRecord an opportunity stage from your system of record
Records one stage event for the opportunity `externalId` (its id in your CRM or booking system). `stage` is required and is looked up in your outcome dictionary (name or alias; case, spaces, hyphens and underscores do not matter). A stage the dictionary does not know is recorded at the entry stage, flagged `unmapped_stage`, and never sent to ad platforms. Stages and statuses move forward only: a late event for an earlier stage does not move the opportunity back, and a lost opportunity is reopened only by a win. Matching, idempotency and privacy are the same as for PUT /api/v1/revenue/{externalId}.
| Parameter | In | Description |
|---|---|---|
| externalId * | path | The opportunity's id in your system, 1 to 128 characters, opaque (not an email address or a formatted phone number), and not starting with orc_test_. |
Body: OpportunityEvent
200Updated an existing outcome, or a duplicate of an event already recorded (status duplicate).DataInWriteResult201Created a new outcome.DataInWriteResult400Invalid body: not JSON, an unknown field, or a field that fails its rule. Each issue names a field; values are never echoed.DataInError401Missing or invalid API key.DataInError403The API key has no write scope.DataInError409The event_id was already used with a different body (no code). Or this id already belongs to a record of another type and another person (code identity_conflict): use a different id, or send allow_identity_change: true. Or the same event_id is being written by another request right now: then the answer carries Retry-After (30 seconds), retry the same request.DataInError413Body larger than 64 KB.DataInError429Rate limited: 600 requests per minute per API key, shared with the Conversions API. See Retry-After.DataInError500The event could not be recorded; retry with the same event_id.DataInError
/api/v1/outcomesscope: readList outcomes
Canonical outcomes (not test, not duplicate), newest first by occurred_at. Outcomes from AI-agent clicks are included and labeled traffic_class ai_agent.
| Parameter | In | Description |
|---|---|---|
| from | query | First day (YYYY-MM-DD, UTC) or an ISO 8601 time. Default: 30 days before `to`. |
| to | query | Last day, inclusive (YYYY-MM-DD, UTC) or an ISO 8601 time. Default: now. At most 400 days. |
| kind | query | Outcome kind. One of: order, lead, call, form, appointment, opportunity, deal. |
| status | query | Status (test and duplicate outcomes are never listed). One of: open, won, lost, cancelled, unqualified. |
| stage | query | Stage name, or its generic label in HIPAA mode. |
| source | query | Source system. |
| match | query | How the outcome was matched to a click. One of: olid, identity, external_id, none. |
| limit | query | Rows per page, 1 to 200 (default 50). |
| cursor | query | The next_cursor of the previous page (opaque). |
200OK{ data: Outcome[], next_cursor: string | null }400Invalid parameter, invalid cursor, or an API key sent in the URL (key_in_url).Error401Missing, invalid or revoked API key.Error403The key lacks the scope (insufficient_scope), or HIPAA mode is on without the workspace BAA (baa_required, person-level endpoints).Error429Rate limited per key or per workspace (rate_limited), or a performance report of the key is still running (report_running); see Retry-After.Error500Internal error.Error
/api/v1/outcomes/{id}scope: readGet one outcome
| Parameter | In | Description |
|---|---|---|
| id * | path |
200OK{ data: Outcome }400Invalid parameter, invalid cursor, or an API key sent in the URL (key_in_url).Error401Missing, invalid or revoked API key.Error403The key lacks the scope (insufficient_scope), or HIPAA mode is on without the workspace BAA (baa_required, person-level endpoints).Error404Not found (also for another workspace).Error429Rate limited per key or per workspace (rate_limited), or a performance report of the key is still running (report_running); see Retry-After.Error500Internal error.Error
/api/v1/touchpointsscope: readList an outcome's touchpoints
The stored journey of one outcome, in order (the journey attribution credit is computed from). Never a landing path, click id, olid or session id.
| Parameter | In | Description |
|---|---|---|
| outcome_id * | query | The outcome id. |
| limit | query | Rows per page, 1 to 200 (default 50). |
| cursor | query | The next_cursor of the previous page (opaque). |
200OK{ data: Touchpoint[], next_cursor: string | null }400Invalid parameter, invalid cursor, or an API key sent in the URL (key_in_url).Error401Missing, invalid or revoked API key.Error403The key lacks the scope (insufficient_scope), or HIPAA mode is on without the workspace BAA (baa_required, person-level endpoints).Error429Rate limited per key or per workspace (rate_limited), or a performance report of the key is still running (report_running); see Retry-After.Error500Internal error.Error
/api/v1/callsscope: readList calls
Call records, newest first by started_at. Never the caller number, its hash or digits, nor the dialed number.
| Parameter | In | Description |
|---|---|---|
| from | query | First day (YYYY-MM-DD, UTC) or an ISO 8601 time. Default: 30 days before `to`. |
| to | query | Last day, inclusive (YYYY-MM-DD, UTC) or an ISO 8601 time. Default: now. At most 400 days. |
| limit | query | Rows per page, 1 to 200 (default 50). |
| cursor | query | The next_cursor of the previous page (opaque). |
200OK{ data: Call[], next_cursor: string | null }400Invalid parameter, invalid cursor, or an API key sent in the URL (key_in_url).Error401Missing, invalid or revoked API key.Error403The key lacks the scope (insufficient_scope), or HIPAA mode is on without the workspace BAA (baa_required, person-level endpoints).Error429Rate limited per key or per workspace (rate_limited), or a performance report of the key is still running (report_running); see Retry-After.Error500Internal error.Error
/api/v1/reports/performancescope: readPerformance report rows
Rows of the attribution performance report by channel, source, campaign, campaign group, ad group or keyword under a rule-based model. Credited numbers are a claim; placeholder values and AI-agent outcomes are apart. In HIPAA mode only rows by channel, unless the owner turned on "Include campaign names in data out" (else 403 campaign_names_off).
| Parameter | In | Description |
|---|---|---|
| from | query | First day, YYYY-MM-DD (UTC). Default: 30 days ago. |
| to | query | Last day, YYYY-MM-DD (UTC). Default: today. |
| dimension | query | Row dimension. One of: channel, source, campaign, campaignGroup, adGroup, keyword. |
| model | query | Attribution model (rule-based only: the modelled models are dashboard-only). One of: first_touch, last_touch, last_non_direct, linear, time_decay, position_based, u_shaped, w_shaped, full_path, custom. |
| stage | query | Count arrivals at this stage as conversions (name, or generic label in HIPAA mode). |
| compare | query | Add the previous period of equal length. One of: 1, true. |
| halfLife | query | time_decay half-life in days. |
| pw | query | position_based weights first,middle,last. |
| cw | query | custom weights by position, comma separated. |
200OK{ data: PerformanceReport }400Invalid parameter, invalid cursor, or an API key sent in the URL (key_in_url).Error401Missing, invalid or revoked API key.Error403The key lacks the scope (insufficient_scope), or HIPAA mode is on without the workspace BAA (baa_required, person-level endpoints).Error429Rate limited per key or per workspace (rate_limited), or a performance report of the key is still running (report_running); see Retry-After.Error500Internal error.Error
/api/v1/spendscope: readList daily ad spend
Daily ad cost per platform, account, campaign, ad group and keyword, newest day first. Amounts are in each row currency, never converted.
| Parameter | In | Description |
|---|---|---|
| from | query | First day (YYYY-MM-DD, UTC) or an ISO 8601 time. Default: 30 days before `to`. |
| to | query | Last day, inclusive (YYYY-MM-DD, UTC) or an ISO 8601 time. Default: now. At most 400 days. |
| platform | query | Ad platform (google, meta, microsoft, tiktok, linkedin, reddit, stackadapt, csv). |
| limit | query | Rows per page, 1 to 200 (default 50). |
| cursor | query | The next_cursor of the previous page (opaque). |
200OK{ data: SpendRow[], next_cursor: string | null }400Invalid parameter, invalid cursor, or an API key sent in the URL (key_in_url).Error401Missing, invalid or revoked API key.Error403The key lacks the scope (insufficient_scope), or HIPAA mode is on without the workspace BAA (baa_required, person-level endpoints).Error429Rate limited per key or per workspace (rate_limited), or a performance report of the key is still running (report_running); see Retry-After.Error500Internal error.Error
/api/v1/hooksscope: readSubscribe a REST hook
Subscribe target_url to one event (the Zapier app triggers use this). The target receives the same signed bodies as a webhook endpoint: verify X-OrcLink-Signature with the secret this answer returns (shown once; only its encrypted form is stored). Refused in HIPAA mode (403 hipaa_mode). target_url must be https and its host must resolve to a public address (else 400 invalid_target_url, "The host is not allowed"). A revoked key stops its hooks. The same key subscribing the same event again within 30 days of an unsubscribe gets the same hook back (same id, the new target_url, a new secret), also when the workspace is over its plan quota; another event or another key is a new hook and then gets 402 over_quota.
Body: { target_url: string, event: string }
201Subscribed.{ id: string, event: string, secret: string }400Invalid parameter, invalid cursor, or an API key sent in the URL (key_in_url).Error401Missing, invalid or revoked API key.Error402The workspace is over its plan quota and this would be a new hook (over_quota). Re-subscribing an event this key unsubscribed within 30 days is not refused.Error403The key lacks the scope (insufficient_scope), or HIPAA mode is on without the workspace BAA (baa_required, person-level endpoints).Error409Too many hooks for the workspace (too_many_hooks).Error429Rate limited per key or per workspace (rate_limited), or a performance report of the key is still running (report_running); see Retry-After.Error500Internal error.Error
/api/v1/hooks/{id}scope: readUnsubscribe a REST hook
Only the API key that subscribed the hook can unsubscribe it (another key, or a hook already unsubscribed, gets 404). The hook stops at once; it is kept 30 days so the same key can subscribe the same event again (the same hook comes back with the new target_url), then deleted with its delivery log. At most 50 unsubscribed hooks per key and event are kept (the oldest go first); a hook a workspace manager turned off is never brought back by the key. A workspace manager can delete any hook in Data out.
| Parameter | In | Description |
|---|---|---|
| id * | path |
200Unsubscribed.{ id: string, deleted: boolean }400Invalid parameter, invalid cursor, or an API key sent in the URL (key_in_url).Error401Missing, invalid or revoked API key.Error403The key lacks the scope (insufficient_scope), or HIPAA mode is on without the workspace BAA (baa_required, person-level endpoints).Error404Not found (also another workspace, or a dashboard webhook endpoint).Error429Rate limited per key or per workspace (rate_limited), or a performance report of the key is still running (report_running); see Retry-After.Error500Internal error.Error
/api/v1/hooks/samplescope: readSample event
One fixed example body for an event type (no workspace data): the shape a hook receives.
| Parameter | In | Description |
|---|---|---|
| event * | query | Event type. One of: form.created, chat.created, call.completed, opportunity.updated, revenue.recorded, outcome.stage_changed. |
200OK{ data: WebhookEvent[] }400Invalid parameter, invalid cursor, or an API key sent in the URL (key_in_url).Error401Missing, invalid or revoked API key.Error403The key lacks the scope (insufficient_scope), or HIPAA mode is on without the workspace BAA (baa_required, person-level endpoints).Error429Rate limited per key or per workspace (rate_limited), or a performance report of the key is still running (report_running); see Retry-After.Error500Internal error.Error
Webhooks
Add an endpoint in Data out. Each delivery is signed: X-OrcLink-Signature: v1=<hex HMAC-SHA256(secret, "<X-OrcLink-Timestamp>.<raw body>")>. Compute it over the raw body, compare in constant time, and refuse a timestamp more than 5 minutes away from your clock. For 24 hours after a secret rotation the header carries two signatures, v1=<new>,v1=<old>: accept the delivery when any entry matches. A second rotation within the 24 hours drops the first old secret. Dedupe by X-OrcLink-Event-Id (a retry or a replay sends the same id), and order events by occurred_at, not by arrival: deliveries can arrive out of order. Events: form.created, chat.created, call.completed, opportunity.updated, revenue.recorded, outcome.stage_changed.
// Node.js receiver
import crypto from 'node:crypto';
function verify(secret, rawBody, signature, timestamp) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = Buffer.from('v1=' + crypto.createHmac('sha256', secret).update(timestamp + '.' + rawBody).digest('hex'));
// Two entries for 24 hours after a secret rotation: any match.
return (signature ?? '').split(',').some((s) => {
const given = Buffer.from(s.trim());
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}(your endpoint)form.created (sent by OrcLink to your endpoint)
OrcLink POSTs this JSON to each enabled endpoint that selected the event. Verify X-OrcLink-Signature: v1=<hex HMAC-SHA256(secret, "<X-OrcLink-Timestamp>.<raw body>")> in constant time. For 24 hours after a secret rotation the header carries two signatures, v1=<new>,v1=<old>: accept the delivery when any v1= entry matches your secret, so a rotation never breaks a live receiver. A second rotation within the 24 hours drops the first old secret (at most two signatures); and refuse a timestamp more than 300 seconds (5 minutes) from your clock. Dedupe by X-OrcLink-Event-Id: a retry or a replay sends the same id. Deliveries can arrive out of order (retries, replays): order events by occurred_at, not by arrival. Answer 2xx within 5 s; other answers are retried with backoff; redirects are not followed; 410 Gone disables the endpoint. In HIPAA mode stage names are generic labels.
| Parameter | In | Description |
|---|---|---|
| X-OrcLink-Event-Id * | header | |
| X-OrcLink-Event-Type * | header | One of: form.created. |
| X-OrcLink-Timestamp * | header | |
| X-OrcLink-Signature * | header | |
| X-OrcLink-Delivery-Attempt * | header |
Body: WebhookEvent
200Received. Any 2xx counts.
Schemas
Error
error{ code: string, message: string }
Match
methodstringlabelstringconfidencestring — external_id matches are lower confidence and never sent to ad platforms.
Outcome
idstring — The outcome's own opaque id.kindstringsourcestring — The system that owns the outcome (shopify, salesforce, hubspot, ghl, ctm, callrail, api, form, chat, ...).external_idstring | null — The source's own id of the record (order id, CRM record id, call id). Your own record id is included, also in HIPAA mode; do not put a name, email or phone in it. null when it is email-shaped or phone-formatted (never sent).stagestring — Current stage (dictionary name). In HIPAA mode a generic label (event_1, event_2, ...): decode it on the Compliance tab.statusstringvaluenumber | null — Net value (gross less refunds) when known, else gross. The source system owns it.gross_valuenumber | nullcurrencystring | null — ISO 4217 code.value_basisstringplaceholder_valueboolean — true when the value is the stage default (a placeholder), not a source amount.matchMatchtraffic_classstring — ai_agent: the matched click was made by an AI agent; report it apart.occurred_atstringstage_reached_atstring | nullclosed_atstring | nullupdated_atstring
Touchpoint
idstringoutcome_idstringpositioninteger — 0 = first touch.kindstring — static_number: a call to a static (offline) tracking number, one channel-level touch (no keyword, no visit).labelstring | null — "static number (channel-level)" on a static_number touch, else null.atstringchannelstring | null — Built-in channel name, or a workspace channel rule label; in HIPAA mode without "Include campaign names in data out" a workspace label is "Custom channel".sourcestring | null — Omitted in HIPAA mode unless the owner turned on "Include campaign names in data out".mediumstring | null — Omitted in HIPAA mode unless campaign names are turned on.campaignstring | null — Omitted in HIPAA mode unless campaign names are turned on.is_directboolean
Call
idstringoutcome_idstring | nullproviderstringdirectionstringstarted_atstringanswered_atstring | nullended_atstring | nullduration_secintegertalk_secintegerresultstringlabelstring | null — The provider label; null in HIPAA mode (free text).has_recordingboolean
SpendRow
idstringdatestringplatformstringaccount_idstring | null — The ad account id. A manager may type it (StackAdapt, Reddit, X), so in HIPAA mode without "Include campaign names in data out" it is null, except "csv" for an uploaded row.campaign_idstring | null — The ad platform id. A CSV-uploaded row (account_id "csv") stores the campaign TEXT here, so in HIPAA mode without "Include campaign names in data out" it is null, like the names.campaign_namestring | null — Omitted in HIPAA mode unless the owner turned on "Include campaign names in data out".ad_group_idstring | null — The ad platform id; for a CSV row the uploaded text, null in HIPAA mode without the opt-in.ad_group_namestring | null — Omitted in HIPAA mode unless campaign names are turned on.keywordstring | null — Omitted in HIPAA mode unless campaign names are turned on.spendnumbercurrencystringimpressionsintegerclicksintegerplatform_conversionsnumber | null — The ad platform's own count (its claim).
PerformanceReport
See the dashboard Attribution > Performance report for definitions (CPL, CPA, ROAS, ROI per currency).
days{ from: string, to: string }compareobject | nulldimensionstringmodelstringmodel_labelstringstagestring | nullstage_columnsstring[]cost_availablebooleanlabelsobjectrows{ key: string, label: string, kind: string, current: Metrics, previous: Metrics | null }[]more_rowsintegertotalsMetricsai_agentobject — Outcomes from AI-agent clicks: one line apart from rows and totals.undatedobject | nullnotesobjectcost_gaps{ platform: string, account_id: string, label: string | null, days: integer, ranges: { from: string, to: string }[] }[] — Days a connected ad account has no stored cost. account_id (a manager may type it) and label (the account name) are omitted in HIPAA mode without "Include campaign names in data out".
Money
currencystring — ISO 4217. XXX: currency not reported (the source record had none): never converted, never in ROAS or ROI.minorintegeramountnumber
Metrics
visits{ human: integer, aiAgent: integer, crawler: integer }stagesobjectconversionsnumberwonConversionsnumberrevenueMoney[] — Values of won outcomes only.placeholderRevenueMoney[]pipelineMoney[] — Pipeline (open): reported values of outcomes not won; never in revenue, ROAS or ROI.costarray | nullcplarray | nullcpaarray | nullroasarray | nullroiarray | null
ConversionEvent
event_namestringevent_idstring | null — Your dedupe id (also the browser pixel dedupe key). It is sent to ad platforms as is, so it must be an opaque id: an email address, a URL, an IP address or an id that looks like a phone number (7 to 15 digits with dashes, dots, spaces, parentheses or a +, for example 415-555-0100 or 2026-10-05-000123) is refused with 400. Use letters, or digits only (an order number 5551234567 is fine). One refused event_id refuses the whole request. An order with the same external id as an order from your connected store or payment source is not sent to ad platforms (the store's own order is): the ids are compared exactly, and only when the store's order arrived first.occurred_atstring | number | null — ISO 8601, unix seconds or unix milliseconds; at most 5 minutes in the future (clock skew); a later time is refused.valuenumber | nullcurrencystring | nullolidstring | nullsession_idstring | nullvisitor_idstring | nullemailstring | null — Hashed on receipt; never stored or echoed.phonestring | null — Hashed on receipt; never stored or echoed.customer_idstring | nullexternal_idstring | null — Opaque id; an email address or a formatted phone number is refused.stagestring | nullkindstring | nullstatusstring | null
ConversionResult
event_idstring | nullstatusstringmatch_methodstring | nullduplicatebooleanerrorstringoutcome_idstring | nullstagestring | nulloutcome_statusstring | nulloutcome_match_methodstring | nullunmapped_stagebooleanoutcome_errorstring
RevenueEvent
At least one of email, phone, olid or lead_id is required. An unknown field is refused. A field sent as null counts as not sent.
event_idstring — Unique id of this event (the idempotency key). Opaque (not an email or phone, not starting with orc_test_). Never sent to ad platforms: the relay uses its own keyed event id. To dedupe with a browser pixel use POST /api/v1/conversions.datestring — ISO 8601: 2026-10-01 or 2026-10-01T14:30:00Z (a date-time needs a time zone). Not more than one day in the future.valuenumber | string — The revenue amount, 0 or more. A numeric string is accepted.currencystring — ISO 4217 code.labelstring — Free text (first 100 characters kept). Dropped when it holds an email or phone, and never stored in HIPAA mode.stagestring — A stage of your outcome dictionary (name or alias). Default: the lowest win stage.statusstring — Default: won.emailstring — Hashed on receipt; never stored, logged or returned.phonestring — Hashed on receipt (7 to 15 digits); never stored, logged or returned.olidstring — The OrcLink click id.lead_idstring — The lead or order id your page sent with orc.identify. Lower-confidence match; never sent to ad platforms.backfillboolean — true: history. The event is written and attributed in reports but never sent to ad platforms. Not sent: an event whose date is more than 24 hours before it arrives is treated as history, because old records are never sent to ad platforms as new conversions. false: the event is live whatever its date (ad platforms still refuse events older than 7 days, so those are never sent).allow_identity_changeboolean — true: this write may move the id to another person (another type and another person: its email or phone differs from the stored record). Without it such a write is refused (409, code identity_conflict).
OpportunityEvent
At least one of email, phone, olid or lead_id is required. An unknown field is refused. A field sent as null counts as not sent.
event_idstring — Unique id of this event (the idempotency key). Opaque (not an email or phone, not starting with orc_test_). Never sent to ad platforms: the relay uses its own keyed event id. To dedupe with a browser pixel use POST /api/v1/conversions.datestring — When the opportunity reached this stage. ISO 8601: 2026-10-01 or 2026-10-01T14:30:00Z (a date-time needs a time zone). Not more than one day in the future.stagestring — A stage of your outcome dictionary (name or alias).valuenumber | string | null — Optional, 0 or more. A numeric string is accepted. Without it the stage default from the dictionary is used (labeled as a placeholder).currencystring — ISO 4217 code.labelstring — Free text (first 100 characters kept). Dropped when it holds an email or phone, and never stored in HIPAA mode.statusstring — Only a status your system states outright; otherwise the dictionary decides.emailstring — Hashed on receipt; never stored, logged or returned.phonestring — Hashed on receipt (7 to 15 digits); never stored, logged or returned.olidstring — The OrcLink click id.lead_idstring — The lead or order id your page sent with orc.identify. Lower-confidence match; never sent to ad platforms.backfillboolean — true: history. The event is written and attributed in reports but never sent to ad platforms. Not sent: an event whose date is more than 24 hours before it arrives is treated as history, because old records are never sent to ad platforms as new conversions. false: the event is live whatever its date (ad platforms still refuse events older than 7 days, so those are never sent).allow_identity_changeboolean — true: this write may move the id to another person (another type and another person: its email or phone differs from the stored record). Without it such a write is refused (409, code identity_conflict).
DataInWriteResult
outcome_idstringexternal_idstringstatusstringstagestringoutcome_statusstringmatch_methodstringunmapped_stageboolean — The stage is not in the dictionary; the outcome was recorded at the entry stage and is not sent to ad platforms.revisionboolean — This write changed the value of an existing outcome (a revision was recorded).backfillboolean — What was stored for this event: written as history (never sent to ad platforms). A duplicate answers with the flag of the first write, not of this request.relaynull | { relayed: boolean, event_id: string, status: string, reason: string }
DataInError
The error body of the revenue and opportunities endpoints: a plain message, like POST /api/v1/conversions.
errorstringcodestring — Set when the 409 is an identity conflict.issues{ path: string, message: string }[]
WebhookEvent
idstring — The event id: the same on a retry or a replay. Dedupe by it.typestringapi_versionstringcreated_atstringoccurred_atstring — When the stage or status was reached. Order events by it: deliveries can arrive out of order.data{ outcome: Outcome + { previous_stage: string | null, previous_status: string | null, event_stage: string, event_status: string, attribution: { model: string, channel: string | null, source: string | null, medium: string | null, campaign: string | null } } }privacy{ hipaa_mode: boolean, generic_stage_names: boolean, campaign_names: boolean }