Run an operation in the user's browser
OpenAPI JSONimport os, requests
r = requests.post( "https://api.eu.apigami.com/call", headers={ "Authorization": f"Bearer {os.environ['APIGAMI_API_KEY']}", "Accept": "application/octet-stream", "Accept-Encoding": "gzip", }, json={ "user_ref": "usr_3f9c1a0e7b2d4c58a61e9f0b2d7c4e13", "op": "request.fetch", "params": {"domain": "example.com", "url": "https://api.example.com/me", "method": "GET", "responseType": "binary"}, },)if not r.ok: raise RuntimeError(r.json()["error"])print(int(r.headers["X-Apigami-Status"]), r.text)# Requests decodes gzip automatically. Use r.content for file bytes.Runs one operation in a browser of the user named by user_ref and returns its result. Apigami takes the browser install_ref names, or else the user’s most recently active browser that can reach the site, and keeps it for the whole call. An action calling with its run token names neither: the run’s browser runs it. For request.fetch, request gzip by default with all three settings: outer Accept: application/octet-stream, outer Accept-Encoding: gzip, and params.responseType: “binary”. This also applies to text or JSON from the site. These HTTP headers do not belong in params.headers. Read the raw response body, X-Apigami-Status for the target status and X-Apigami-Headers for its JSON-encoded headers. Enable gzip decoding in your HTTP client, such as curl –compressed; Node fetch and Python Requests decode automatically. Gzip requires install 0.4.0+; small bodies, already compressed types and older installs may return uncompressed bytes. The default JSON envelope and other operations do not use this compression. The first call for a domain the user was never asked about opens the approval window in their browser and waits up to 45 s for the answer: approved, the call goes through in the same request; refused, 403 no grant; unanswered, 403 with consentPending. A call runs for at most 20 s in the browser. emails.send is answered by Apigami itself, never by a browser: no domain, no approval window, and the email reaches the user whether or not a browser of theirs is online. ai.generate is answered by Apigami too, in the EU: a model reads the instructions, text, PDFs and images you send and answers in text, or in JSON following output_schema; it takes no user_ref, and a page’s token is refused.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”Example
gzipSend gzip together with Accept: application/octet-stream and params.responseType: “binary” to request compression of request.fetch responses. This header alone is insufficient. Enable automatic gzip decoding in your HTTP client.
Request Bodyrequired
Section titled “Request Bodyrequired”object
With an application key, required, except for ai.generate, which refuses it, and emails.send to the team: the user whose browser runs the call, as POST /connect/token returned it. Under a run token (an action calling POST /call with the token Apigami handed it in X-Apigami-Run), leave it out: the run already names its browser, and naming one answers 400.
With an application key, optional: one browser of that user, as POST /connect/token or GET /v1/users/{user_ref}/installs returned it. Named, that browser is never replaced by another; left out, Apigami takes the most recently active browser of the user that can reach the site. Under a run token, leave it out: the run already names its browser, and naming one answers 400.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
Https:// only, on the domain or one of its subdomains.
Cookie, Origin, Referer, User-Agent and Host are set by the browser and cannot be overridden.
object
Map header names to cookie names. Values are read and attached inside the browser without being returned to your app.
object
Sent as-is. Encode JSON yourself and set content-type.
How Apigami rebuilds the bytes of body, not the format the target expects: the target only ever receives bytes and never sees base64, and what it expects is what content-type says. text sends the UTF-8 of the string; base64 decodes it to bytes first, which is how you PUT a PDF, a DOCX or an image to an upload url. A target that wants JSON carrying a base64 field takes that JSON as a plain text body instead. Requires install 0.8.0 or later: an older one answers 409 rather than send the base64 itself.
Use binary with outer Accept: application/octet-stream and Accept-Encoding: gzip to request raw responses with compression, even for text or JSON. text decodes UTF-8; base64 returns a base64 body in the JSON envelope. binary without the raw Accept header also returns a base64 envelope. Do not set compress: Apigami decides from the install version.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
An https url on the domain or a subdomain of it.
Bring the tab to the front. Default false: it opens in the background.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
From tabs.list. Omitted: the first open tab on the domain.
An https url on the domain to go to. Omitted: the tab is reloaded.
Leave a page with unsaved changes. Its beforeunload is dismissed by default, which means staying, so without this the navigation quietly does not happen. Accepts exactly one dialog, for this navigation only. Apigami Dev Assistant build only: the dialog is answered through the debugger, and the capture is attached to the tab for it.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
From tabs.list. Omitted: the first open tab on the domain.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
From tabs.list. Omitted: the first open tab on the domain.
CSS selector of the part to read. A ref returned by an outline works here, date and all.
Outline is the map of the page, one line per node, each ref reusable as a selector. Open dialogs and popups come first, under [open overlays], the rest under [page].
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
From tabs.list. Omitted: the first open tab on the domain.
Read this one key whole instead of listing the keys.
Omitted: both areas, when listing.
Click an element, as a real Chrome input event. Apigami Dev Assistant build only: a standard install answers gestures require the Apigami Dev Assistant build, which is what keeps a gesture out of a deployed application. Two refusals: a button whose accessible name says it pays, orders, subscribes or closes an account, which is a speed bump rather than a barrier; and any target inside a payment form, decided by Chrome own kCompleteCreditCardForm rule, which is scoped to that form and leaves the rest of the page clickable, readable and photographable. A third refusal is about the site rather than the page: the browser asks Apigami what it thinks of the host in the tab before acting, and a host rated block — online banking, mainly — refuses clicks and keystrokes for every application while reading and screenshots keep working. Unreachable and with no recent answer held, the gesture is refused rather than allowed.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
From tabs.list. Omitted: the first open tab on the domain.
CSS selector. A ref returned by a tabs.read outline works here. Must match exactly one element: a selector matching several is refused rather than resolved to the first.
2 for a double click, 3 for a triple.
Type into a field. Apigami Dev Assistant build only. Never types into a password, a one-time code, a CVV, a card number or an IBAN, nor anywhere inside a payment form as Chrome own kCompleteCreditCardForm rule defines it: the refusal happens in the page, before a character is dispatched, and no parameter turns it off. A third refusal is about the site rather than the page: the browser asks Apigami what it thinks of the host in the tab before acting, and a host rated block — online banking, mainly — refuses clicks and keystrokes for every application while reading and screenshots keep working. Unreachable and with no recent answer held, the gesture is refused rather than allowed.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
From tabs.list. Omitted: the first open tab on the domain.
CSS selector of the field. A ref returned by a tabs.read outline works here. Must match exactly one element.
Inserted in one go, which fires input but not one keydown per character.
Select what is already in the field first, so the text replaces it.
Send a real Enter afterwards, for a site that reacts to the key rather than to the text.
An image of the tab in front. Apigami Dev Assistant build only. Chrome draws that tab and no other, so a tab sitting behind is refused after 2.5 s rather than waited on, unless foreground is true (install 0.14.0), which brings it forward for the capture and puts the user back on the tab they were on. tabs.read has no such limit and reads a background tab as outline, markdown, text or html; read the page with it first anyway, since it describes the same page in a fraction of the size and gives refs you can act on.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
From tabs.list. Omitted: the first open tab on the domain.
Clip to this element. Omitted: the visible viewport. A clip over three viewport heights is cut to three.
Jpeg only. Lowered automatically rather than returning an image too large to read.
Longest width in CSS pixels, never device pixels: a high-density screen costs nothing extra.
Bring the tab forward for the length of the capture, then put back the tab the user was on. The only way to photograph a tab that is not in front, since Chrome paints no other. Install 0.14.0 or later, 409 below it.
Move a page, or one scrollable panel inside it, and say whether there is anything left. Apigami Dev Assistant build only, install 0.11.0 or later. A real page stacks several scrollable boxes and only the page knows which one moves, so selector names something INSIDE the one that must move; the browser walks up to whichever ancestor scrolls AND can still move in the direction asked for, skipping one that is already at its limit. Refused on a site rated block, like every gesture. Read scrollable, atStart and atEnd before calling again: scrollable: false means you named something that does not scroll, not that you have seen everything.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
From tabs.list. Omitted: the first open tab on the domain.
Something inside the panel to move, a ref from a tabs.read outline. Omitted: the window.
CSS pixels, capped at three screens of the container per call.
Give a state to a control: one option of a select with value or label, or a checkbox or radio with checked. One of the three is required. Apigami Dev Assistant build only, install 0.12.0 or later. Separate from tabs.click because a click on a select opens the operating system own menu, which the debugger cannot see, walk or close. An option is set through the native value setter then input and change are dispatched; a checkbox is read first and clicked only if it is not already in the state asked for, so a replayed call answers changed false instead of undoing itself. A payment form refuses this like every other gesture — an expiry month is a select in exactly that form.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
From tabs.list. Omitted: the first open tab on the domain.
The control itself: a ref from an outline, a select, a checkbox or a radio.
The value of the option to choose, matched exactly.
The visible text of the option, exactly then ignoring case and surrounding spaces.
For a checkbox or a radio. Idempotent.
Send one key. Apigami Dev Assistant build only, install 0.12.0 or later. The list of names is closed, because a virtual key code is needed per key and an approximate one does nothing silently rather than failing. A printable character requires a selector: without one it would land in whatever holds the focus, which is what the credential refusal exists to prevent. Browser shortcuts (Ctrl+F, Ctrl+T, Ctrl+W, Ctrl+Tab) are handled by Chrome itself and never reach the page.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
From tabs.list. Omitted: the first open tab on the domain.
Where the key goes, focused by a real click first. Omitted: whatever holds the focus.
Enter, Tab, Escape, Backspace, Delete, Home, End, PageUp, PageDown, an arrow, or one printable character.
Held while the key is sent, and they reach the page, never Chrome. Anything but shift makes the keystroke a command rather than a character, so it is only accepted on a latin letter, a digit or a named key.
Wait until a tab stops changing, instead of guessing a delay. Apigami Dev Assistant build only, install 0.11.0 or later. Returns once the page has gone quietMs without touching its DOM and, unless checkNetwork is false, once nothing it asked for is still in flight — read from the capture, with nothing injected into the page. A timeout is not an error: settled false with reason timeout means the page never stops, which a ticker or a live feed does.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
From tabs.list. Omitted: the first open tab on the domain.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
Return only these cookies. Omitted, the whole jar for the domain comes back.
Hold a text/event-stream open in the browser, tab closed, and relay every event to your webhook endpoints as stream.opened / stream.event / stream.closed. Stored by Apigami: survives a browser restart, ends on unsubscribe, on a withdrawn approval, or after 30 days offline. At most 10 subscriptions per app on one browser (409 past that), and a browser on 0.6.0 or later.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
object
Https:// only, on the domain or one of its subdomains. {{uuid}} becomes one fresh uuid per open.
object
Map header names to cookie names, resolved inside the browser.
object
A request the browser replays every intervalMs while the stream is open.
object
object
Https:// only, on the domain or one of its subdomains. {{uuid}} becomes one fresh uuid per open.
object
Map header names to cookie names, resolved inside the browser.
object
No byte for this long closes the stream, which reopens on its own.
Cookie names that must exist before the browser opens the stream; it waits for them otherwise.
Regular expressions in RE2 syntax (no backreferences or lookaround); an event is relayed only if one matches its data.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
The subscription, as returned by stream.subscribe.
object
A bare host name, e.g. example.com. The site the user approved; subdomains are covered.
An email to the user named by user_ref, or under a run token to the run’s person, or with to: "team" to every member of your app’s team. No field takes an address or HTML: Apigami lays out the blocks between its header (your app’s logo and name) and its footer (why the person receives it and how to stop), in light and dark, with a plain-text part. Your manifest must declare "permissions": ["emails"], except to your team.
object
transactional: about the user’s own use of your app. marketing: news, only to a user who ticked your news box, with Postmark’s one-click unsubscribe.
One line.
object
object
Https://, shown as it is.
object
object
object
object
A small image, a short title and a text: one line of a list of news or products.
object
object
Https://, shown as it is.
object
object
object
What the image shows.
object
Every member of your app’s team instead of a user: a transactional email, no user_ref, at most 50 a member a day; what a public form tells you. Refused under a page’s token and a brick’s run.
A model reads the instructions and the parts, in the EU, stores nothing and never trains on them, and answers { output, finish_reason, usage }. Your manifest must declare "permissions": ["ai"] in the version whose code calls. No user_ref; a page’s token is refused. Refusals: /docs/ai/#refusals.
object
What the model must do.
Read in order. 500 000 characters of text and 14 MB of files at most, all parts together.
object
object
object
A Word or OpenDocument file is refused with a 415: convert it to PDF or text.
Standard base64, padded. Its first bytes must match media_type.
A JSON Schema of the answer, 16 kB at most, using only type, properties, required, items, prefixItems, additionalProperties, enum (strings or numbers), anyOf, oneOf, title, description, format, minItems, maxItems, minimum, maximum, propertyOrdering, $schema, $id, $anchor, $defs, $ref: what zod and pydantic write passes. With it, output is the parsed JSON; without it, text.
object
Optional: the key already identifies the app. Must be the key’s app.
Optional, for older integrations. Same as appId.
Examples
Fetch raw bytes; send Accept: application/octet-stream and Accept-Encoding: gzip
{ "user_ref": "usr_3f9c1a0e7b2d4c58a61e9f0b2d7c4e13", "op": "request.fetch", "params": { "domain": "example.com", "url": "https://api.example.com/me", "method": "GET", "headers": { "accept": "application/json" }, "responseType": "binary" }}List the open tabs of a site
{ "user_ref": "usr_3f9c1a0e7b2d4c58a61e9f0b2d7c4e13", "op": "tabs.list", "params": { "domain": "example.com" }}The same, in one browser of the user
{ "user_ref": "usr_3f9c1a0e7b2d4c58a61e9f0b2d7c4e13", "install_ref": "ins_8b1e4f2a9c3d4e5f8a7b6c5d4e3f2a1b", "op": "tabs.list", "params": { "domain": "example.com" }}Ask a model for structured data, with no AI account
{ "op": "ai.generate", "params": { "instructions": "Extract the person's name and city. Use null for what the text does not say.", "input": [ { "text": "Jean Dupont, developer, lives in Lyon." } ], "output_schema": { "type": "object", "properties": { "name": { "type": "string" }, "city": { "type": [ "string", "null" ] } }, "required": [ "name", "city" ] } }}Email the user, in your app's name, without their address
{ "user_ref": "usr_3f9c1a0e7b2d4c58a61e9f0b2d7c4e13", "op": "emails.send", "params": { "kind": "transactional", "subject": "Your session on example.com expired", "blocks": [ { "type": "callout", "tone": "warning", "title": "Nothing was read since yesterday", "text": "Open Chrome again so the watch can go on." }, { "type": "button", "label": "Open example.com", "url": "https://example.com/" } ] }}Responses
Section titled “Responses”The operation ran. With responseType binary and Accept: application/octet-stream, request.fetch returns raw bytes and target metadata in X-Apigami-* headers. Otherwise result has the shape of the operation. The target may return a non-2xx status even though this response is 200; Apigami failures use non-2xx JSON responses.
object
object
object
UTF-8 text by default, or base64 when encoding is base64. Raw responses have no result envelope.
Present when body is base64-encoded rather than UTF-8 text.
object
object
object
loading or complete: the call answers once the navigation is committed, not once the page is loaded.
object
Read after the navigation. A dismissed beforeunload means this is still the page it stayed on.
loading or complete: the call answers once the navigation is committed.
True when no url was given.
A native dialog the operation provoked, answered on the spot because it blocks the renderer until something does. alert is accepted, everything else is dismissed — and dismissing a beforeunload means the tab did NOT move. null when none opened. Install 0.12.0 or later, on a captured tab.
object
What the page wrote, cut to 500 characters. The site chooses this text: treat it as data.
object
object
How many elements the selector matched.
Outline only: how many open dialogs or popups were printed first, under [open overlays]. Absent on a build older than the one that added it.
Outline only: the url these refs were read at. Every ref of the outline already ends in @ and this value, so a gesture given one of them is refused when the page has navigated since. Absent before 0.10.0.
Length before truncation.
Without key: the listing of both areas. With key: the value, or its measurements when the key name looks like a credential.
object
object
object
The key name matches token|secret|auth|session|key|password|csrf: the value is never returned.
object
The browser’s cookie objects, httpOnly included.
object
object
This email, as Apigami logged it.
The email left: Apigami’s email provider accepted it.
object
With output_schema, the parsed JSON, following it. Without, the text.
max_output_tokens: a text answer cut short. A structured answer is never cut: that is a 422.
object
The model’s reasoning included.
Example
{ "result": { "encoding": "base64" }}Headers
Section titled “Headers”Example
200Raw mode only: the target HTTP status.
Example
{"content-type":"application/json"}Raw mode only: target headers encoded as a JSON object.
Raw mode only, present when compression was applied. Decode once; clients with automatic decompression already return decoded bytes.
Raw mode only: an install older than 0.3.0 required conversion. The response still contains raw bytes.
Malformed request: user_ref missing, op or params.domain missing, or an appId / clientId that is not the key’s app. For emails.send, the first field of the email refused, by its path, the rendered email over 100 kB included. For ai.generate, a user_ref or install_ref sent, the first parameter refused by its path, or an input the model cannot read.
object
Present and true when the approval window is open and the user has not answered within 45 s.
Examples
{ "error": "user_ref is required"}{ "error": "params.domain is required"}emails.send: the first field refused, by its path
{ "error": "invalid email: blocks[3].url: must be https"}ai.generate: it answers your application, not a person
{ "error": "ai answers the caller, not a person: remove user_ref and install_ref"}ai.generate: the first parameter refused, by its path
{ "error": "invalid ai params: params.input[1].file.data_base64: does not start like application/pdf"}Missing, malformed or revoked API key.
object
Present and true when the approval window is open and the user has not answered within 45 s.
Example
{ "error": "bad client credentials"}Not allowed: the team has no active member, the user is not connected to this app (never connected on the connect page, or disconnected), Apigami disabled their account, they left the team of your private app, install_ref is not one of this user’s browsers, the user refused this domain, or has not answered yet (consentPending). For emails.send: the manifest declares no emails permission, sending is suspended after complaints, news to a user who did not tick your news box, the address cannot receive, or a run token that serves nobody. For ai.generate: the version whose code calls declares no ai permission, a page’s token, or a team the AI is not open to yet.
object
Present and true when the approval window is open and the user has not answered within 45 s.
Examples
{ "error": "no grant"}{ "error": "consent requested", "consentPending": true}{ "error": "client disabled"}{ "error": "not paired"}{ "error": "account disabled"}{ "error": "app is private"}{ "error": "install_ref is not a browser of this user"}{ "error": "emails not permitted for this app"}{ "error": "emails suspended for this app"}{ "error": "marketing not accepted by this user"}{ "error": "recipient unreachable"}{ "error": "ai not permitted for this app: add \"ai\" to x-apigami.permissions and deploy"}{ "error": "a page cannot call the ai: call it from an action, which the page calls with apigami.call"}{ "error": "ai is not open to this group yet"}{ "error": "this run serves nobody: to tell your own team, add \"to\": \"team\" to params; to write to a person, record the work in your database and send it with the version's key, `env.APIGAMI_KEY`, from the action of a cron, or with an API key (`apg_…`) from your own backend"}The user_ref is not one of your team’s.
object
Present and true when the approval window is open and the user has not answered within 45 s.
Example
{ "error": "unknown user_ref"}No browser of the user can take the call: none is attached, none holds the browser’s permission for the site, or several are online for a site never asked about and none is named. For ai.generate under an API key: your application has no production version yet.
object
Present and true when the approval window is open and the user has not answered within 45 s.
Examples
{ "error": "no browser is attached to this user"}{ "error": "several browsers of this user are online and none holds the permission for example.com: name one with install_ref"}{ "error": "publish a version with app_publish before an api key calls the ai: the key is held to production's manifest"}Ai.generate: more than 14 MB of files, decoded, in one call.
object
Present and true when the approval window is open and the user has not answered within 45 s.
Example
{ "error": "ai input too large: at most 14 MB of files per call"}Ai.generate: a file that is not a PDF, plain text or a PNG, JPEG or WebP image. Convert a Word or OpenDocument file to PDF or text first.
object
Present and true when the approval window is open and the user has not answered within 45 s.
Example
{ "error": "unsupported media type application/msword: send pdf, text or an image"}Ai.generate: the model ran and gave nothing usable. Its filters declined the content (do not retry as is), or a structured answer was cut at max_output_tokens.
object
Present and true when the approval window is open and the user has not answered within 45 s.
Examples
{ "error": "the model declined to answer: safety"}{ "error": "ai output cut at max_output_tokens: raise it or shorten the schema"}Emails.send: that user already received 5 emails in the last 24 hours, from every application together, or, for kind: marketing, your news in the last 24 hours (one a day per application and per user). Nothing was sent.
object
Present and true when the approval window is open and the user has not answered within 45 s.
Examples
5 emails in 24 hours
{ "error": "emails rate limited"}Your news already received in 24 hours
{ "error": "news already sent to this user today"}The operation failed in the browser, or its answer was lost: the browser dropped mid-call, or did not answer within 35 s. A lost answer carries outcome: "unknown": the request may have reached the site, so check before repeating a write. For emails.send, the email could not be sent, try again later: nothing was sent. For ai.generate, the model could not answer, try again later.
object
Present and true when the approval window is open and the user has not answered within 45 s.
Example
{ "error": "operation has timed out"}That browser cannot be reached. Nothing is queued. extension offline: the browser holds no connection right now. extension outdated: Apigami in that browser must be updated before it can connect again; Chrome installs the update on its own, and restarting Chrome applies it sooner. For ai.generate: the model is busy, with Retry-After in seconds, or the AI is not configured on Apigami’s side.
object
Present and true when the approval window is open and the user has not answered within 45 s.
Examples
{ "error": "extension offline"}{ "error": "extension outdated"}{ "error": "the model is busy, try again in a minute"}{ "error": "ai is not configured"}Headers
Section titled “Headers”Example
60Ai.generate, busy model only: the seconds to wait before trying again.