ConstraintsFind the constraint in the paid funnel

Find the constraint in the paid funnel

Builds the full funnel per campaign (or ad set / ad) over the requested window and returns, for each row, the step whose fix would unlock the most conversions. The funnel is impressions → clicks → sessions → [leads] → conversions. Each step is compared against the pooled rate across every row in the account (not a global benchmark), and the constraint is the step with the highest recoverable_conversions — the extra conversions that closing the gap to that pooled rate would produce, after carrying the row's own downstream rates. A campaign already at or above the pooled rate on every step returns constraint: null: nothing to fix, scale it instead. Supply lead_event to split the on-site step into landing (visit → lead) and sales (lead → conversion). Without it those collapse into a single site step, because sessions → conversions genuinely cannot distinguish a page problem from a closing problem. Conversions are credited to sessions — their own when it resolved to a campaign, otherwise the person's nearest paid touch within attribution_window_days. This is effectively last-PAID-touch: an organic-session purchase by a person with a paid touch in-window is credited to paid. Pass attribution_window_days=0 for strict own-session-only credit. Always read tracking.match_rate before trusting the rest. It is the share of ad-click sessions whose UTMs resolved to a synced Meta row; a low value means the clicks → sessions step is measuring UTM hygiene rather than landing-page quality.

curl -X GET "https://mythic-analytics.gulp.workers.dev/client/v1/constraints?location_id=example_string&from=2024-12-25&to=2024-12-25&days=28&group_by=campaign&attribution=session&conversion_event=transaction_completed&lead_event=example_string&min_impressions=1000&campaign=example_string&revenue_path=%24.value&table=meta_ads_ads_insights_action_type&attribution_window_days=30&include=audit%2Cevents&resolve_identities=true&benchmark_ad=3.14&benchmark_tracking=3.14&benchmark_landing=3.14&benchmark_sales=3.14&benchmark_site=3.14" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
{
  "success": true,
  "data": {
    "window": {
      "from": "2024-12-25",
      "to": "2024-12-25"
    },
    "grain": "campaign",
    "campaign": "example_string",
    "attribution": "session",
    "conversion_event": "example_string",
    "lead_event": "example_string",
    "steps": [
      {
        "step": "ad",
        "from": "example_string",
        "to": "example_string"
      }
    ],
    "benchmarks": {},
    "benchmark_sources": {},
    "pooled_rates": {},
    "stored_benchmarks": {
      "default": {},
      "default_notes": {},
      "campaigns": [
        {
          "campaign": "example_string",
          "benchmarks": {},
          "notes": {},
          "matched_rows": 42
        }
      ],
      "not_applicable_steps": [
        "example_string"
      ],
      "not_applicable_reason": "example_string"
    },
    "stored_benchmarks_unavailable": "example_string",
    "tracking": {
      "matched_sessions": 42,
      "unmatched_sessions": 42,
      "match_rate": 3.14,
      "joined_by_id": 123,
      "joined_by_url_param": 42,
      "grain_resolvable": true,
      "joined_by_name": 42,
      "click_to_session_rate": 3.14,
      "unexpanded_macro": {
        "sessions": 42,
        "sample": "example_string",
        "message": "example_string"
      }
    },
    "conversion_attribution": {
      "window_days": 42,
      "identity": "distinct_id",
      "total_conversions_in_window": 42,
      "credited_via_session": 42,
      "credited_via_person": 42,
      "uncredited": 42,
      "credited_coverage": 3.14,
      "identity_resolution": {
        "enabled": true,
        "conversions_resolved_to_person": 42,
        "conversions_resolved_rate": 3.14,
        "distinct_converting_persons": 42
      }
    },
    "diagnostic": {
      "code": "no_campaign_data",
      "message": "example_string"
    },
    "audit": {},
    "event_inventory": {},
    "auto_selected": {
      "conversion_event": "transaction_completed",
      "lead_event": "input_captured",
      "revenue_path": "$.meta_amount",
      "chosen_from": 20
    },
    "warning": {
      "code": "low_conversion_coverage",
      "message": "example_string"
    },
    "excluded_low_volume": 42,
    "min_impressions": 42,
    "rows": [
      {
        "campaign_id": "example_string",
        "campaign_name": "John Doe",
        "impressions": 42,
        "clicks": 42,
        "spend": 3.14,
        "sessions": 42,
        "lead_sessions": 42,
        "converting_sessions": 42,
        "conversions": 42,
        "revenue": 3.14,
        "top_landing_page": "example_string",
        "cpa": 3.14,
        "roas": 3.14,
        "funnel": {},
        "benchmarks": {},
        "benchmark_sources": {},
        "creative": {
          "is_representative": true,
          "headline": "example_string",
          "body": "example_string",
          "description": "example_string",
          "call_to_action": "example_string",
          "destination_url": "example_string",
          "effective_destination_url": "example_string",
          "display_url": "example_string",
          "url_tags": "example_string",
          "image_url": "example_string",
          "video_id": "example_string",
          "format": "image",
          "asset_count": 10,
          "assets": [
            {
              "headline": "example_string",
              "body": "example_string",
              "image_url": "example_string",
              "link": "example_string"
            }
          ],
          "unavailable": "example_string",
          "reason_code": "creative_not_synced"
        },
        "converting_persons": 42,
        "person_ids": [
          "example_string"
        ],
        "measurable": {},
        "status": "constrained",
        "constraint": {
          "step": "ad",
          "diagnosis": "example_string",
          "rate": 3.14,
          "benchmark_rate": 3.14,
          "benchmark_source": "override",
          "benchmark_note": "example_string",
          "confidence": "low",
          "confidence_reason": "example_string",
          "rate_vs_benchmark": 3.14,
          "recoverable_conversions": 3.14
        }
      }
    ],
    "summary": {
      "primary_constraint": {
        "step": "example_string",
        "diagnosis": "example_string",
        "recoverable_conversions": 3.14,
        "entities": 42,
        "spend": 3.14
      },
      "by_step": [
        {}
      ]
    }
  }
}
GET
/client/v1/constraints
GET
Base URLstring

