Skip to content
View as Markdown

List conversions ​

GET /v1/campaigns/{campaign_id}/conversions

Returns one conversion row per lead of a campaign, oldest first.

Scope: leads:read

Rows are ordered by created_at, oldest first, so a lead created while you page shows up on a later page and rows already returned are never repeated. Use it to see which marketing leads became customers and to send those outcomes back to an ad platform.

A deal is closed when its card is in a stage whose outcome is set to Won in the pipeline's stage settings, and lost in a stage whose outcome is Lost; the stage's name does not matter. Set the outcome of every winning and losing stage in DialEcho; leads already in those stages then report the close from when they entered the stage.

The campaign must be one the key may reach, otherwise 404. Reading does not need the member's permission to manage leads.

Path parameters ​

NameTypeRequiredDescription
campaign_idUUIDYesA campaign the key may reach.

Query parameters ​

NameTypeRequiredDescription
created_aftertimestampNoOnly leads created at or after this time. ISO 8601 with a UTC offset, for example 2026-11-01T00:00:00Z (URL-encode + as %2B).
created_beforetimestampNoOnly leads created before this time. After created_after.
milestonestringNoOnly leads that reached it: called (first_call_at set), appointment_booked, appointment_held (appointment_status is held), closed (closed_at set) or lost (lost_at set). One of called, appointment_booked, appointment_held, closed, lost.
limitintegerNoRows per page. 1 to 100. Default: 50.
cursorstringNoThe next_cursor of the previous page, unchanged. Opaque: never build or edit one. Up to 4,096 characters.

Response ​

Status: 200 OK

A lead that has not got anywhere yet has every milestone null and, if it is not on the board, "card": null. Every key is always present.

NameTypeDescription
dataarray of objectsConversion rows, oldest first.
data.lead_idUUIDThe lead.
data.campaign_idUUIDThe lead's campaign.
data.created_attimestampWhen the lead was created in DialEcho (RFC 3339, UTC).
data.contactobjectThe lead's details as GET /v1/leads/{lead_id} returns them, with the same field visibility: a field the member's role hides is null, one it masks comes back masked.
data.contact.first_namestring | nullFirst name.
data.contact.last_namestring | nullLast name.
data.contact.emailstring | nullEmail address.
data.contact.phonestring | nullPhone number in E.164 (+14155550132); a stored number that is not a valid phone number is returned as stored.
data.contact.citystring | nullCity.
data.contact.statestring | nullState.
data.contact.postal_codestring | nullZIP or postal code.
data.contact.email_sha256string | nullSHA-256 (lowercase hex) of the email trimmed and lowercased: Meta's hashed em. null without an email, or unless the member sees the email in full.
data.contact.phone_sha256string | nullSHA-256 (lowercase hex) of the number's digits with country code and no + (14155550132): Meta's hashed ph. null without a valid number, or unless the member sees the phone in full.
data.attributionobject | nullThe attribution stored on the lead under the 16 attribution keys POST /v1/leads accepts (utm_source, utm_medium, utm_campaign, utm_term, utm_content, utm_id, adset_id, ad_id, fbclid, fbc, fbp, gclid, meta_event_id, page_url, form_name, referrer), whether sent through the API or set another way such as an import. Every key is present, null when never set. The whole object is null when the member's role hides custom fields; when it masks them, set values read Restricted.
data.milestonesobjectHow far the lead got. Timestamps are RFC 3339 in UTC; null until the lead reaches the milestone.
data.milestones.first_call_attimestamp | nullWhen the first outbound call to the lead by one of the campaign's AI agents started: a POST /v1/calls call, a campaign or a pipeline agent's call. Calls by your team and calls the lead made are not counted.
data.milestones.first_call_outcomestring | nullHow that call went: reached (a real conversation), answered (picked up but no conversation), voicemail, no_answer (not answered, busy or not connected) or in_progress. One of in_progress, reached, answered, voicemail, no_answer.
data.milestones.appointment_booked_attimestamp | nullWhen the lead's first appointment in the campaign was booked, however it was booked, even if it was later moved or cancelled.
data.milestones.appointment_attimestamp | nullWhen the appointment is or was scheduled: the first one that was held; if none was held, the latest one not cancelled; if all were cancelled, the latest one.
data.milestones.appointment_statusstring | nullThat appointment's status. Attendance your team records decides (shown up is held, no-show is no_show); otherwise held when your team's appointment call reached the lead and no_show when your team recorded nobody answered. An unanswered call with nothing recorded leaves it scheduled. One of scheduled, held, no_show, cancelled.
data.milestones.appointment_held_attimestamp | nullFor a held appointment, when the call that reached the lead started; for one only marked as shown up, its scheduled start.
data.milestones.closed_attimestamp | nullWhen the card entered the Won stage it is in now (a deal reopened and won again reports the latest win). null unless card.deal_status is won.
data.milestones.lost_attimestamp | nullThe same for a Lost stage. null unless card.deal_status is lost.
data.cardobject | nullThe card GET /v1/leads/{lead_id} returns as card, or null if the lead has none the member can open: its main card (the deal), or, for a member who may open only a follow-through card of the lead, that card with its own pipeline, stage, deal_status, closed_at and lost_at and the deal value it shares with the main card. Nothing of a card the member may not open is shown. Always shown, whatever the member's field visibility.
data.card.idUUIDThe card.
data.card.pipeline_idstring | nullThe card's pipeline.
data.card.pipeline_namestring | nullThe pipeline's name.
data.card.stagestring | nullSlug of the current stage.
data.card.stage_namestring | nullName of the current stage.
data.card.deal_statusstringabandoned when your team marked the deal abandoned, otherwise the outcome set on the card's stage in DialEcho (Won, Lost or Open), whatever the stage is called. A follow-through card the deal moves into after it is won is reported only to a member who may open that card and not the main card. One of open, won, lost, abandoned.
data.card.deal_valuenumber | nullThe deal value, shared by the main card and its follow-through cards, or null.
next_cursorstring | nullPass it back as cursor for the next page; null when there is none.
has_morebooleantrue while more pages may follow. Keep paging until it is false.

