Skip to content

API reference ​

Base: https://app.durum.ai/api/v1/ Authentication: Authorization: Bearer durum_sk_live_… header (see Authentication).

Conventions ​

  • Reads: GET method, parameters in the query string. from and to are YYYY-MM-DD dates, optional unless stated otherwise. Without a period, the route applies its own default window.
  • Writes: PUT or POST depending on the route, JSON body with Content-Type: application/json. Every write requires a write:* scope.
  • Response: 200 with { "data": … }. Every error returns { "error": "…" } with the matching HTTP code. Responses carry Cache-Control: private, no-store.
  • Your data only: the account is derived from the key. No parameter lets you point at another one.
  • Stripped fields: see Fields never returned.

Self-documenting index

GET https://app.durum.ai/api/v1 (with or without a trailing slash) returns, without authentication, the list of read and write routes with their method, scope and full path (/api/v1/dashboard/kpis), plus base_url and a link to this page.

GET https://app.durum.ai/api/v1/tools (key required) returns a JSON manifest (tools[] with name, method, path, scope, inputSchema) limited to the routes covered by your key's scopes. It does not list every route (the calls and creatives routes are missing): the index above remains the reference list.

Dashboard ​

RouteScopeParameters
GET /dashboard/kpisread:dashboardfrom, to
GET /dashboard/dailyread:dashboardfrom, to
GET /dashboard/breakdownsread:dashboardfrom, to, dim (default source; values source, campaign, product, rep)

Responses:

  • kpis: an object of KPIs aggregated over the period (including leads, applications, bookings, meetings, shows, no_shows, total_sales, revenue, ticket_moyen).
  • daily: an array, one row per day: report_date, leads, applications, bookings, meetings, no_shows, sales, revenue, spend.
  • breakdowns: an array, one row per dimension value: dimension_key, leads, applications, bookings, shows, no_shows, sales, revenue.
bash
curl "https://app.durum.ai/api/v1/dashboard/breakdowns?from=2026-08-01&to=2026-08-31&dim=campaign" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY"

Ads ​

RouteScopeParameters
GET /ads/spendread:adsfrom, to
GET /ads/campaignsread:adsfrom, to
GET /ads/rulesread:adsnone
GET /ads/rules/runsread:adsrule_id, limit (integer, default 30, max 100)

Responses:

  • spend: an array, one row per ad and platform: ad_id, ad_name, campaign_name, campaign_id, adset_name, adset_id, platform, total_spend, total_clicks, total_impressions, total_reach.
  • campaigns: an array, one row per campaign and platform, sorted by descending spend: campaign_id, campaign_name, platform, total_spend, total_clicks, total_impressions, total_reach, nb_ads, nb_adsets.
  • rules: your automated ad rules (label, type, campaign, enabled state, dry-run mode, parameters, last run).
  • rules/runs: the latest runs of one rule.
bash
curl "https://app.durum.ai/api/v1/ads/rules/runs?rule_id=RULE_ID&limit=10" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY"

Attribution ​

RouteScopeParameters
GET /attributionread:attributionfrom, to
GET /attribution/healthread:attributionnone
GET /attribution/reportread:attributionfrom, to

Responses:

  • attribution: an array, one row per ad. Columns are prefixed out_: identity (out_ad_id, out_ad_name, out_campaign_id, out_campaign_name, out_adset_id, out_adset_name, out_platform), media (out_total_spend, out_total_clicks, out_total_impressions, out_total_reach), results (out_leads, out_applications, out_bookings, out_meetings, out_meetings_present, out_meetings_no_show, out_sales, out_revenue, out_show_rate, out_close_rate, out_rep_names), split by model (out_sales_ft and out_revenue_ft first touch, out_sales_lt and out_revenue_lt last touch, out_sales_assisted and out_revenue_assisted), adjustments (out_refund_amount, out_dispute_amount, out_net_revenue) and out_is_excluded.
  • health: a capture-health object: verdict, events_pub_30j, pct_exact_id, pct_par_nom, pct_non_resolu, pixel_sessions_30j, pixel_derniere_activite, pixel_pct_avec_id, issues.
  • report: an array per source, campaign and ad: out_source, out_campaign, out_ad_id, out_ad_name, out_spend, out_gross_revenue, out_refund_amount, out_dispute_amount, out_net_revenue, out_roi_gross, out_roi_net, out_touchpoints.
