Browser sessions
A session opens a payer's browser profile without an intent, for you to drive step by step. Use one to sign a customer in to a shop once, so their later intents find them signed in, or to see what a profile holds. Nothing decides for you in a session: you send each step.
A session never pays and never places an order. Buying is what intents are for, with their spending caps and receipts.
Signing a customer in
Many shops want an account to check out, and their sign-in asks for things only your customer knows: a password, a code sent to their phone. The easy way is the sign-in link an intent waits with when the shop asks: you send it, your customer signs in on our page, and you build nothing. A session lets your app run the sign-in itself instead, and pass those on.
-
Open a session on the customer's profile, at the shop's sign-in page:
bashcurl https://api.openintents.io/v1/sessions \ -H "Authorization: Bearer $OPENINTENTS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "customer": "cus_42", "start_url": "https://www.example-shop.com/signin" }'It answers
201with the sessionqueued. It isopenwithin seconds: follow its events or retrieve it. -
Look at the page with a
snapshot. Each part you can act on has aref, such ase7for the email box. -
Act: type the email into
e7, click Continue, type the password, then the code the shop sent. Show your customer the page with a screenshot and pass on what they type. -
Release the session. The profile keeps what the session changed: the customer is signed in for their next intents at that shop.
Your app builds the screen where the customer types; their password passes through your app on its way. Don't log it. OpenIntents keeps what a session types only until the command runs (see typed text).
The session object
{
"id": "ses_51f0e2",
"object": "session",
"profile": "prf_7c1d04e2a9b3",
"customer": "cus_42",
"status": "open",
"test": false,
"start_url": "https://www.example-shop.com/signin",
"url": "https://www.example-shop.com/signin",
"title": "Sign in",
"created_at": "2026-10-13T08:30:00Z",
"opened_at": "2026-10-13T08:30:04Z",
"expires_at": "2026-10-13T08:45:04Z",
"closed_at": null,
"close_reason": null
}| Status | Meaning |
|---|---|
queued | Waiting for a browser. |
opening | The browser is starting on the profile. |
open | Ready for commands. |
closing | Released (or at a limit): the browser closes and the profile is saved. |
closed | Over; close_reason says why. |
failed | Over, because its browser stopped; the profile keeps what was last saved. |
close_reason is released, idle, timed_out, data_limit,
browser_lost or error. Others may appear.
Commands
Commands run one at a time, in the order sent. The request waits up to 20
seconds: 200 with the result, or 202 with the command still pending,
which you follow with GET /v1/sessions/{id}/commands/{command_id}. A retry
with the same Idempotency-Key returns the same command instead of running it
again.
| Command | Body | Returns |
|---|---|---|
| Navigate | { "type": "navigate", "url": "https://..." } | The page's url and title. |
| Snapshot | { "type": "snapshot" } | snapshot: the page's parts, each you can act on with a ref. |
| Act | { "type": "act", "ref": "e7", "action": "type", "text": "[email protected]" } | The page after it. |
| Read | { "type": "read", "selector": "#order-history" } | text: the page's text, or one element's. |
act takes an action: click, type (with text), select (with
value) or press (with key, such as Enter, Tab or Shift+Tab).
{
"id": "cmd_9a2c",
"object": "session_command",
"type": "act",
"status": "done",
"url": "https://www.example-shop.com/signin/password",
"title": "Sign in",
"error": null,
"created_at": "2026-10-13T08:30:12Z",
"completed_at": "2026-10-13T08:30:13Z"
}A few rules keep acts on the element you meant:
- An
actnames areffrom the latest snapshot. After anavigate, and after anyactbut atypewithout line breaks, take a new snapshot first: the page may have changed under the old refs. Otherwise the act fails withnot_found, as it does when the page moved on since the snapshot. typegoes only into text fields (not_actionableotherwise). A frame's elements have refs of their own: act on those, not on the frame.textmay hold line breaks but no other control characters, andkeyis one key, alone or with Shift or Control. Anything else answers400.
What a session refuses
A command a session will not run comes back refused, with the reason in
error. payment_not_allowed is for:
- a control that places an order or pays (Place order, Pay, Buy now, Subscribe, Donate and the like), and on a checkout page also a bare Continue, Confirm, Submit or Next;
- any action on a payment card's field;
- typing what looks like a card number;
- Enter, pressed or typed as a line break, at a checkout or where an order button shows.
A navigate goes only to public https URLs on the default port
(url_not_allowed). Commands that could not run come back failed:
error.code | Meaning |
|---|---|
not_found | No such ref in the latest snapshot, or the page changed since. |
not_actionable | The element can't take that action. |
timed_out | The page took too long. |
browser_error | The browser could not do it. |
browser_lost | The browser stopped; the session failed with it. |
session_closed | The session ended before the command finished. |
Screenshots
A JPEG of the page, taken now. It shows the payer's signed-in pages, so it is
sent Cache-Control: no-store and never kept. At most 2 screenshots of a
session wait at once (429, too_many_commands).
Typed text
What a type command types (a password, say) is kept, sealed, only until the
browser takes the command. Results and events never return it: events show
typed_chars, how many characters were typed, and a snapshot never includes
what text fields hold. The page itself may still show it, in a screenshot for
example.
Limits
- An account can have 2 sessions that have not ended at once
(
409,session_limit). - A session closes
max_minutesafter it opens (at most and by default 15), after 5 minutes with no command sent or run (idle), or once its pages use more data than a session may (data_limit). Screenshots and event streams don't keep it open. - At most 20 commands wait in a session at once (
429,too_many_commands).
These may change.
Endpoints
profile or customer (not both; neither opens your account's own),
start_url, max_minutes and webhook_url (an https URL for this
session's events), all optional.
409 profile_busy when an intent or another session uses the profile.
Closes the browser and saves the profile: closing, then closed
(released). Releasing one that already ended returns it unchanged.
The session's timeline, as a page or a live stream. Webhooks can tell you when a session opens, closes or fails.