Deploying Recipe Templates via the API
Overview
A recipe template is a blueprint: the transformation steps of a working recipe, plus the set of extractions those steps read from. Deploying it into a workspace re-creates both — the extracts for the accounts you name, and the transformation on top of them.
You can do that from the interface, one workspace at a time, or through the Embedded API, which is what this guide covers. It is the path to take when you are deploying the same template into many workspaces, or when your own product creates the workspaces and connects the accounts.
The full request and response reference lives in the API documentation. This page is the sequence those calls go in, the rules that decide whether a deployment succeeds, and the check that tells you the data is really there.
Objects and Identifiers
Four objects carry similar names. Keeping them apart makes the rest of this page easier:
| Object | What it is | Through the API |
|---|---|---|
| Recipe template | A blueprint: a snapshot of a recipe's transformation steps plus the extracts it reads. | Read-only |
| Automated recipe | One deployment of a template inside one workspace. | Create, read, update, build |
| Extract template | A different object: the definition of a report available from a data source. | Separate endpoints |
| Extract | One running extraction — one report, one account, one workspace. | Read; created for you when a recipe builds |
Two identifiers appear side by side and are easy to mix up. template_id is an integer.
recipe_id is a long numeric string, and it stays a string — do not parse it
into a number.
Authentication
The base URL is https://embedded.improvado.io. Every call below is workspace-scoped, so it needs
a bearer token: exchange the Embedded API credentials Improvado issued you for a token scoped to
one workspace, as described in
Authorization.
Three properties of that token shape how you script a deployment across an estate:
- A token belongs to one workspace. It carries the workspace identity itself, so no workspace header is needed alongside it — and deploying into a hundred workspaces means holding a hundred tokens.
-
The 30-minute lifetime slides. Every call renews it, so re-mint when a call
returns
401rather than on a timer. - Minting a token provisions a machine identity with Editor rights in that workspace. That is expected — worth knowing before you script it, since the identity is created once per workspace and stays there.
Every endpoint in the sequence below needs a trailing slash. Without one the API
answers 301, and most HTTP clients follow a 301 by downgrading the POST to a
GET and dropping the request body — so the call looks like it worked at the transport layer, and
then fails when you parse an HTML error page as JSON.
Turn redirect-following off for this API and treat any 301 as a bug in your URL — for example
requests.post(url, json=body, headers=h, allow_redirects=False) in Python.
The Deployment Sequence
Five calls, in this order.
Step 1. Read the Template
GET /api/v3/automated-recipe-templates/
GET /api/v3/automated-recipe-templates/{template_id}/
Templates are agency-level. Every workspace in your agency sees the same set, so
a template authored once is immediately deployable everywhere — there is no copying step and no
per-workspace registration. A template_id means the same thing in every one of your workspaces;
resolve it once and pin it.
From the detail response, take data_sources[].name — the machine name, such as tiktok_ads —
not title. The configuration call in step 4 validates against name.
Step 2. Discover Connections and Accounts
GET /api/v3/datasources/{data_source}/connections/
GET /api/v3/connections/{connection_id}/accounts/
Each account carries both an id and an account_id.
id is the internal identifier and it is the one the configuration call needs.
account_id is the external identifier from the ad platform, such as act_123456789.
The same ad account has a different internal id in every workspace, and the same connector has a
different connection_id. There is no agency-wide account list to reuse. Your stable join key is
the external account_id — run discovery in each workspace and map through it.
Step 3. Create the Recipe
POST /api/v3/automated-recipes/
{"template_id": 1234, "title": "Any display name"}
The response gives you the recipe_id you will use for the rest of the sequence, along with a
datatable_id. The state on this response is the initial default — nothing has been configured
and nothing is running yet.
One recipe per template per workspace. A second create for the same template
returns 400 with the identifiers of what already exists, in an existing_recipe_ids list. Treat
that as "already deployed" rather than as a failure. The list can hold more than one entry; prefer
one that is not in a failed state, otherwise the most recently updated.
The output table name comes from the template, not from your title. Two deployments of the same template under different titles produce the same table name. Do not derive the table name from anything you control — read it back instead (see below).
Step 4. Configure Data Sources, Connections and Accounts
PUT /api/v3/automated-recipes/{recipe_id}/
{
"data_sources": ["facebook", "tiktok_ads"],
"connections": [
{
"connection_id": 50001,
"connection_data_source": "facebook",
"accounts": [{"id": 900001}, {"id": 900002}]
},
{
"connection_id": 50002,
"connection_data_source": "tiktok_ads",
"accounts": [{"id": 900003}, {"id": 900004}]
}
],
"sync_from_date": "2026-08-01"
}
| Field | Required | Rule |
|---|---|---|
data_sources |
Yes | Non-empty. Every name must appear in the template's own data_sources. |
connections |
Yes | Non-empty. |
connections[].connection_data_source |
Yes | Must also appear in data_sources. |
connections[].accounts |
Yes |
Non-empty list of objects, each with an id. Bare identifiers are
rejected.
|
sync_from_date |
No |
Date as YYYY-MM-DD. Omit the key entirely if you are not using it — do not send null.
|
A successful PUT returns 200 with an empty body — no echo of what was stored.
Follow it with a GET and compare against what you sent.
This call records configuration only. It provisions nothing and starts nothing. Skipping it is terminal: a build with no configuration behind it fails, and the error message mentions neither accounts nor connections.
Step 5. Build
POST /api/v3/automated-recipes/{recipe_id}/build/
This is the only call that provisions extracts. For every memorized extract of the template and every connection you configured, it creates the extractions for the whole account list, and they start immediately.
Extracts are de-duplicated within a workspace: if the same report already exists there for the same account, it is reused and reconfigured rather than duplicated — but a reused extract is not restarted. Nothing is shared across workspaces, so extraction cost scales with the number of workspaces you deploy into.
Calling build while a build is already running returns 400.
Configuration Is Append-Only
The first PUT is the only one that starts from a blank slate. After it, you can add data
sources, connections and accounts — you cannot remove any of them.
Always send the complete set, never a delta. A connection left out of the payload
reads as a removal and returns 400.
An id that does not exist is accepted with 200 and then fails the build — and that failure
takes down every data source in the recipe, not just the one carrying the bad identifier. Because
configuration is append-only and there is no delete on this API, the bad identifier cannot be
taken out afterwards, and the recipe keeps its template slot and its output table name in that
workspace.
The preventive rule is simple: run discovery immediately before the PUT and send only
identifiers you just read back.
One more thing to know about repeat configuration: a PUT clears the recipe's previous failure
diagnostics. A recipe sitting in a failed state with a useful error message reads as freshly
created immediately afterwards, with nothing running. Capture state and error with a GET
before each PUT.
Find the Output Table
Read the table name — do not compute it.
GET /api/v3/automated-recipes/{recipe_id}/
GET /api/v3/data-tables/{datatable_id}/
The recipe's detail response carries a datatable_id. The data-table response resolves it into
sql_name — the object you query — and datasource_name, the database it lives in.
datatable_id is only on the detail response. The list endpoint
GET /api/v3/automated-recipes/ does not carry it, nor
connections or data_sources; conversely the list carries workspace_id and template_title,
which the detail response does not. Neither is a superset of the other, so building a fleet-wide
table inventory means one list call per workspace plus one detail call per recipe.
A datatable_id is assigned at create time and resolves to a plausible sql_name before the
table exists. freshness is what tells you a table was actually produced: it stays null until the
first successful build. It is not a health signal — a recipe that built once and failed a later
rebuild keeps its freshness while its state reads as failed.
Confirm the Data Landed
A recipe reaching its ready state means the output table was produced. It does not, on its own, mean every account you configured is in it.
Readiness is evaluated on the output table rather than per extraction. Once the first account's rows land, the table counts as fresh and the build proceeds. Accounts whose extractions are still running at that moment are not waited for, and they are not retried afterwards. The busier a workspace already is, the more likely this is to happen — which means the same deployment can be complete in one workspace and short a few accounts in another, with no error raised anywhere.
So gate on the extracts, and then on the data:
-
Poll
GET /api/v3/automated-recipes/{recipe_id}/until the recipe reaches its ready state. -
Call
GET /api/v3/extracts/and require every extract belonging to your recipe's connections to report a synced status. An extract still syncing shows its last sync as in progress. - Confirm account coverage in the table itself.
SELECT __account_id, count()
FROM <datasource_name>.<sql_name>
GROUP BY __account_id
Compare that result against the account list you configured. Only this third check is conclusive. A non-zero total row count hides a missing account — the recipe's SQL has no account predicate, so it returns whatever is present in that workspace for those reports, including rows from accounts you never configured.
A recipe moves from extracting data, through building dependencies, to either a ready or a failed state. Two things to know before you build logic on it:
-
State matching is exact, including case. Filtering the list endpoint with a
statevalue that does not match character for character returns200with zero results rather than an error — so a fleet health check written with the wrong casing reports a perfectly clean estate of nothing. Read the value back from a real recipe and compare against that instead of hardcoding a string. -
The initial state is not evidence of progress. It is the default at create
time, it is re-applied by every configuration
PUT, and it is re-applied when a build is dispatched. Those three situations are indistinguishable from the field alone.
last_build_at does not move when a build starts, so measure any timeout from your own POST to
the build endpoint. And error is often null on a failed recipe — the absence of an error message
is not evidence that nothing went wrong.
What the API Cannot Do
| Operation | Availability | What it means for you |
|---|---|---|
| Create or edit a recipe template | Interface only | Authoring cannot be automated. One browser session per template; every deployment afterwards is scriptable. |
| Delete a recipe | Not available | A failed deployment is permanent and holds its template slot and table name in that workspace. |
| Remove an account, connection or data source | Not available | When an end user disconnects an ad account, the recipe cannot be reduced to match. Contact support. |
| Re-sync a deployed recipe after its template changed | Not available | A recipe is a snapshot and never picks up later template changes. |
| Per-extraction status scoped to one recipe | Not available | Use GET /api/v3/extracts/ and correlate by connection. |
Template content is immutable by design. Only the title and description can change. Any change to the transformation logic is a new template, and recipes already deployed from the old one keep running the old logic.
Preparing a Template That Deploys Cleanly
Templates are authored in the interface — see Custom Recipe Templates — but two of the constraints decide whether the API path works later.
A template is only as portable as its sources. When a template is created, the source recipe's dependencies are resolved by the shape of the tables it reads: extracts and other recipes are captured into the template, and anything else — a manually uploaded table, an external view, a hand-written table reference — is dropped. The template is still created, the copied SQL still names that table, and the table does not exist in any other workspace. Before promoting a recipe, confirm every source it reads is an extract or another recipe.
A recipe created through the API cannot be promoted to a template by a person. It is owned by the machine identity that created it, and promoting requires edit rights on the source recipe. The "deploy by API, refine by hand, promote back" loop does not close — author templates from recipes built in the interface.
Because there is no delete or archive for templates and their content is immutable, a title is permanent and it determines the output table name of every recipe deployed from it. A new version of a report is a new template, so the catalogue only grows. Plan for that with a naming convention — one template per data source, report and version, with the version in the title — and keep your own registry mapping your product's concepts to template identifiers.
Running Across Many Workspaces
- One token per workspace, 30 minutes, sliding. Plan minting and rotation.
-
Discovery runs per workspace. Connection and account identifiers are
per-workspace; only the external
account_idis stable. Cache the mapping keyed by workspace. - Table inventory costs one list call per workspace plus one detail call per recipe, because neither response is a superset of the other.
- Extraction cost scales per workspace — nothing is reused across them.
- Template identifiers are shared across your agency. Validate them against the template list before each deployment cycle and treat a "template not found" response as a signal to re-resolve rather than as a transient error.
- Output table names repeat. Uniqueness is enforced per workspace, not agency-wide, so the same template yields the same table name everywhere. That is expected — do not try to make them unique across workspaces.
A health check for a fleet, per workspace:
- List the automated recipes in the workspace.
- For each one, read its detail: state, error,
datatable_id, configured connections. - Require every extract on those connections to report a synced status.
- Require a non-null
freshnesson the data table. - Query the table grouped by account and compare against the configured account list.
Flag as unhealthy any recipe in a failed state, any recipe whose deadline has passed measured from your own build call, and any recipe whose account coverage is short — even when its state reads as ready. That last class is the one that otherwise goes unnoticed.
There are no published rate limits on this API. If you are fanning out across hundreds of workspaces, agree a concurrency ceiling with your Improvado contact rather than discovering one.
Was this article helpful?
Thanks for the feedback!