bash
curl "https://app.durum.ai/api/v1/attribution?from=2026-08-01&to=2026-08-31" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY"

Events ​

RouteScopeParameters
GET /eventsread:eventstype, from, to, limit (integer, default 50, max 200), cursor, rep
GET /events/summaryread:eventsfrom, to

type accepts lead, application, booking, no_show, sale, cancel, payment, sale_lost. Without type, all events are returned. sale groups sales and payments; application includes qualified applications; booking includes marketing and follow-up bookings. rep filters on the assigned rep's name.

events returns an array from newest to oldest, one row per event: id, event_type, event_datetime, contact_email, contact_name, contact_phone, contact_id, value, amount, product_key, product_name, assigned_user_name, is_paid, payment_status, booking_date, booking_time, utm_source, utm_campaign, utm_content, campaign_name, calendar_name, meta_ad_id, is_baseline, cancelled_at, has_more, counted, drop_reason, group_key, contact_seq.

Cursor pagination: has_more is true on the rows when a next page exists. To fetch it, call the route again with cursor set to the event_datetime of the last row received (ISO 8601 timestamp): the next page only contains events strictly older than that.

events/summary returns an array per ad and campaign: ad_name, campaign_name, leads, applications, bookings, sales, no_shows, cancellations, total_value.

bash
curl "https://app.durum.ai/api/v1/events?type=sale&from=2026-08-01&to=2026-08-31&limit=100" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY"

# next page
curl "https://app.durum.ai/api/v1/events?type=sale&from=2026-08-01&to=2026-08-31&limit=100&cursor=2026-08-14T15:02:11Z" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY"

Sales and finance ​

RouteScopeParameters
GET /sales/summaryread:salesfrom, to
GET /sales/repsread:salesfrom, to
GET /sales/cycleread:salesfrom, to
GET /finance/pnlread:financefrom, to

Responses:

  • sales/summary: an object (sales KPIs, pipeline, no-show).
  • sales/reps: an array, one row per rep: rep_name, bookings, meetings, shows, no_shows, sales, revenue, close_rate, no_show_rate, ticket_moyen, contacts_met, contacts_closed.
  • sales/cycle: an object: avg_days, median_days, p25_days, p75_days, sample_size, conversions, weekly_trend.
  • finance/pnl: a P&L object. On top of the global list of fields never returned, this route strips every field whose name contains margin, profit, wholesale, markup, cost_of or cogs.
bash
curl "https://app.durum.ai/api/v1/sales/reps?from=2026-08-01&to=2026-08-31" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY"

Funnel ​

RouteScopeParameters
GET /funnelread:funnelfrom, to

Returns an object: funnel stages, conversion by source, campaign and product, time to conversion.

bash
curl "https://app.durum.ai/api/v1/funnel?from=2026-08-01&to=2026-08-31" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY"

Contacts ​

RouteScopeParameters
GET /contactsread:contactsfrom, to, limit (integer, default 100, max 500), offset (integer, default 0), search
GET /contacts/customersread:contactsnone
GET /contacts/purchasesread:contactsfrom, to
GET /contacts/callsread:callsemail (required)