Example request ​

curl

bash
curl "https://development.agencydevworks.ai/v1/campaigns/cccccccc-0000-4000-8000-000000000001/conversions?created_after=2026-11-01T00:00:00Z&milestone=closed&limit=50" \
  -H "Authorization: Bearer dei_your_key"

JavaScript

js
const response = await fetch("https://development.agencydevworks.ai/v1/campaigns/cccccccc-0000-4000-8000-000000000001/conversions?created_after=2026-11-01T00:00:00Z&milestone=closed&limit=50", {
  headers: {
    "Authorization": "Bearer dei_your_key",
  },
});
const data = await response.json();

Example response ​

Status: 200 OK

json
{
  "data": [
    {
      "lead_id": "aaaaaaaa-0000-4000-8000-000000000001",
      "campaign_id": "cccccccc-0000-4000-8000-000000000001",
      "created_at": "2026-11-02T15:55:55Z",
      "contact": {
        "first_name": "Pat",
        "last_name": "Lee",
        "email": "pat@example.com",
        "phone": "+14155550132",
        "city": "San Francisco",
        "state": "CA",
        "postal_code": "94105",
        "email_sha256": "fe9733fcc96501276776fc927df08a395835461a2c5b12c65ebab3454877e227",
        "phone_sha256": "11072303eb5c8cb5c5a1ebb762f11b37b07131265b46fdfe1276196bfbd3ca51"
      },
      "attribution": {
        "form_name": "quote-step-1",
        "page_url": "https://www.example.com/get-a-quote",
        "referrer": null,
        "utm_source": "facebook",
        "utm_medium": "paid_social",
        "utm_campaign": "fall-promo",
        "utm_term": null,
        "utm_content": null,
        "utm_id": "120200000000000001",
        "adset_id": "120200000000000002",
        "ad_id": "120200000000000003",
        "fbclid": "IwAR0exampleClickId",
        "fbc": null,
        "fbp": "fb.1.1791900000000.1234567890",
        "gclid": null,
        "meta_event_id": null
      },
      "milestones": {
        "first_call_at": "2026-11-02T15:56:04Z",
        "first_call_outcome": "reached",
        "appointment_booked_at": "2026-11-02T15:59:41Z",
        "appointment_at": "2026-11-04T18:00:00Z",
        "appointment_status": "held",
        "appointment_held_at": "2026-11-04T18:02:13Z",
        "closed_at": "2026-11-09T16:30:00Z",
        "lost_at": null
      },
      "card": {
        "id": "dddddddd-0000-4000-8000-000000000001",
        "pipeline_id": "eeeeeeee-0000-4000-8000-000000000001",
        "pipeline_name": "Sales",
        "stage": "won",
        "stage_name": "Won",
        "deal_status": "won",
        "deal_value": 18500.0
      }
    }
  ],
  "next_cursor": null,
  "has_more": false
}

