Raw platform requests
Tempio models the entities every platform has in common — campaigns, ad groups, ads, creatives, keywords, media, account defaults — and normalises them so one loop works everywhere. Platforms are larger than that model. Meta adds a field, Google exposes a resource Tempio never mapped, and the thing you need is one endpoint away from something we already read.
The escape hatch is that endpoint. It reaches the platform's own API on a connection you have already made, through the platform's own client, and it is off until a workspace owner turns it on.
It covers six platforms: Meta, Google, TikTok, Reddit, X and Microsoft Advertising. On Meta, Google and TikTok it reaches past what Tempio models. On Reddit, X and Microsoft Advertising it is how you work with the account at all, because those connections are connect only.
What it is for
Reads first, because reads are most of it: a Meta field added last month, a Google resource with no mirror, a TikTok report shape Tempio does not build. A read through the hatch executes immediately and hands back the platform's own response body.
Writes are the other half, and they go through the same gate as every other change Tempio makes: a raw write is a change request before it is a change.
Connect-only connections
A Reddit, X or Microsoft Advertising connection is connect only. Tempio holds
the connection, keeps its token in the vault and lists its ad accounts, and
reads no campaigns and no daily numbers from it. list_connections shows it
with capabilities.coverage: "connect_only" and an empty operations list,
and tempio_help marks it the same way. get_account_brief answers each of
its ad accounts with "Tempio reads no daily numbers from Reddit." (or X, or
Microsoft Advertising) in place of figures. Nothing Tempio models can be
drafted on it: no budget change, no status change, no new campaign.
The escape hatch is what does work on it. Read with platform_request.get,
which executes the read and hands back the platform's own answer. Change
something with platform_request.mutate and a sentence saying what the request
changes and why; the change waits at the gate for a person. The switches,
refusals, limits and audit rows on this page apply to a connect-only connection
exactly as they apply to a Meta one.
Every tool result that carries workspace warnings, a raw read included, carries
connect_only_connection for each connect-only connection, in place of the
operation_unsupported a connection with an empty operations list would
otherwise get:
| Code | Class | Severity | What it says |
|---|---|---|---|
connect_only_connection | write_gate_closed | info | This connection lists ad accounts and carries raw requests only. Tempio reads no campaigns or daily numbers from it; use platform_request to read or change anything on it. |
Turning it on
Settings → Raw platform requests (/settings/escape-hatch). Two switches,
and both start off:
| Switch | What it allows |
|---|---|
| Raw reads | Reads. An agent holding a key for this workspace can call a read endpoint on any connected platform. |
| Raw writes | Mutations. A write endpoint may be drafted; nothing is sent until the draft is approved. |
Turning raw reads on, or either switch off, takes effect on the press. Turning raw writes on asks first. The box, Turn on raw writes?, says an agent may then draft writes to a platform's own API and that each one waits for a person to approve it, and the switch stays where it was until you press Turn on.
Owner only. An admin can write approval rules, mint keys and approve drafts, and cannot touch this one — it is the single setting that reaches past Tempio's own model, so it belongs to the workspace rather than to its administration. An admin sees the switches disabled with the reason in a row under them.
Turning raw reads off turns raw writes off with them, and the raw writes switch stays disabled until reads are back on. "Reads off, writes on" is not a state anyone means.
How a request is written
Both halves take the same five things: the connection, a method, the platform's own path, an optional query and body, and a description.
{
"entity": "platform_request",
"action": "get",
"params": {
"connectionId": "…",
"method": "GET",
"path": "/v26.0/act_123456/adsets",
"query": { "fields": "name,optimization_goal" }
}
}The path is the platform's own path and nothing else. It starts with /,
names no host and no scheme, and cannot climb above the platform's API base.
The base comes from the same constant the adapter's client uses, so it is never
an argument and can never be pointed somewhere else. A path that resolves off
the platform's host is refused before anything is sent.
Each platform's paths look like these. The Reddit, X and Microsoft rows show the shape of that platform's paths, not a promise that the endpoint exists; the platform's own API reference is the list of what does.
| Platform | Path |
|---|---|
| Meta | /v26.0/act_123456/adsets |
/customers/1234567890/googleAds:search | |
| TikTok | /campaign/get/ |
/ad_accounts/{id}/campaigns | |
| X | /accounts/{id}/campaigns |
| Microsoft Advertising | /CampaignManagement/v13/Campaigns/QueryByAccountId |
Microsoft Advertising runs one host per service, so its path starts with the
service: /CampaignManagement/, /CustomerManagement/, /Reporting/,
/AdInsight/ or /Bulk/. The service picks the host, and a path under none of
the five is refused. The five services, their hosts and the read rule below
rest on the spec's "Microsoft reads" and "Microsoft REST API" vendor-fact rows
until the local session checks them on a real Microsoft Advertising account.
Query parameters are strings, exactly as they go on the wire. A parameter that
carries a credential — access_token, developer_token, client_secret — is
refused outright: your platform token travels in a header, never in a URL.
The same goes for the credentials and signing parameters the client adds
itself, in the query and the body alike: any key starting with oauth_, and
developertoken, authenticationtoken and client_id. Keys are matched
exactly and without regard to case, so pageToken and page_token, the
platforms' own paging cursors, pass.
customerid and customeraccountid are refused in the query only. The client
sets both as headers from the connection. A body may still name a customer or
an account, because some platforms' read operations take CustomerId in the
body, and the path and the connection's grant decide what a request reaches
either way.
Two further refusals apply to the query and the body alike. A key that
overrides the HTTP method (method, _method, X-HTTP-Method-Override) is
refused everywhere, because the method and the endpoint are what decide read or
change and a request that rewrites its own method would decide for itself. And
a GET carries no body: send the fields as query parameters, or send the method
that carries one.
Read or write is decided by the endpoint
Not by the HTTP method. Google's two reads (googleAds:search and
searchStream) are POSTs, and Microsoft Advertising reads with POSTs too, so a
classifier that trusted the verb would stage a read as a draft and hand you
nothing back. Each platform has its own table of read endpoints:
| Platform | Reads |
|---|---|
| Meta | GET on a Graph node or edge under a version segment — /v26.0/act_123/insights |
googleAds:search, googleAds:searchStream and the :generate* planning methods, and GET on any resource | |
| TikTok | a path whose LAST segment is get or list — /campaign/get/, /advertiser/list/ — plus the two asynchronous report reads, /report/task/check/ and /report/task/download/ |
any GET | |
| X | any GET. The analytics job endpoints are POSTs that create a job, so they are writes, as TikTok's /report/task/create/ is |
| Microsoft Advertising | a POST or GET whose last path segment starts with Get, Query or Search, such as /CampaignManagement/v13/Campaigns/QueryByAccountId. A DELETE or PUT to one of those paths is a write |
The Reddit, X and Microsoft Advertising rows carry the same caveat as their paths above: they describe the shape of each platform's API, not a checked list of its endpoints.
Anything the table does not match is treated as a write. That is the safe direction to be wrong in: a misclassified read costs you an approval, and a misclassified write would be an unreviewed change on a live ad account.
Ask for a write endpoint through platform_request.get and it is refused, by
name, and told to use platform_request.mutate.
Size and time
Every raw request, on all six platforms, is held to the same limits:
| Limit | Value | What happens past it |
|---|---|---|
| Path | 2,000 characters | Refused as invalid_payload |
| Query | 50 parameters, and the whole URL at most 8 KiB | Refused as invalid_payload, naming the limit |
| Request body | 256 KiB, measured as the JSON that is sent | Refused before anything is sent, and a write with a larger body is refused when it is drafted: "The body is 300 KiB; a raw request carries at most 256 KiB." |
| A read's answer | 1 MiB | The body is replaced by { "truncated": true, "responseBytes": … } with the escape_hatch_response_truncated warning |
| A write's stored response | 64 KiB | The stored response is replaced by its size, as below |
| Time | 30 seconds per call | A read fails as network; an applied write fails and is not sent again |
A body of exactly 1 MiB comes back whole. When an answer is too large, ask for fewer fields or a smaller page.
The 30 seconds cover the whole call: a platform that sends its headers and then stalls on the body is stopped at the same deadline as one that never answers.
Redirects are never followed
A raw request carries your platform credential in a header, and a redirect names an address you did not choose. So the hatch never follows one. A 3xx answer is refused; on Meta the sentence is "Meta answered with a redirect to another address. A raw request never follows one, because it would carry your credential there." Nothing is sent to the address the redirect named.
Credentials are removed from every answer
Platforms echo what they were sent. A debug endpoint can repeat the Authorization header, an OAuth endpoint answers with tokens, and an error message can quote the credential it rejected. Before a raw answer leaves Tempio, the hatch removes:
- the value of any key named
access_token,refresh_token,id_token,token,client_secret,developer_token,developertoken,oauth_token,oauth_token_secret,authorization,password,api_keyorapikey, matched exactly and without regard to case; - inside every string, the connection's own tokens and Tempio's app secrets for that platform, wherever they appear;
- anything shaped like
Bearerfollowed by twenty or more token characters.
Each is replaced by [redacted]. A body that is not JSON is treated as one
string. Paging cursors are not on the list and come through untouched.
This happens inside the escape hatch before anything else sees the answer: a
read's body, a write's response before it is stored on the draft, and the
platform's own words in an error. An agent's result, the stored response, a
failed draft's reason and the sentence you are refused with all carry the
redacted text. When anything was removed, the answer carries
escape_hatch_redacted, and the audit row records how many values were
replaced.
What a raw request can reach
A raw request carries the connection's grant, so it reaches whatever that grant can reach on the platform, not only the account the connection names. When one consent picked two accounts, one grant backs both connections, and a raw read on the first can name the second account in its path. The same holds on every platform, Meta included.
Restricted connections decide who may use a
connection. They do not narrow what its grant reaches. Tempio does not pin a raw
request to the connection's account: that would mean recognising an account id
in every path shape each platform uses (/ad_accounts/, /accounts/, a
CustomerAccountId in a Microsoft body), and a pin that missed one shape would
read as a guarantee it is not. Microsoft Advertising's client sets the
CustomerAccountId header from the connection, which scopes Microsoft's
account-level services; it is not a pin either. That header rests on the
spec's "Microsoft headers" row until the same check.
To keep a person away from one of two accounts, connect the two under separate logins. Each connection's grant then reaches its own account, and restricting that person from one connection keeps them off its account.
The raw budget
Each connection may send 30 raw requests a minute, reads and applied writes together. The budget is taken before anything is sent, so running out costs the platform nothing.
- A read past the budget is refused as
rate_limited, and the refusal says how many seconds to wait. Another connection in the same workspace has its own budget and still reads. - An approved write takes its call from the budget before the draft is marked submitted. When the budget is empty, the apply waits for the minute to turn and runs again; this is the one failure of a raw write that is retried, because nothing was sent. If the budget is still empty after every retry, the draft fails with "Nothing was sent: this connection's raw request budget stayed empty through every retry." A write whose apply stopped for any other reason before it was sent fails with "Nothing was sent: the change never reached the step that sends it."
The platform's own rate limits still apply on top. A platform that answers
with its own limit is reported as rate_limited.
What a read's answer carries
A read comes back in the same envelope as every other tool result, with the
platform's body as data and these warnings:
| Code | Severity | When | What it says |
|---|---|---|---|
escape_hatch_unmodelled | info | Every raw read | This is the platform's own answer. Tempio has not normalised it: money is in the platform's units rather than minor units, ratios are the platform's own, and none of it is in Tempio's reports. |
escape_hatch_response_truncated | degraded | The answer was larger than 1 MiB | The platform's answer was larger than 1 MiB and is not included. Ask for fewer fields or a smaller page. |
escape_hatch_redacted | info | Something was removed from the answer, or from a write's stored response | Values shaped like credentials were removed from the platform's answer before it reached you. |
An applied write carries escape_hatch_unverified, below, and
escape_hatch_redacted when its stored response had anything removed.
Why a write becomes a change request
Because it is a change, and every change Tempio makes is a change request first. A raw write lands in the same queue, at the same page, with the same audit trail as a budget change.
Two things are true of it that are not true of anything else in that queue:
- The description is mandatory, and has to read as a sentence. A reviewer
opening the draft sees your words and the request line;
POST /v26.0/act_x/adsetsdescribes nothing, so it is refused as a description. - No rule can approve it. An approval rule is written against a shape — an operation, a cap, a direction — and a raw request has none of those. It waits for a person, always, whatever rules your workspace has written.
Everything else about the gate holds. An approval chain that names
platform_api_request, or names no operation, holds a raw change like any
other, and with separation of duties on, the person who drafted it cannot
approve it.
What Tempio does not verify
Every other change Tempio applies is read back from the platform afterwards and checked against what was asked for. A raw request cannot be: there is no canonical row to read back and no marker to stamp, because Tempio does not model the endpoint.
So an applied raw write carries a warning that says exactly that — the platform's own response is the only record of what happened — and the same sentence is shown at the gate before you approve, not after. The platform's response is stored on the draft, so you can read what it said; a response over 64 KiB is replaced by its own size, because a change request is a row somebody opens in a browser and not a copy of your account.
Once the write lands, the connection re-syncs. A raw write can change an entity Tempio mirrors, and a slow mirror is better than a silently wrong one. On a connect-only connection the re-sync is the accounts step, so a raw change that renamed or closed an account shows in the account list.
What is on the record
Every call — read, write, and every refusal — writes an audit row carrying who made it, which credential kind they used, the connection, the method, the host, the path, the description, the platform's own request id, whether it succeeded, how large the response was, and how many credential-shaped values were removed from it.
A refused call is on the record too: the row carries the method, the path as you wrote it, and the sentence you were refused with, so a workspace can read what an agent tried as well as what it did.
The response body is never written to the audit log. It can carry customer data, and an audit log is not a data store. The size is; the bytes are not.
What it cannot do
- Reach a host that is not the platform's.
- Run on a connection outside your workspace.
- Run on a connection whose kill switch is closed — a paused or disconnected connection refuses a raw request exactly as it refuses an apply.
- Run on a platform this deployment has not enabled.
- Return a token, a grant, or a header Tempio sent. You get the platform's body, with any credential it echoed removed, and nothing else.
- Follow a redirect, or run longer than 30 seconds.
- Skip the gate for a write.
Related
- Approvals — the rules that can clear a change, the chains that hold one, and why neither clears this one.
- MCP reference — the tools, the scopes and the keys. A raw read
needs a
readkey; drafting a raw write needs adraftkey.