Responses:

  • contacts: an array, one row per contact: email, total_events, event_types, first_event_date, last_event_date, name, phone, sources, assigned_user_name, value, form_titles, has_campaign, tags, form_response. Paginated with limit and offset.
  • contacts/customers: an array, one row per customer: id, contact_email, contact_name, contact_phone, contact_id, first_event_type, first_event_date, latest_lead_date, first_payment_date, commission_status, commission_expires_at, total_revenue, total_purchases, total_commission, products, created_at.
  • contacts/purchases: an array, one row per purchase: id, purchase_date, product_key, product_name, contact_email, contact_name, amount, commissionable, commission_rate, commission_amount, invoice_id.
  • contacts/calls: the calls of the last 365 days this email took part in, with their analysis. Same object as calls below, plus contact_email. Without email, the response is an empty array.
bash
curl "https://app.durum.ai/api/v1/contacts?search=example.com&limit=200&offset=0" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY"

Calls ​

RouteScopeParameters
GET /callsread:callsfrom, to, limit (integer, default 50, max 200), offset (integer, default 0)
GET /calls/transcriptread:callsid (integer, required)
GET /calls/recordingread:callsid (integer, required)

calls is the entry point: an array of analyzed calls from newest to oldest, one row per call: artifact_id, starts_at, title, duration_sec, call_analysis_id, summary, call_type, is_qualified, icp_fit_score, qualification_reasoning, next_step_type, next_step_summary, next_step_proposed, scorecard_total, rep_id, markers (array of { ts, label, status }). The artifact_id is the id for the two routes below.

calls/transcript returns the raw transcript: an ordered array of segments, each with idx, start_ms, end_ms, speaker_id, speaker_label, speaker_email, text, confidence. If the call does not belong to your account, the response is 403 forbidden. Without id, the response is an empty array.

calls/recording does not return the file: it returns signed URLs valid for one hour, to download directly afterwards:

json
{
  "data": {
    "artifact_id": 1234,
    "content_type": "audio/mpeg",
    "expires_in": 3600,
    "audio_url": "https://…",
    "video_url": null,
    "has_video": false
  }
}

Errors specific to calls/recording: 400 invalid_id (id missing or not positive), 403 forbidden (call from another account or nonexistent), 404 no_recording_available (no audio or video file).

bash
curl "https://app.durum.ai/api/v1/calls?from=2026-08-01&to=2026-08-31&limit=50" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY"

curl "https://app.durum.ai/api/v1/calls/transcript?id=1234" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY"

curl "https://app.durum.ai/api/v1/calls/recording?id=1234" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY"

Journeys and touchpoints ​

RouteScopeParameters
GET /journeysread:journeysfrom, to, limit (integer, default 50, max 200), offset (integer, default 0), search (email fragment)
GET /journeys/contactread:journeysemail (required), pageviews (true or 1 to include page views)
GET /touchpointsread:journeysfrom, to, limit (integer, default 100, max 500), offset (integer, default 0), kind (web or conversion)

These routes expose the UTM captured at each touchpoint, not only the first or the last one.

  • journeys: an object { coverage, paging, journeys }. coverage says since when web touchpoints exist (web_touchpoints_since): before that date, the absence of a web touch does not mean the absence of a visit. paging holds limit, offset, returned. Each item of journeys: contact (email, phone, first_name, last_name), first_seen_at, last_seen_at, first_touch and last_touch (occurred_at, event_type, is_paid, platform, utm, ad_ids), touchpoint_counts (web, conversion, total), milestones (first_lead_at, first_booking_at, first_sale_at, revenue), event_types_seen. from and to filter on the contact's last activity date.
  • journeys/contact: an object { contact, first_touch_at, last_touch_at, counts, touchpoints }. counts holds total, web, conversion, page_view. touchpoints is the full chronological chain: each point carries the UTM (utm), the ad ids and click ids captured at that moment.
  • touchpoints: an object { paging, touchpoints }, newest first, for syncing into your warehouse. Each point carries among others utm, ad_ids, click_ids, channel, value, currency, product, is_imported. A kind other than web or conversion returns 200 with { "error": "invalid kind (web|conversion)", "touchpoints": [] }.

The utm object holds source, medium, campaign, content, term, campaign_name, adset_name, ad_name, channel, type, sequence, step.