Errors ​

HTTPCodeWhen
403insufficient_scopeThe key does not have leads:read. Checked before the query is validated.
404not_foundThe campaign does not exist or the key cannot reach it.
409resync_requiredThe cursor expired (after 30 days) or the key's or its member's access changed since it was issued. Start again from the first page.
422invalid_cursorThe cursor was not issued to this key for this request, was changed, or was sent with different parameters than the page it came from.
422invalid_requestA parameter is invalid: campaign_id not a UUID, a timestamp without a UTC offset, created_before not after created_after, an unknown milestone, a limit outside 1 to 100 or a cursor over 4,096 characters. error.fields names it (query.milestone, campaign_id, ...).
503dependency_unavailableThe report or the access check is temporarily unavailable. Retry the same request with backoff.

Every endpoint can also answer 401 invalid_api_key, 429 rate_limited and 503 dependency_unavailable. See Errors in the overview.

Sending conversions to Meta ​

DialEcho does not send anything to Meta. Your server, or your marketing team's tool, reads this report and sends the events to Meta's Conversions API (POST https://graph.facebook.com/{api-version}/{pixel-id}/events), so Meta's optimization learns from leads that became appointments and sales. Keep the event_ids you have sent and send each event once.

Pull at least daily: Meta only accepts recent event_times (7 days for most events) and deals often close weeks after the lead. A daily job requesting ?created_after=<today minus your longest sales cycle>&milestone=closed (and milestone=appointment_booked) catches every close while it is still recent.

action_source says where the conversion happened: phone_call for an appointment or deal made on a call (the usual case here), website for a form or a booking your site made (Meta then requires the visitor's user agent and page, which the report does not have, so send those from your website's server when they happen), other for in person.

Meta event_nameSend it for rows whereevent_time (Unix seconds)event_idcustom_data
LeadOnly leads your website's server did not already send, for example a lead that phoned you.created_atattribution.meta_event_id when set (deduplicates against the Pixel), otherwise dialecho-lead-{lead_id}none
Schedulemilestones.appointment_booked_at is setappointment_booked_atdialecho-schedule-{lead_id}none
Purchasemilestones.closed_at is setclosed_atdialecho-purchase-{lead_id}{"value": card.deal_value, "currency": "USD"}; DialEcho stores no currency and shows deal values in US dollars

Meta user_data from a row ​

Send these user_data keys with every event. Omit a key whose source is null.

Meta keyFrom the row
em[contact.email_sha256] (already hashed)
ph[contact.phone_sha256] (already hashed)
fn, lnSHA-256 of contact.first_name / last_name, trimmed and lowercased
ctSHA-256 of contact.city, lowercased with spaces and punctuation removed
stSHA-256 of contact.state as a lowercase 2-letter code (ca)
zpSHA-256 of the first 5 digits of contact.postal_code (US)
countrySHA-256 of us for US leads
external_idSHA-256 of lead_id, the same on every event of a lead
fbcattribution.fbc when set; otherwise, when attribution.fbclid is set, fb.1.{created_at in Unix milliseconds}.{fbclid}. Not hashed.
fbpattribution.fbp when set. Not hashed.