Target server for requests. Edit to use your own host.

Bearer Token
Bearer Tokenstring
Required

Agency key (ak_) or location secret key (sk_). Scoped keys (mcp_) are accepted too and need constraints:read or constraints:write. See Using an mcp_ key over HTTP.

Agency key (ak_) or location secret key (sk_). Scoped keys (mcp_) are accepted too and need constraints:read or constraints:write. See Using an mcp_ key over HTTP.
query
location_idstring

Location to scope to. Required for ak_ keys (or send the X-Location-Id header); ignored for sk_.

query
fromstring

Window start (YYYY-MM-DD). Defaults to 28 days before to.

Format: date
query
tostring

Window end (YYYY-MM-DD), inclusive. Defaults to yesterday.

Format: date
query
daysinteger

Window length in days, inclusive of both ends — the same shorthand /client/v1/data takes. days=90 ending yesterday is the usual call. Ignored when from is given explicitly. Out-of-range values are rejected, not clamped.

Min: 1 • Max: 365
query
group_bystring

Grain of the analysis. ad is where creative lives, but it matches through utm_content, which carries a usable ad identifier on far fewer accounts than utm_id does — expect a lower match_rate.

Options: campaign, adset, ad
query
attributionstring

session uses the conversion's own session's campaign when it has one, else the person's MOST RECENT paid touch in the window — effectively last-paid-touch, closest to what the ad platform itself would claim. first_touch uses the person's EARLIEST paid touch: the campaign that first acquired them, however long ago.

Options: session, first_touch
query
conversion_eventstring

Event name counted as the conversion. Discover this client's real event names via GET /client/v1/data/events/breakdown?dimension=event_name. Pass `auto` to let the endpoint choose: it runs the eligibility probe first, takes its suggested pairing, and reports the choice under auto_selected so it is visible rather than magic. That replaces a round trip you would otherwise make to /constraints/events purely to learn which event to ask about. An explicit lead_event still wins over the suggestion. If nothing in the window can serve as a conversion you get a 422 no_usable_conversion_event rather than an analysis of a dead funnel.

query
lead_eventstring

Optional mid-funnel event. Supplying it splits the on-site step into landing and sales. Must differ from conversion_event.

query
min_impressionsinteger

Rows below this many impressions are excluded from the analysis but still counted in the pooled benchmarks — dropping them from the pool would inflate the bar the surviving rows are judged against.

query
campaignstring

Drill into one campaign, matched by campaign id or name (trimmed, case-insensitive). With group_by=adset or ad it scopes the rows to that campaign's children. Benchmarks stay the account-wide pooled rates, so the campaign is still judged against the whole account — that comparison is the point of the drilldown. If nothing matches, the response carries a campaign_not_found diagnostic instead of a silent empty list.

query
revenue_pathstring

