Atribu
API Reference

Demo profile

Sixty days of seeded data on a real profile, run through the real attribution engine, with a CAPI destination that records instead of sending.

Endpoints
POST   /api/v1/profiles/demo?workspace_id={workspace_id}
DELETE /api/v1/profiles/demo?workspace_id={workspace_id}

Scope: analytics:read, plus an ACTIVE owner or admin membership on the workspace. Session bearers and user tokens only.

One call gives a workspace something to read before any live account is connected: a profile flagged is_demo, sixty days of one fictional clinic's Meta traffic, and a Meta CAPI destination that records what it would have sent instead of sending it.

It exists so that the rest of this reference has something to run against. Every curl on the Overview, Timeseries, Breakdowns, Campaigns, Conversions and Conversion Sync pages answers with real numbers on a demo profile, on the first day, with no Meta, GHL or Stripe account connected.

The engine runs for real

The seed writes the pipeline's inputs — ad structure, daily spend, identities, marketing touches, and outcome events through the same CRM-ingest primitive every real provider uses — and then enqueues an ordinary attribution recompute.

conversions and the credit assigned across all five attribution models come out of the pipeline, not out of the seed. Nothing here is precomputed and nothing is faked downstream of ingest.

That is also why seeded.conversions in the response is what the recompute will produce: it runs asynchronously. Poll GET /api/v1/overview until the numbers appear — typically seconds.

What is seeded

Ads3 campaigns × 2 ad sets × 8 ads, as ad_entity_dim rows
Spendone row per ad per day, 60 days — so ROAS has a denominator
People~40 customers, with unroutable @demo.invalid emails and reserved-block phone numbers
Touchespaid Meta clicks (with fbclid and UTMs following Atribu's own convention) plus organic and direct visits
Outcomeslead_createdappointment_bookedpayment_received, plus one refund so cash netting is visible
Definitionsthe standard conversion_definitions set, seeded by the database exactly as for a real profile
Exportsone Meta CAPI destination and one enabled signal rule on the cash definition

The dataset is deterministic: the same workspace seeded twice produces the same numbers.

Exports record; they never leave

The destination is an ordinary tracking_export_destinations row over an ordinary connection, with a placeholder pixel id and a placeholder token. Nothing about it is a special "recording destination" type — the candidate builder, the cross-path dedup, the privacy and legal gates and the payload build all run unmodified.

Recording is a property of the profile. In the export worker, an export whose profile is is_demo is written to conversion_exports with the exact payload Meta would have received and is never posted:

conversion_exports
{
  "status": "skipped",
  "skip_reason": "demo_profile_recorded",
  "payload": { "data": [{ "event_name": "Purchase", "event_id": "…", "user_data": {  } }] }
}

skipped rather than a new recorded status because the status column is a closed set and the honest word for "this was never sent" is already in it. The reason column is what carries why.

A demo profile can never reach Meta, even if someone edits its destination to a real pixel id. The suppression is on the profile, at the last moment before the network call.

What the demo is excluded from

Reads scoped to the demo profile show its money. That is the point — an empty demo would demonstrate nothing.

Every read that spans the profiles of a workspace leaves it out:

  • the workspace pacing aggregate;
  • Top Performers cohorts and the creative_feature_store rebuild (a demo profile is never scored, so it can never appear in a leaderboard or move a real ad's percentile);
  • the agency Reports hub;
  • alert digest emails;
  • the ML ad-fact and creative-lifecycle passes;
  • the active_profiles plan limit — a demo consumes no seat, and creating one is not gated by the limit.

Create

Request
curl -X POST "https://api.atribu.app/api/v1/profiles/demo?workspace_id=$WORKSPACE_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"currency":"USD","timezone":"America/Santiago"}'

Both body fields are optional; an empty body is fine.

201 Created
{
  "data": {
    "profile": {
      "id": "…",
      "name": "Demo — Acme Agency",
      "workspace_id": "…",
      "currency": "USD",
      "is_demo": true
    },
    "seeded": {
      "touches": 103,
      "outcome_events": 79,
      "conversions": 79,
      "spend_days": 60,
      "definitions": 12
    },
    "destination": { "id": "…", "mode": "recording" },
    "created": true,
    "warnings": []
  },
  "meta": { "workspace_id": "…" }
}

Idempotent. A workspace holds at most one demo profile. Calling this again re-seeds the existing one and answers 200 with created: false; only the first call answers 201. warnings is empty on a clean run and names anything that did not land — the seed never half-fails silently.

Delete

Request
curl -X DELETE "https://api.atribu.app/api/v1/profiles/demo?workspace_id=$WORKSPACE_ID" \
  -H "Authorization: Bearer $TOKEN"
200 OK
{
  "data": {
    "deleted": true,
    "profile_id": "…",
    "archived": true,
    "retained_audit_rows": 4
  },
  "meta": { "workspace_id": "…" }
}

Delete means archive + purge, and audit rows are retained. The seeded data — touches, outcome events, conversions, attribution, spend, the ad structure, the customers and the conversion definitions — is deleted, and the profile is archived rather than removed.

conversion_exports is the record of what Atribu transmitted (or, for a demo, would have transmitted) on your behalf. A database trigger refuses to let any user-facing path erase it, so those rows — and the handful their foreign keys pin in place: the exported outcome event, its customer, the destination and the rule — stay attached to the archived profile. retained_audit_rows counts them.

The archived profile disappears from every read (archived_at IS NULL is the filter the whole product already applies) and releases the workspace's demo slot, so POST /api/v1/profiles/demo succeeds immediately afterwards.

Deleting nothing answers 200 with deleted: false, not 404, so a retry never looks like a failure. A 409 demo_purge_blocked means a trigger refused part of the purge; the profile is archived regardless and retrying changes nothing.

Telling a demo apart

is_demo is on GET /api/v1/profile and on every row of GET /api/v1/profiles. Read the flag — never the name, which anyone may change.

Errors

statuswhen
400 invalid_requestmissing or malformed workspace_id, or a malformed body
400 validation_erroran unknown body field, or a currency that is not a 3-letter code
403 insufficient_scopethe credential is an API key — it names one profile and belongs to no workspace
404 not_foundthe workspace does not exist, or you are not an ACTIVE owner/admin member

SDK

const demo = await atribu.profile.createDemo(workspaceId);
// … poll atribu.analytics.overview({ from, to }) until the numbers land
await atribu.profile.deleteDemo(workspaceId);

On this page