Recommendations
The AI media buyer's suggestions, how to apply one safely, and why the apply is idempotent.
Scale a winner, pause an underperformer, reallocate budget, refresh a creative
before fatigue lands. GET /api/v1/recommendations lists the open ones for the
key's profile; the workspace-wide view is the MCP tool
list_workspace_recommendations.
All list parameters are comma-separated and validated strictly — an unknown
value is a 400, never a silent drop. A filter that silently drops an unknown
value returns a different set than the caller asked for and says nothing.
What a recommendation is based on
A recommendation attests; it does not predict. Atribu makes one because an
ad has already closed sales it could attribute — at least 3 of them — and
its cost per sale is at least 10% away from that client's own median. Every
number in rationale.evidence is reproducible by the advertiser: ad spend from
Meta Ads Manager, the sale count and value from their payment provider.
A "sale" is a cash payment (revenue_type = 'cash'), never a lead. The cash
kinds carry window_days, attributed_cash_outcomes, cost_per_cash_outcome,
profile_median_cost_per_cash_outcome, cost_delta_pct (negative = cheaper
than the median), spend and currency. ad_name is on the row — render it,
never target.ad_id.
rationale.text is OPTIONAL commentary written by the arbiter, not the
argument. Compose the sentence a reader sees from rationale.evidence.
expected_impact_dollars is arithmetic on that ad's own observed unit
economics over the next 7 days, under the stated assumption that extra budget
performs like the window did. Phrase it with "if", never "expected",
"forecast" or "at risk".
Expect the list to be short. The three-sale floor is a minimum-evidence rule, not a tuned threshold, and the binding constraint on how many recommendations exist is cash-attribution coverage, not the criterion.
Applying one
POST /api/v1/recommendations/{id}/applyExecutes through the meta-actions layer: one write spine, one audit trail
(meta_action_log), one rollback system. There is no queue and nothing to
poll — the response says what happened.
`Idempotency-Key` is required
1–256 characters. Leg-level dedup is automatic (the executor derives its keys
from the recommendation id), so retrying an already-applied recommendation
answers 200 with replayed: true rather than double-spending a budget.
GET /api/v1/recommendations/{id}/diagnose answers why did this
recommendation do what it did — the read to make before explaining an applied
change to a client, and the one to make when it did not do what you expected.
POST /api/v1/recommendations/{id}/dismiss closes one without applying it.
When a human should approve first
require_approval: true on the apply mints an approve
hand-off carrying the exact preview, and answers
202 with {status: "awaiting_approval", handoff: {id, url, expires_at}}
having written nothing. The human opens the URL signed out, sees what
changes and on which account, and presses Approve or Reject; your agent polls
GET /api/v1/handoffs/{id} for the same result payload the direct call
returns.
Approval is OR'd, never AND'd
It is required when the principal is not a workspace owner/admin, or
whenever any caller passes require_approval: true. The second is the agency
case: an owner/admin token deliberately routing every Meta write on a client's
account past the client. Rejecting or expiring leaves no partial write, and the
pending action's own row is the audit record — status plus
result.outcome ∈ approved | rejected | expired | failed.
Over MCP, apply_recommendation's confirm mints the same object under the
same rule.
Until then, the MCP tool's preview → dry_run → confirm ceremony plus the
workspace-level mcp_writeback_enabled gate is the safety boundary — see
Write-back.
| Method | Path | What it does |
|---|---|---|
GET | /api/v1/recommendations | List open recommendations for this key's profile |
POST | /api/v1/recommendations/{id}/apply | Apply a recommendation (inline, terminal response) |
GET | /api/v1/recommendations/{id}/diagnose | Why did this recommendation do what it did |
POST | /api/v1/recommendations/{id}/dismiss | Dismiss a recommendation |
Generated from openapi.json. The full request and response schema for every operation is in the OpenAPI document.