bash
curl "https://app.durum.ai/api/v1/journeys/contact?email=jean@example.com&pageviews=true" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY"

Creatives ​

RouteScopeParameters
GET /creatives/requestsread:creativesstatus, unnotified (1 or true), limit (integer, default 50, max 200)

Creative needs are the requests DURUM writes when creatives are missing. Without status, the route returns the requests in progress (open and partial); with status, only that exact status. unnotified=1 returns only the requests never relayed to a team. Response: an array from oldest to newest, each item with id, reason, status, requested_count, filled_count, context, created_at, notified_at.

The intended flow for an external tool: read with unnotified=1, relay to the team, then acknowledge with POST /creatives/requests/ack (see Writes). The acknowledgement only sets notified_at; it does not close the request.

bash
curl "https://app.durum.ai/api/v1/creatives/requests?unnotified=1" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY"

ICP and pixels ​

RouteScopeParameters
GET /icp/criteriaread:icpnone
GET /capi/pixelsread:capinone
  • icp/criteria: the JSON object of your ideal customer criteria.
  • capi/pixels: your connected Meta pixels and the default one (routing configuration). Access tokens are never returned: every field whose name contains access_token, secret or api_key is stripped before sending.
bash
curl "https://app.durum.ai/api/v1/capi/pixels" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY"

Writes ​

Writes take a JSON body. An invalid body returns 400 with the validation message in error. They only act on your account.

RouteMethodsScopeBody
/goalsPUT, POSTwrite:goals{ "goals": [ { "metric_key": "…", "target_value": 0 } ] }
/sale-rulePUT, POSTwrite:settings{ "sale_rule": { … } }
/icpPUT, POSTwrite:icp{ "icp": { … } }
/ads/rules/enablePOST, PUTwrite:ads{ "rule_id": "…", "enabled": true }
/ads/rules/dry-runPOST, PUTwrite:ads{ "rule_id": "…", "dry_run": true }
/ads/rules/deletePOSTwrite:ads{ "rule_id": "…" }
/creatives/upload-urlPOSTwrite:creatives{ "content_type": "image/png", "label": "…" }
/creatives/importPOSTwrite:creatives{ "external_ref": "…", "creative": { … } }
/creatives/requests/ackPOSTwrite:creatives{ "ids": [ "uuid", … ] }

A method not listed for the route answers 405 method_not_allowed.

Goals, sale rule, ICP ​

  • /goals: goals is an array of { metric_key, target_value } objects. Each goal is saved on the monthly period. A body without goals returns 400 missing goals object. On success, data is { "success": true }.
  • /sale-rule: sale_rule is your sale counting rule object (how a sale is counted and deduplicated). Body without sale_rule: 400 missing sale_rule object.
  • /icp: icp replaces your ICP criteria. Body without icp: 400 missing icp object.
bash
curl -X PUT "https://app.durum.ai/api/v1/goals" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"goals":[{"metric_key":"revenue","target_value":100000}]}'

Ad rules ​

rule_id is required (otherwise 400 missing rule_id). enabled and dry_run are true unless you explicitly send false. Dry-run mode computes without acting on Meta. Deletion is final. A rule that is not yours answers 403 forbidden. Arming a rule for real (dry_run: false) requires a paid tier: on the Discovery plan, the refusal carries the code plan_free_no_autopilot (the API itself already answers 403 plan_free_no_api there).

bash
curl -X POST "https://app.durum.ai/api/v1/ads/rules/dry-run" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rule_id":"RULE_ID","dry_run":true}'

Importing a creative in two steps ​

An import lands in the reserve of your creative library: it publishes nothing, touches no budget and talks to no ad network. The visual can only point to a file uploaded through the authorization issued in step 1: a foreign URL is refused.

Step 1: POST /creatives/upload-url. Body: content_type required, one of image/jpeg, image/png, image/webp, video/mp4 (otherwise 400); label optional (readable label, sanitized, 60 characters max). The storage path is composed server-side inside your account's folder. Response:

