Public API surface map
Routes in the Apex public API v1 contract, grouped by host and authentication.
Public API v1
The public Apex API is the /api/v1/* surface defined by the served OpenAPI contract. Contract routes require an API key unless listed under "Routes without API-key authentication". The same document also describes the explicitly listed dashboard session operations. Use /api/v1/openapi.json for methods, request bodies, responses, and schemas.
Dashboard handlers outside /api/v1/* are session-scoped application internals. Only those registered under "Dashboard session routes" appear in this map. Separately, GET /api/exp is the SDK's storefront-configuration transport. It takes a shopId, needs no session or API key, and is served with a public 30-second cache. It is an SDK delivery endpoint, not part of the v1 contract; integrate through the hosted SDK script rather than calling it directly.
The route inventory below is verified against src/lib/api/openapi-spec.ts; its drift test fails if the OpenAPI paths, their operation-level servers, their security declarations, or this list change independently. OpenAPI parameter syntax ({id}) is used verbatim. Worker operations declare the events host as their server and are listed with the full URL. Dashboard session operations declare / as their server and use absolute /api/* paths.
Contract routes
/api/v1/account/profile/api/v1/admin/client-links/api/v1/admin/customers/api/v1/admin/customers/{orgId}/api/v1/admin/customers/{orgId}/move-stores/api/v1/admin/customers/{orgId}/setup/api/v1/admin/customers/{orgId}/unique-visitors/api/v1/admin/settings/api/v1/admin/shops/{id}/archive/api/v1/admin/shops/{id}/restore/api/v1/admin/shops/{id}/settings/api/v1/admin/shops/{id}/tracking-health/api/v1/admin/tracking/quarantine/api/v1/analytics/entry-exit/api/v1/analytics/geo/api/v1/analytics/order-journey/api/v1/analytics/pages/api/v1/analytics/profit/api/v1/analytics/realtime/api/v1/analytics/sessions/api/v1/analytics/stats/api/v1/analytics/suggest-goals/api/v1/analytics/timeseries/api/v1/analytics/traffic/api/v1/analytics/url-inventory/api/v1/analytics/visitor-series/api/v1/assets/api/v1/assets/{id}/api/v1/assets/{id}/restore/api/v1/audit-logs/api/v1/backlog/api/v1/backlog/fields/api/v1/backlog/fields/{id}/api/v1/backlog/{id}/api/v1/backlog/{id}/approve/api/v1/backlog/{id}/archive/api/v1/backlog/{id}/design/api/v1/backlog/{id}/reject/api/v1/backlog/{id}/restore/api/v1/brand-context/api/v1/commerce/catalog/api/v1/consent/api/v1/cost-settings/api/v1/data-deletion/api/v1/data-export/api/v1/environments/api/v1/events/export/api/v1/exclusion-groups/api/v1/exclusion-groups/{id}/api/v1/exclusion-groups/{id}/members/api/v1/exclusion-groups/{id}/restore/api/v1/experiment-events/api/v1/experiments/api/v1/experiments/by-slug/{slug}/api/v1/experiments/conflicts/api/v1/experiments/filesystem/schema/api/v1/experiments/generate/api/v1/experiments/{id}/api/v1/experiments/{id}/commerce/api/v1/experiments/{id}/commerce/price/csv/api/v1/experiments/{id}/commerce/price/rows/api/v1/experiments/{id}/complete/api/v1/experiments/{id}/duplicate/api/v1/experiments/{id}/filesystem/api/v1/experiments/{id}/filesystem/apply/api/v1/experiments/{id}/filesystem/plan/api/v1/experiments/{id}/idea/api/v1/experiments/{id}/pause/api/v1/experiments/{id}/qa/api/v1/experiments/{id}/qa/commerce-probes/api/v1/experiments/{id}/qa/events/api/v1/experiments/{id}/qa/evidence/api/v1/experiments/{id}/qa/run/api/v1/experiments/{id}/readiness/api/v1/experiments/{id}/results/api/v1/experiments/{id}/results/ad-roas/api/v1/experiments/{id}/results/by-channel/api/v1/experiments/{id}/results/explain/api/v1/experiments/{id}/screenshots/api/v1/experiments/{id}/start/api/v1/experiments/{id}/variations/generate/api/v1/experiments/{id}/variations/{variationId}/build/api/v1/experiments/{id}/variations/{variationId}/files/api/v1/experiments/{id}/variations/{variationId}/files/{path}/api/v1/experiments/{id}/variations/{variationId}/revisions/api/v1/experiments/{id}/variations/{variationId}/workspace/api/v1/external-pixels/snippet/api/v1/external-tracking/api/v1/flags/api/v1/flags/{id}/api/v1/flags/{id}/activate/api/v1/flags/{id}/archive/api/v1/flags/{id}/code/api/v1/flags/{id}/conclude/api/v1/flags/{id}/experiences/api/v1/flags/{id}/experiences/{experienceId}/api/v1/flags/{id}/pause/api/v1/flags/{id}/qa-devices/api/v1/flags/{id}/qa-devices/{identity}/api/v1/flags/{id}/reopen/api/v1/flags/{id}/rules/api/v1/flags/{id}/rules/reorder/api/v1/flags/{id}/status/api/v1/forms/api/v1/forms/detect/api/v1/forms/{key}/api/v1/forms/{key}/errors/api/v1/forms/{key}/fields/api/v1/forms/{key}/fields/{identity}/api/v1/forms/{key}/sessions/api/v1/forms/{key}/summary/api/v1/funnels/api/v1/funnels/{id}/api/v1/funnels/{id}/restore/api/v1/funnels/{id}/run/api/v1/goals/api/v1/goals/{id}/api/v1/goals/{id}/restore/api/v1/harness/sessions/api/v1/harness/sessions/{id}/publish/api/v1/harness/sessions/{id}/versions/api/v1/inbox/messages/api/v1/inbox/read-state/api/v1/inspect/api/v1/install-domains/api/v1/install-status/api/v1/install/assess/api/v1/key/api/v1/keys/api/v1/keys/self/shop/api/v1/keys/{id}/api/v1/landing-pages/api/v1/landing-pages/domains/api/v1/landing-pages/domains/{id}/api/v1/landing-pages/domains/{id}/verify/api/v1/landing-pages/{id}/api/v1/landing-pages/{id}/deploy/api/v1/landing-pages/{id}/experiment/api/v1/landing-pages/{id}/filesystem/api/v1/landing-pages/{id}/filesystem/apply/api/v1/landing-pages/{id}/filesystem/plan/api/v1/landing-pages/{id}/filesystem/upload/api/v1/landing-pages/{id}/filesystem/upload-grant/api/v1/landing-pages/{id}/preview/api/v1/landing-pages/{id}/rollback/api/v1/landing-pages/{id}/versions/api/v1/me/api/v1/me/context/api/v1/notifications/ready/email/api/v1/notifications/ready/slack/api/v1/org/api/v1/org/invitations/api/v1/org/invitations/{id}/api/v1/org/members/api/v1/org/members/{id}/api/v1/pages/api/v1/pages/{id}/api/v1/personalizations/api/v1/personalizations/{id}/api/v1/personalizations/{id}/activate/api/v1/personalizations/{id}/archive/api/v1/personalizations/{id}/pause/api/v1/preview-sessions/api/v1/roadmap/approved/api/v1/roadmap/items/{id}/handoff/api/v1/runtime-settings/api/v1/runtime-settings/recipes/api/v1/screenshots/{jobId}/api/v1/segments/api/v1/segments/{id}/api/v1/shops/api/v1/shops/{id}/api/v1/shops/{id}/audit-logs/api/v1/shops/{id}/auto-stop/api/v1/shops/{id}/engine/api/v1/shops/{id}/engine/guardrails/api/v1/shops/{id}/engine/interventions/{iid}/rollout/api/v1/shops/{id}/engine/pause/api/v1/shops/{id}/engine/proposals/{pid}/approve/api/v1/shops/{id}/engine/proposals/{pid}/reject/api/v1/shops/{id}/engine/proposals/{pid}/veto/api/v1/shops/{id}/engine/start/api/v1/shops/{id}/engine/stop/api/v1/shops/{id}/guardian/api/v1/shops/{id}/install-instructions/api/v1/shops/{id}/plugin-token/api/v1/shops/{id}/shopware/prebind/api/v1/shops/{id}/store-profile/api/v1/shops/{id}/store-profile/facts/{factId}/api/v1/shops/{id}/store-profile/findings/review/api/v1/shops/{id}/store-profile/learning-runs/api/v1/shops/{id}/testing-defaults/api/v1/signals/api/v1/signals/{id}/api/v1/sos/api/v1/sos/attachment-uploads/api/v1/sos/mine/api/v1/surveys/api/v1/surveys/{id}/api/v1/surveys/{id}/complete/api/v1/surveys/{id}/duplicate/api/v1/surveys/{id}/pause/api/v1/surveys/{id}/start/api/v1/targeting/test/api/v1/team/invitations/api/v1/team/invitations/{id}/api/v1/team/members/api/v1/team/members/{id}/api/v1/telemetry/cli/api/v1/test-results/api/v1/test-results/import/confirm/api/v1/test-results/import/inspect/api/v1/test-results/import/preview/api/v1/test-results/{id}/api/v1/tracking/discovery/api/v1/tracking/health/api/v1/tracking/latencyCompare forced-preview and natural page-load decision latency./api/v1/tracking/simulate-trigger/api/v1/tracking/verifyLook up one test phone or account withidentity=<device or user id>(exactvisitor_idoruser_id, 1 to 128 characters after trimming). Use the device UUID before identify and the app user id after identify. With identity,minutesdefaults to 60 and is capped at 1,440; without it, the default remains 120 and the maximum is 10,080. Events include platform, app version, SDK version and environment. Requires a private API key withexperiments:readpermission./api/v1/usage/unique-visitors/api/v1/variations/api/v1/variations/{id}/api/v1/variations/{id}/restore/api/v1/webhooks/api/v1/webhooks/{id}/api/v1/webhooks/{id}/deliveries/api/v1/webhooks/{id}/restore/api/v1/webhooks/{id}/test
Organization member suspension
Owners and admins can use PATCH /api/v1/org/members/{id} with
{ "suspended": true, "reason": "Offboarded" } to suspend a member, or
{ "suspended": false } to reinstate them. The optional reason must contain
non-whitespace text and is only allowed when suspending. Send role or
default_shop_role changes in a separate request; combining them with
suspended returns 400.
The member list and PATCH response include nullable suspended_at,
suspended_by, and suspended_reason fields. Repeating the current state
returns 200 without adding another audit entry. Self-suspension and
self-reinstatement return 400 with cannot_suspend_self; suspending the
last active owner returns 400 with last_owner. Missing members return
404 with not_found.
Admins cannot suspend or reinstate owners; both requests return 403.
A suspended member's user key receives 403 with member_suspended when
accessing that organization. Account context discovery excludes suspended
organizations. Access to other active memberships remains available, and
reinstatement restores organization access. CLI login approval refuses an
account key when every organization membership is suspended.
Shop API keys carry no user identity and are not affected by member suspension.
Dashboard session routes
These routes return 404 while the server rollout is off. Brand undo uses explicit all_with_source, copies-only all, or one shop_id, with per-store atomic receipts and access redaction.
These POST operations require a dashboard session cookie and an explicit x-apex-shop header; they do not accept API keys. Both require experiments:write, and restoring a running test also requires experiments:launch and the ordinary resume safety checks, including QA. Preview is read-only. Apply binds the reviewed completion and preview version, requires an idempotency key and a reason, and restores the same run and assignments. A committed refresh failure returns HTTP 200 with receipt warnings; retry the same key to repair publication. Already-sent external effects cannot be recalled, and queued retries or in-flight sends may still deliver.
/api/experiments/{id}/undo-completion/api/experiments/{id}/undo-completion/preview/api/flags/{id}/experiences/api/flags/{id}/experiences/{experienceId}/api/flags/{id}/rules/api/flags/{id}/rules/reorder/api/multi-shop-programs/{id}/actions/undo-completion/api/multi-shop-programs/{id}/actions/undo-completion/preview
Worker runtime routes
Served by the events Worker; the OpenAPI operations declare https://events.drip-apex.com/v1 as their server. Neither route exists on the dashboard host.
https://events.drip-apex.com/v1/assignhttps://events.drip-apex.com/v1/handover/verify
Routes without API-key authentication
These operations declare security: [] in the contract. Handover verification requires a valid X-Drip-Shop header and a signed, unexpired verifyToken in the JSON body. The token must match the resolved shop. A missing or invalid shop header returns 400 before token verification. An invalid, expired, or mismatched token returns 401. No ingest API key is required.
https://events.drip-apex.com/v1/handover/verify
Note: This release adds the /api/v1/analytics/order-journey analytics contract for bounded order-journey diagnostics and Shopify order correlation; see docs/specs/spec-6686-order-journey.md for the contract details.
Results for an audience and written summaries
Call GET /api/v1/experiments/{id}/results?segment=device:mobile for mobile
or ?segment=device:desktop for desktop. Omit both slice parameters for All.
exclude_segment=dim:value reads the complement; segment wins if both are
present. Dimensions are device, country, traffic_source, platform,
campaign, visitor_kind, landing_page and weekday. Values match observed
stored values exactly. currency remains an optional three-letter ISO code.
Every response includes segment_applied: { requested: { dim, value, mode } | null, applied, reason }. Invalid or unknown requests, unobserved values and unavailable
preflights return the unsliced read with applied: false and respectively
invalid_value, unknown_dimension, unobserved_value or preflight_unavailable.
Check this field before labelling numbers as mobile or desktop.
Store-order commerce is unsliced. For an applied segment it becomes
{ unavailable: true, reason: "not_sliced_by_segment" } without store money or
orders. Tracked revenue stays sliced. Per-segment ARPU is directional:
revenue.perTreatment[].result.significance carries
reason: "revenue_significance_uncertified" and unavailableContext: "view";
there is no certified per-device revenue confidence. The primary-goal probability
is decision.judgedConfidence and measures that goal, not ARPU.
Use these fields for a dashboard card, pairing each treatment with control:
| Card number | Field or calculation |
|---|---|
| Users | arms[].visitors |
| Transactions | Revenue goal metric controlDisplayOrders / variantDisplayOrders; check their source fields (attributed or pixel). controlCount / variantCount are converting visitors, not orders. |
| CR | Display orders / arm visitors |
| AOV | Revenue goal metric controlAov / aov (revenue / display orders) |
| ARPU | arms[].revenue / arms[].visitors |
| Each delta | Treatment / control − 1; undefined when control is zero |
| Runtime revenue delta | (Treatment ARPU − control ARPU) × treatment visitors |
Arm revenue includes matched orders without a tracked visit while arm visitors
exclude those visits. The metric's controlValuePerVisitor / valuePerVisitor
use its goal population, so they can differ from card ARPU. Keep these definitions
distinct. Commerce-system order truth is available only on the unsliced call.
The unsliced response also includes narrative, either null or
{ learning, psychology, insights: [{ title, description }], follow_ups, stakeholder_summary, generated_at }. generated_at is the cached row's ISO
creation time. Only a summary matching the current v15 input hash is returned.
When narrative_unavailable_reason is generating, generation runs after the
numbers response; a later All call returns the cached summary. Other reasons are
gated (the conversion significance floor), not_narratable (no readable
narrative dataset) and request_unsliced (a slice was requested). Sliced requests
never look up or generate a summary. Count-every goals in summaries report events
per visitor, not a percentage conversion rate. Concurrent cold All calls can each
generate for the same hash; the existing unique-key upsert retains one row.
CLI: apex experiments results <id-or-slug> --segment device:mobile or
--exclude-segment device:mobile. MCP get_results accepts segment and
exclude_segment with the same values and reports whether the slice applied.
Structured test-result imports
GET /api/v1/test-resultslists past results for the selected shop withsource=import|native|all,limit(1–200, default 50), and cursor pagination in{ data, pagination }.GET /api/v1/test-results/{id}reads a result by UUID, legacy ID, or test key in{ data }; missing or foreign-shop results return 404. Both reads accept shop or user keys withexperiments:read. Each row carriesresult_source(importornative), classified like the Program page: a row with a source ID or source name is imported, including results migrated from the backlog.DELETE /api/v1/test-results/{id}permanently removes one imported result withexperiments:writeand JSON{ "confirm": true }, returning the removed row in{ data }. The full record is retained in the audit log. Native results return 409, absent or foreign-shop results return 404, missing confirmation returns 400, and an unavailable required audit returns 500 without removing anything.POST /api/v1/test-results/import/inspectaccepts multipartfilesand returns detected columns and values.POST /api/v1/test-results/import/previewaccepts the samefilesplus a JSONmappingpart and returns mapped rows, row errors, and warnings without writing.POST /api/v1/test-results/import/confirmaccepts JSON{ rows }in batches of at most 500 and writes imported test results.
All three routes require experiments:write; user API keys must also pin a shop context.
Rollout code edits
PATCH /api/v1/flags/{id}/code requires flags:write. Send custom_js and/or
custom_css (null clears; omission preserves), plus expected_updated_at from
the On variant in GET /flags/{id}, or expected_artifact_hash from a prior
code-save receipt. Stale versions return 409. Only On code and its version are
changed; targeting, schedule, rollout, status, flag identity and Off stay intact.
Edited code replaces that side's external artifact reference, if present.
The response reports saved, artifact_hash, publication, reviewStatus,
blockingIssues and warningIssues (inside data on success). Blocking issues
refuse persistence. A 502 with saved: true means the save committed but shop
publication failed; read back before retrying. Pending/warning publication
statuses remain distinct from persistence. Render evidence is not accepted by
this endpoint; needs_review is advisory for an already active rollout.
CLI: apex flags update-code <id> --js ./on.js [--css ./on.css]. The CLI reads
the current On version before saving (requiring flags:read as well) and never automatically retries a conflict.