JSON path to the conversion's monetary value inside the event's properties. properties_json is the properties object, so the path is $.value, not $.properties.value.

query
tablestring

Meta insights table in the client's dataset, if it differs from the platform default.

query
attribution_window_daysinteger

How long after a campaign touch a conversion may still be credited to it. A conversion with no session of its own (all CRM/server-side conversions) is credited to the person's nearest campaign touch inside this window; touches are looked up back to from minus the window, so early-window conversions still find the visit that earned them. Meta's own default is 7-day click — pass 7 for numbers comparable to Ads Manager. Pass 0 for strict own-session credit only (disables person crediting; the conservative reading).

Min: 0 • Max: 365
query
includestring

Comma-separated optional sections, all off by default: include=creative,audit,events. creative adds each row's ad copy, destination and assets. Off by default because it is not free: measured on one client it scans 285.9 MB on every call at every grain, independent of the report window (the creatives table sits in a single partition that nothing prunes, and one JSON column is 139 MB of it). If you join creative yourself, leave it off. Row labels are unaffected — ad_name comes from ads-insights and top_landing_page from sessions. audit returns the tracking-hygiene report (the same body as /constraints/audit) under an audit key, and events returns the event inventory (/constraints/events) under event_inventory. These stay separate BigQuery jobs — one request cannot make them one scan — but they run in parallel with the analysis, so a caller that needed all three stops paying three serial round trips and pays the slowest one instead. A section that fails is reported as {"unavailable": "<reason>"} inside a 200; the analysis itself is never failed by it. An unknown name is a 400 rather than a silent omission.

query
resolve_identitiesboolean

Identity stitching — resolve people through the central identity snapshots (merge tombstones collapsed) rather than the raw distinct_id. On by default, and you almost never want it off. A CRM/server-side conversion arrives with no session of its own, so credit can only reach a campaign through the person; the conversion's distinct_id is a CRM contact id that shares nothing with the browsing device's id until the snapshot maps both onto one person. Measured on one client over 30 days (273 conversions): the raw distinct_id could credit 10% of conversions to a paid touch, canonical persons credited 81%. Pass false (or 0) to opt out — useful only to reproduce a figure from before this was the default. conversion_attribution.identity_resolution reports how much of the resolution actually worked.

query
benchmark_adnumber

Target rate for the impressions -> clicks step, replacing the account's pooled rate. A rate between 0 and 1 — a 2% CTR target is 0.02. Values above 1 are rejected, not interpreted as percentages.

Format: double • Min: 0 • Max: 1
query
benchmark_trackingnumber

Target rate for the clicks -> sessions step. Rate between 0 and 1.

Format: double • Min: 0 • Max: 1
query
benchmark_landingnumber

Target rate for the sessions -> leads step. Rate between 0 and 1. Requires lead_event; without it this step does not exist and the request is rejected.

Format: double • Min: 0 • Max: 1
query
benchmark_salesnumber

Target rate for the leads -> conversions step. Rate between 0 and 1. Requires lead_event.

Format: double • Min: 0 • Max: 1
query
benchmark_sitenumber

Target rate for the combined sessions -> conversions step. Rate between 0 and 1. Only valid WITHOUT lead_event, which splits this step into landing and sales.

Format: double • Min: 0 • Max: 1
Request Preview
Response

Response will appear here after sending the request

Authentication

header
Authorizationstring
Required

Bearer token. Agency key (ak_) or location secret key (sk_). Scoped keys (mcp_) are accepted too and need constraints:read or constraints:write. See Using an mcp_ key over HTTP.

Query Parameters

location_idstring

Location to scope to. Required for ak_ keys (or send the X-Location-Id header); ignored for sk_.

fromstring

Window start (YYYY-MM-DD). Defaults to 28 days before to.

tostring

Window end (YYYY-MM-DD), inclusive. Defaults to yesterday.

daysinteger

Window length in days, inclusive of both ends — the same shorthand /client/v1/data takes. days=90 ending yesterday is the usual call. Ignored when from is given explicitly. Out-of-range values are rejected, not clamped.

group_bystring

Grain of the analysis. ad is where creative lives, but it matches through utm_content, which carries a usable ad identifier on far fewer accounts than utm_id does — expect a lower match_rate.

Allowed values:campaignadsetad
attributionstring

session uses the conversion's own session's campaign when it has one, else the person's MOST RECENT paid touch in the window — effectively last-paid-touch, closest to what the ad platform itself would claim. first_touch uses the person's EARLIEST paid touch: the campaign that first acquired them, however long ago.