json
{
  "data": {
    "asset_url": "https://…/creative-assets/…/my-visual-….png",
    "upload_url": "https://…",
    "token": "…",
    "path": "…/2026/09/my-visual-….png",
    "content_type": "image/png"
  }
}

Then send the file with a PUT to upload_url, with the same Content-Type and the binary file as body. The authorization is short-lived.

Step 2: POST /creatives/import. Body: external_ref required (your stable identifier, 200 characters max) and creative, an object whose recognized fields are angle, primary_text, headline, hook, cta, destination_url, asset_url (the asset_url from step 1, as is), asset_kind (image or video), variant_label, visual_subtype, funnel, product, audience, video_script, source (free provenance object). Response: { "id", "external_ref", "angle", "score", "en_attente" } where en_attente is the number of unpublished creatives in the reserve and score is null until computed.

The import is idempotent: replaying the same external_ref updates the creative instead of creating a second one. An already published creative is never overwritten; its id is simply returned.

bash
curl -X POST "https://app.durum.ai/api/v1/creatives/upload-url" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content_type":"image/png","label":"september-offer"}'

curl -X PUT "RECEIVED_UPLOAD_URL" \
  -H "Content-Type: image/png" \
  --data-binary @september-offer.png

curl -X POST "https://app.durum.ai/api/v1/creatives/import" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"external_ref":"drive-2026-09-offer-1","creative":{"angle":"Tax savings","headline":"Cut your taxes","primary_text":"…","asset_url":"RECEIVED_ASSET_URL","asset_kind":"image"}}'

Acknowledging creative needs ​

POST /creatives/requests/ack with ids, a non-empty array of request UUIDs (otherwise 400 missing ids (array of request UUIDs)). Only your never-relayed requests are marked; data is the number of requests updated. Replaying the same batch has no effect.

bash
curl -X POST "https://app.durum.ai/api/v1/creatives/requests/ack" \
  -H "Authorization: Bearer durum_sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ids":["3f2a…","9c1d…"]}'

Fields never returned ​

Before sending, every response is walked at all nesting levels and every field whose name contains one of the following fragments is removed:

  • credentials and secrets: access_token, refresh_token, api_key, apikey, secret, signing, service_role, password, webhook_secret, encryption, tokenenc, bearer;
  • AI internals: model, model_name, provider, prompt, system_prompt, temperature, embedding;
  • internal costs: cost, cost_estimate, token_count, tokens_used, price_input, price_output;
  • margins: gross_margin, net_profit, net_profit_after_ads, wholesale, markup, platform_fee, margin_pct, agency_margin, contribution_margin.

Any text value that looks like an AI model identifier is replaced with DURUM.ai. finance/pnl adds its own fragments (see Sales and finance).

Response codes ​

CodeBodyMeaning
200{ "data": … }success
400{ "error": "…" }invalid parameter or body; error holds the message (for example missing rule_id, invalid_id, external_ref is required)
401{ "error": "invalid_or_missing_api_key" }key missing, invalid, revoked or expired
403{ "error": "insufficient_scope", "required": "…" }the key lacks the required scope
403{ "error": "plan_free_no_api" }the workspace is on the Discovery plan: the REST API and the MCP server require a paid tier
403{ "error": "forbidden" }the resource does not belong to your account
404{ "error": "unknown_route", "route": "/api/v1/…" }unknown route
404{ "error": "not_found" }account or object not found for this write
404{ "error": "no_recording_available" }calls/recording without a file
405{ "error": "method_not_allowed" }method not supported for this route
429{ "error": "rate_limited", "retry_after_seconds": 60 }rate limit exceeded
429{ "error": "too_many_failed_attempts", "retry_after_seconds": 60 }too many failed authentications from your IP address
500{ "error": "tenant_barrier" }response blocked by the account check; contact DURUM
502{ "error": "upstream_error" }internal error while reading or writing; retry later

Powered by DURUM.ai: ad attribution and operational intelligence