Allowed values:sessionfirst_touch
conversion_eventstring

Event name counted as the conversion. Discover this client's real event names via GET /client/v1/data/events/breakdown?dimension=event_name. Pass auto to let the endpoint choose: it runs the eligibility probe first, takes its suggested pairing, and reports the choice under auto_selected so it is visible rather than magic. That replaces a round trip you would otherwise make to /constraints/events purely to learn which event to ask about. An explicit lead_event still wins over the suggestion. If nothing in the window can serve as a conversion you get a 422 no_usable_conversion_event rather than an analysis of a dead funnel.

lead_eventstring

Optional mid-funnel event. Supplying it splits the on-site step into landing and sales. Must differ from conversion_event.

min_impressionsinteger

Rows below this many impressions are excluded from the analysis but still counted in the pooled benchmarks — dropping them from the pool would inflate the bar the surviving rows are judged against.

campaignstring

Drill into one campaign, matched by campaign id or name (trimmed, case-insensitive). With group_by=adset or ad it scopes the rows to that campaign's children. Benchmarks stay the account-wide pooled rates, so the campaign is still judged against the whole account — that comparison is the point of the drilldown. If nothing matches, the response carries a campaign_not_found diagnostic instead of a silent empty list.

revenue_pathstring

JSON path to the conversion's monetary value inside the event's properties. properties_json is the properties object, so the path is $.value, not $.properties.value.

tablestring

Meta insights table in the client's dataset, if it differs from the platform default.

attribution_window_daysinteger

How long after a campaign touch a conversion may still be credited to it. A conversion with no session of its own (all CRM/server-side conversions) is credited to the person's nearest campaign touch inside this window; touches are looked up back to from minus the window, so early-window conversions still find the visit that earned them. Meta's own default is 7-day click — pass 7 for numbers comparable to Ads Manager. Pass 0 for strict own-session credit only (disables person crediting; the conservative reading).

includestring

Comma-separated optional sections, all off by default: include=creative,audit,events. creative adds each row's ad copy, destination and assets. Off by default because it is not free: measured on one client it scans 285.9 MB on every call at every grain, independent of the report window (the creatives table sits in a single partition that nothing prunes, and one JSON column is 139 MB of it). If you join creative yourself, leave it off. Row labels are unaffected — ad_name comes from ads-insights and top_landing_page from sessions. audit returns the tracking-hygiene report (the same body as /constraints/audit) under an audit key, and events returns the event inventory (/constraints/events) under event_inventory. These stay separate BigQuery jobs — one request cannot make them one scan — but they run in parallel with the analysis, so a caller that needed all three stops paying three serial round trips and pays the slowest one instead. A section that fails is reported as \{&quot;unavailable&quot;: &quot;\<reason\>&quot;\} inside a 200; the analysis itself is never failed by it. An unknown name is a 400 rather than a silent omission.

Example:
audit,events
resolve_identitiesboolean

Identity stitching — resolve people through the central identity snapshots (merge tombstones collapsed) rather than the raw distinct_id. On by default, and you almost never want it off. A CRM/server-side conversion arrives with no session of its own, so credit can only reach a campaign through the person; the conversion's distinct_id is a CRM contact id that shares nothing with the browsing device's id until the snapshot maps both onto one person. Measured on one client over 30 days (273 conversions): the raw distinct_id could credit 10% of conversions to a paid touch, canonical persons credited 81%. Pass false (or 0) to opt out — useful only to reproduce a figure from before this was the default. conversion_attribution.identity_resolution reports how much of the resolution actually worked.

benchmark_adnumber

Target rate for the impressions -\> clicks step, replacing the account's pooled rate. A rate between 0 and 1 — a 2% CTR target is 0.02. Values above 1 are rejected, not interpreted as percentages.

benchmark_trackingnumber

Target rate for the clicks -\> sessions step. Rate between 0 and 1.

benchmark_landingnumber

Target rate for the sessions -\> leads step. Rate between 0 and 1. Requires lead_event; without it this step does not exist and the request is rejected.

benchmark_salesnumber

Target rate for the leads -\> conversions step. Rate between 0 and 1. Requires lead_event.

benchmark_sitenumber

Target rate for the combined sessions -\> conversions step. Rate between 0 and 1. Only valid WITHOUT lead_event, which splits this step into landing and sales.

Responses

successboolean
dataobject