HTTP API reference
Everything an agent can do in its window, as JSON over HTTPS. The same abilities as the MCP tools, for your own code.
Basics
- Address
https://api.joinpane.com/v1- Key
Authorization: Bearer jp_live_…on every request. See Keys.- Requests
- JSON bodies, with
Content-Type: application/json. Without it the body isn't read: the request fails with400 invalid_input, or goes ahead without what you sent. - Answers
- JSON, with an
x-request-idheader. Send your ownx-request-id, 8 to 64 letters, numbers, dots, dashes or underscores, to find a request again. - Ids
- Windows are
win_…, questionsapr_…, taskstsk_…and identitiesidn_…. In paths,{window}and{question}stand for them. - Examples
$JOINPANE_KEYis your key,$WINDOWa window's id and$QUESTIONa question's.
MCP does the same through tools: see MCP tools. Each endpoint below names its tool.
Step results
Reading and acting in a window answer with the same shape. status says how it went.
| Status | Means |
|---|---|
| done | It acted, or read. You get the page: url, title, text, truncated, elements, tabs, and a screenshot when the step takes one. element is the one it acted on. |
| waiting | The press needs a person's yes and nobody answered within 20 seconds. question has the id to wait on. See Asking a person. |
| declined | A person said no. question says who. |
| stopped | It didn't act. reason and message say why; url and title say where. |
The page's text is its words, up to 8,000 characters; truncated says whether there were more. elements lists up to 400 things to press or fill in, and screenshot is the visible part of the page as a JPEG data URL. Each element has:
| Field | What it is |
|---|---|
| ref | What to act on, like e12. Refs come from the latest step: every read hands out new ones. |
| role | button, link, textbox, checkbox, combobox and the like. |
| name | What a screen reader would call it: its label or text. |
| type | A field's kind, like text, email or file. |
| value | A field's text. Never set for password fields. |
| checked | For checkboxes, radios and switches. |
| disabled | It can't be pressed now. |
| offscreen | It's outside the visible part of the page. Pressing it scrolls it into view. |
A read
{
"status": "done",
"url": "https://studio.example/content/uploads",
"title": "Uploads",
"text": "Content Uploads Search videos New upload Uploads 12 videos Episode 13 Draft Oct 13 Episode 12 Published Oct 6 Episode 11 Published Sep 29",
"truncated": false,
"elements": [
{
"ref": "e1",
"role": "link",
"name": "Content"
},
{
"ref": "e2",
"role": "link",
"name": "Uploads"
},
{
"ref": "e3",
"role": "searchbox",
"name": "Search videos",
"type": "search"
},
{
"ref": "e4",
"role": "button",
"name": "New upload"
},
{
"ref": "e5",
"role": "link",
"name": "Episode 13"
},
{
"ref": "e6",
"role": "link",
"name": "Episode 12"
},
{
"ref": "e7",
"role": "link",
"name": "Episode 11"
}
],
"tabs": [
{
"index": 0,
"title": "Uploads",
"url": "https://studio.example/content/uploads",
"active": true
}
],
"screenshot": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD…"
}A press waiting for a person
{
"status": "waiting",
"question": {
"id": "apr_01k7f4a6s2d8g3h5j9k1m7p0qr",
"status": "pending",
"question": "Uploader wants to press \"Publish\" on studio.example",
"options": [
"Approve",
"Decline"
],
"answer": null,
"answeredBy": null,
"askedPeople": [
"Mei Lin",
"Rin Kato"
]
}
}A step that stopped
{
"status": "stopped",
"reason": "stale-ref",
"message": "That element isn't on the page any more. Read the page again and use a new ref.",
"url": "https://studio.example/content/ep13/details",
"title": "Episode 13 details"
}| Reason | What happened |
|---|---|
| stale-ref | The element isn't on the page any more. Read the page again and use a ref from the new read. |
| password | It's a password field and agents in this workspace can't type into them. Ask a person. |
| blocked | The address isn't allowed by the identity's rules, the key isn't one it can press, there's no tab at that index, or the website didn't take the file. message says which. |
| human-check | The page wants a person to prove they're human. Joinpane never solves these: ask a person to do it in the window, then carry on. |
The key and its identities
Check a key works and see where its agent may go.
Check the key
The agent this key belongs to, its workspace, and the identities it may use with its access in each: drive (can drive) or read (read only).
Request
curl https://api.joinpane.com/v1/me \ -H "Authorization: Bearer $JOINPANE_KEY"
Response
{
"agent": {
"id": "agt_01k6v2q8r4m7x3c9t5b1n0hdzw",
"name": "Uploader",
"workspace": "Northlight"
},
"identities": [
{
"id": "idn_01k5r8m2c6x9v3t7q1b4n0wzhd",
"name": "Studio uploads",
"website": "studio.example",
"access": "drive"
}
]
}List identities
The same identities with how each is doing now: live, asleep, waking, code-needed, signed-out, not-signed-in, blocked, paused or setting-up.
list_identitiesRequest
curl https://api.joinpane.com/v1/identities \ -H "Authorization: Bearer $JOINPANE_KEY"
Response
{
"identities": [
{
"id": "idn_01k5r8m2c6x9v3t7q1b4n0wzhd",
"name": "Studio uploads",
"website": "studio.example",
"access": "drive",
"status": "asleep"
}
]
}- An asleep identity wakes when a window opens in it, in a second or two. A paused one, or one still being added, can't be opened until a person sorts it out.
Windows
An agent works in its own window inside an identity, which anyone with access can watch. Each window counts toward the identity's 5, people and agents together.
Open a window
Opens the agent's window in an identity, already signed in. If it has one there already, you get that one back.
open_window| Body | Description |
|---|---|
| identitystring, required | The identity's name or id. Names match whatever the case and punctuation, so studio-uploads finds Studio uploads. |
| anotherboolean | Open one more window there instead of getting back the one it has, up to the agent's Windows at once (1 to 3). Default false. |
Request
curl https://api.joinpane.com/v1/windows \
-H "Authorization: Bearer $JOINPANE_KEY" \
-H "Content-Type: application/json" \
-d '{"identity":"Studio uploads"}'Response
{
"window": {
"id": "win_01k7f3q2m9x4c8v6t1b5n0r2hd",
"identity": {
"id": "idn_01k5r8m2c6x9v3t7q1b4n0wzhd",
"name": "Studio uploads"
},
"url": "https://studio.example/content/uploads",
"title": "Uploads",
"access": "drive"
}
}503 node_unavailablemeans the identity is still starting: try again in a few seconds.409 seats_fullmeans all 5 windows are open. Try again when one closes.409 conflictwithanothermeans the agent already has as many windows there as it may.
List windows
The agent's open windows, in every identity it may use.
list_windowsRequest
curl https://api.joinpane.com/v1/windows \ -H "Authorization: Bearer $JOINPANE_KEY"
Response
{
"windows": [
{
"id": "win_01k7f3q2m9x4c8v6t1b5n0r2hd",
"identity": {
"id": "idn_01k5r8m2c6x9v3t7q1b4n0wzhd",
"name": "Studio uploads"
},
"url": "https://studio.example/content/uploads",
"title": "Uploads",
"access": "drive"
}
]
}Close a window
Closes the window. It frees one of the identity's 5 and stops counting toward the agent's browser hours.
close_windowRequest
curl -X DELETE https://api.joinpane.com/v1/windows/$WINDOW \ -H "Authorization: Bearer $JOINPANE_KEY"
Response
{
"ok": true
}Reading and acting
Each of these is a step in the agent's window. A step acts the way a person would, with real clicks and keys, waits for the page to settle, and answers with a step result.
Read the page
The page in the window's current tab: its text, the elements you can press or fill in, its tabs and a screenshot.
read_page| Query | Description |
|---|---|
| screenshottrue or false | Include a screenshot. Default true. |
Request
curl https://api.joinpane.com/v1/windows/$WINDOW/page \ -H "Authorization: Bearer $JOINPANE_KEY"
Response
{
"status": "done",
"url": "https://studio.example/content/uploads",
"title": "Uploads",
"text": "Content Uploads Search videos New upload Uploads 12 videos Episode 13 Draft Oct 13 Episode 12 Published Oct 6 Episode 11 Published Sep 29",
"truncated": false,
"elements": [
{
"ref": "e1",
"role": "link",
"name": "Content"
},
{
"ref": "e2",
"role": "link",
"name": "Uploads"
},
{
"ref": "e3",
"role": "searchbox",
"name": "Search videos",
"type": "search"
},
{
"ref": "e4",
"role": "button",
"name": "New upload"
},
{
"ref": "e5",
"role": "link",
"name": "Episode 13"
},
{
"ref": "e6",
"role": "link",
"name": "Episode 12"
},
{
"ref": "e7",
"role": "link",
"name": "Episode 11"
}
],
"tabs": [
{
"index": 0,
"title": "Uploads",
"url": "https://studio.example/content/uploads",
"active": true
}
],
"screenshot": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD…"
}Press an element
Presses a button, link or other element by its ref, then reads the page with a screenshot. Presses that publish or pay ask a person first, and so can presses that delete, send messages or change settings. See Asking a person.
click| Body | Description |
|---|---|
| refstring, required | An element's ref from the latest read, like e12. |
| approvalstring | The id of a question a person approved, to make the press it asked about. |
Request
curl https://api.joinpane.com/v1/windows/$WINDOW/click \
-H "Authorization: Bearer $JOINPANE_KEY" \
-H "Content-Type: application/json" \
-d '{"ref":"e5"}'Response
{
"status": "done",
"element": {
"ref": "e5",
"role": "link",
"name": "Episode 13"
},
"url": "https://studio.example/content/ep13/details",
"title": "Episode 13 details",
"text": "Content Uploads Episode 13 details Title Description Thumbnail Visibility Draft Save draft Publish",
"truncated": false,
"elements": [
{
"ref": "e1",
"role": "link",
"name": "Content"
},
{
"ref": "e2",
"role": "link",
"name": "Uploads"
},
{
"ref": "e3",
"role": "textbox",
"name": "Title",
"type": "text",
"value": "Episode 13"
},
{
"ref": "e4",
"role": "textbox",
"name": "Description"
},
{
"ref": "e5",
"role": "button",
"name": "Upload thumbnail"
},
{
"ref": "e6",
"role": "button",
"name": "Save draft"
},
{
"ref": "e7",
"role": "button",
"name": "Publish"
}
],
"tabs": [
{
"index": 0,
"title": "Episode 13 details",
"url": "https://studio.example/content/ep13/details",
"active": true
}
],
"screenshot": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD…"
}Type into a field
Clicks into a field by its ref and types, then reads the page without a screenshot.
type| Body | Description |
|---|---|
| refstring, required | The field's ref, like e4. |
| textstring, required | What to type. Up to 20,000 characters. |
| clearboolean | Replace what's in the field. Default true. |
| submitboolean | Press Enter afterwards. Default false. Enter presses the form's button, so a submit that publishes or pays (or deletes, sends or changes settings, when the agent asks about those) asks a person first, like a click. |
| approvalstring | The id of a question a person approved, to make the submit it asked about. |
Request
curl https://api.joinpane.com/v1/windows/$WINDOW/type \
-H "Authorization: Bearer $JOINPANE_KEY" \
-H "Content-Type: application/json" \
-d '{"ref":"e4","text":"Our spring live special, in full."}'Response
{
"status": "done",
"element": {
"ref": "e4",
"role": "textbox",
"name": "Description"
},
"url": "https://studio.example/content/ep13/details",
"title": "Episode 13 details",
"text": "Content Uploads Episode 13 details Title Description Thumbnail Visibility Draft Save draft Publish",
"truncated": false,
"elements": [
{
"ref": "e1",
"role": "link",
"name": "Content"
},
{
"ref": "e2",
"role": "link",
"name": "Uploads"
},
{
"ref": "e3",
"role": "textbox",
"name": "Title",
"type": "text",
"value": "Episode 13"
},
{
"ref": "e4",
"role": "textbox",
"name": "Description",
"value": "Our spring live special, in full."
},
{
"ref": "e5",
"role": "button",
"name": "Upload thumbnail"
},
{
"ref": "e6",
"role": "button",
"name": "Save draft"
},
{
"ref": "e7",
"role": "button",
"name": "Publish"
}
],
"tabs": [
{
"index": 0,
"title": "Episode 13 details",
"url": "https://studio.example/content/ep13/details",
"active": true
}
]
}- Password fields stop with
passwordunless an admin lets agents type into them. Their values never appear in a read.
Press a key
Presses one key, then reads the page without a screenshot.
press_key| Body | Description |
|---|---|
| keystring, required | One of Enter, Tab, Escape, Backspace, Delete, Space, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, PageUp, PageDown, Home, End. Enter and Space press what's focused (or its form's button), so they ask a person first when a click on it would. |
| approvalstring | The id of a question a person approved, to make the press it asked about. |
Request
curl https://api.joinpane.com/v1/windows/$WINDOW/press \
-H "Authorization: Bearer $JOINPANE_KEY" \
-H "Content-Type: application/json" \
-d '{"key":"Escape"}'Response
200 with a step result.
Scroll
Scrolls by most of a screen, then reads the page with a screenshot.
scroll| Body | Description |
|---|---|
| directionstring | down or up. Default down. |
Request
curl https://api.joinpane.com/v1/windows/$WINDOW/scroll \
-H "Authorization: Bearer $JOINPANE_KEY" \
-H "Content-Type: application/json" \
-d '{"direction":"down"}'Response
200 with a step result.
Go back
Goes back a page in the current tab, then reads it with a screenshot.
go_backRequest
curl -X POST https://api.joinpane.com/v1/windows/$WINDOW/back \ -H "Authorization: Bearer $JOINPANE_KEY"
Response
200 with a step result.
Go forward
Goes forward a page in the current tab, then reads it with a screenshot.
Request
curl -X POST https://api.joinpane.com/v1/windows/$WINDOW/forward \ -H "Authorization: Bearer $JOINPANE_KEY"
Response
200 with a step result.
Switch tab
Makes another of the window's tabs the current one, by the index a read gives it, then reads it with a screenshot. Websites open tabs of their own; a read lists them all.
switch_tabRequest
curl -X POST https://api.joinpane.com/v1/windows/$WINDOW/tabs/1 \ -H "Authorization: Bearer $JOINPANE_KEY"
Response
200 with a step result.
Drive files
When a website asks for a file, an agent sends one from Drive. Which files it may send is an admin's choice: its identities' files, all of Drive, or none.
List Drive files
The Drive files the agent may send, newest first, up to 500. Sizes are in bytes.
list_drive_files| Query | Description |
|---|---|
| identitystring | An identity's id, to list only files from it. Applies when the agent may send only its identities' files. |
Request
curl https://api.joinpane.com/v1/drive \ -H "Authorization: Bearer $JOINPANE_KEY"
Response
{
"files": [
{
"id": "file_01k6y9e3w7r1t5z8x2c4v6b0nm",
"name": "ep13_final_cut.mp4",
"size": 1840227533,
"mime": "video/mp4",
"identityId": "idn_01k5r8m2c6x9v3t7q1b4n0wzhd"
}
]
}Send Drive files
Sends Drive files to the website: presses its file field or upload button and answers the file picker. Then reads the page without a screenshot.
upload_file| Body | Description |
|---|---|
| filesstring[], required | File names or ids from Drive, 1 to 20. |
| refstring | The file field or upload button. Without it, the page's first file field. |
Request
curl https://api.joinpane.com/v1/windows/$WINDOW/upload \
-H "Authorization: Bearer $JOINPANE_KEY" \
-H "Content-Type: application/json" \
-d '{"files":["ep13_final_cut.mp4"],"ref":"e4"}'Response
200 with a step result.
- Many websites hide their file field behind a button. If the read lists no element of type
file, pass the button's ref.
Asking people
Presses that need a person's yes ask on their own. An agent can also ask a question of its own, with the answers it offers.
Ask people
Asks the people this agent asks, all at once, with a screenshot of the window. The first answer counts. Waits up to 20 seconds for it before answering.
ask_person| Body | Description |
|---|---|
| windowstring, required | One of the agent's open windows. |
| questionstring, required | The question, 3 to 500 characters. |
| detailstring | What you'll do with the answer. Up to 2,000 characters. |
| optionsstring[] | The answers to offer, 2 to 6. Default ["Yes", "No"]. |
Request
curl https://api.joinpane.com/v1/questions \
-H "Authorization: Bearer $JOINPANE_KEY" \
-H "Content-Type: application/json" \
-d '{"window":"win_01k7f3q2m9x4c8v6t1b5n0r2hd","question":"Two drafts are called Episode 13. Which one should I publish?","options":["The one from Oct 12","The one from Oct 13"]}'Response
{
"question": {
"id": "apr_01k7f4a6s2d8g3h5j9k1m7p0qr",
"status": "pending",
"question": "Two drafts are called Episode 13. Which one should I publish?",
"options": [
"The one from Oct 12",
"The one from Oct 13"
],
"answer": null,
"answeredBy": null,
"askedPeople": [
"Mei Lin",
"Rin Kato"
]
}
}Check a question
Where a question stands: pending, answered, skipped (nobody answered in 30 minutes and the agent skips) or cancelled.
wait_for_answer| Query | Description |
|---|---|
| waitseconds | Wait up to this long for an answer before replying, 0 to 25. Default 0. |
Request
curl "https://api.joinpane.com/v1/questions/$QUESTION?wait=25" \ -H "Authorization: Bearer $JOINPANE_KEY"
Response
{
"question": {
"id": "apr_01k7f4a6s2d8g3h5j9k1m7p0qr",
"status": "answered",
"question": "Uploader wants to press \"Publish\" on studio.example",
"options": [
"Approve",
"Decline"
],
"answer": "Approve",
"answeredBy": "Mei Lin",
"askedPeople": [
"Mei Lin",
"Rin Kato"
]
}
}Tasks
A task names what the agent is doing, so people watching can follow its steps in Agents, and its plan shows the steps still to come. Opening a window starts one called “Working in Studio uploads” when none is running there. See Tasks and plans.
Start a task
Starts a named task. The agent's task still running there ends as done.
start_task| Body | Description |
|---|---|
| titlestring, required | What it's doing, up to 200 characters. |
| identitystring | The identity it's for, by name or id. |
| planstring[] | The steps it means to take, which people watching see as the steps to come. Up to 20, each up to 120 characters: longer ones are shortened and empty ones dropped. |
Request
curl https://api.joinpane.com/v1/tasks \
-H "Authorization: Bearer $JOINPANE_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Publish episode 13","identity":"Studio uploads","plan":["Open episode 13","Add its description","Ask before publishing","Publish it"]}'Response
{
"task": {
"id": "tsk_01k7f3p0y5w2e8r4t6z9x1c3vb",
"title": "Publish episode 13",
"status": "running",
"plan": [
"Open episode 13",
"Add its description",
"Ask before publishing",
"Publish it"
]
}
}Update the plan
Replaces the plan of the agent's latest task that's still going. Send what's left to do, not what's done.
update_plan| Body | Description |
|---|---|
| stepsstring[], required | The steps still to come, in order, cleaned like plan above. An empty list clears the plan. |
Request
curl -X PUT https://api.joinpane.com/v1/tasks/current/plan \
-H "Authorization: Bearer $JOINPANE_KEY" \
-H "Content-Type: application/json" \
-d '{"steps":["Ask before publishing","Publish it"]}'Response
{
"task": {
"id": "tsk_01k7f3p0y5w2e8r4t6z9x1c3vb",
"title": "Publish episode 13",
"plan": [
"Ask before publishing",
"Publish it"
]
}
}404 not_foundwhen no task is running: start one first.
Finish the task
Ends the agent's latest task that's still going, as done or stopped.
finish_task| Body | Description |
|---|---|
| statusstring | done or stopped. Default done. |
| summarystring | How it went, up to 500 characters. |
Request
curl https://api.joinpane.com/v1/tasks/current/finish \
-H "Authorization: Bearer $JOINPANE_KEY" \
-H "Content-Type: application/json" \
-d '{"summary":"Published episode 13."}'Response
{
"task": {
"id": "tsk_01k7f3p0y5w2e8r4t6z9x1c3vb",
"title": "Publish episode 13",
"status": "done"
}
}Errors
An error comes with its HTTP status as {"error": {"code", "message"}}, with fields for invalid input and the requestId. Messages say what happened and how to fix it, in words you can show a person or a model as they are.
{
"error": {
"code": "seats_full",
"message": "All 5 windows in Studio uploads are open. Try again when one closes.",
"requestId": "5b0e8f0e-2f4a-4c47-9d0e-3c1f6a7b2d91"
}
}| Code | When |
|---|---|
| invalid_input400 | A field is missing or isn't valid. fields names each one. Send JSON with Content-Type: application/json. |
| bad_request400 | The request can't be done as asked: no file field on the page, a Drive file the agent may not send, a question from a window that isn't open. |
| unauthenticated401 | No key, or one that's wrong, expired or turned off. |
| forbidden403 | Not allowed: a press or typing where the agent can only read, someone else's window, an approval for a different press or one nobody approved. |
| plan_upgrade_required403 | The workspace is on Free, which has no agents. |
| not_found404 | An identity the agent can't use, a window that's closed, or a question or task that doesn't exist. A closed window: open one again. |
| conflict409 | The agent is paused (by a person, or at its monthly budget), a person is driving its window, nobody is set to answer its questions, or a step didn't work. message says which. |
| identity_unavailable409 | The identity is paused or still being added. |
| seats_full409 | All 5 windows in the identity are open. |
| node_unavailable503 | The identity is starting, or the window didn't answer in time. Try again in a few seconds. |
| internal500 | Something went wrong on our side and nothing was changed. Try again; if it keeps happening, send us the requestId. |
A 401 also carries WWW-Authenticate: Bearer realm="joinpane".
Limits and timing
- There's no rate limit per request. What limits an agent is its windows, its monthly hours and how long each step takes. See Windows, hours and pauses.
- One step at a time per window. Wait for a step's answer before sending the next one to the same window.
- A step waits for the page to settle, a few seconds at most, and gives up after 45 seconds with
503 node_unavailable. - Waiting for people: a press or a question waits up to 20 seconds within the call;
GET /v1/questions/{question}waits up to 25 withwait. People have 30 minutes before the agent's “If nobody answers” setting applies. - Sizes: page text up to 8,000 characters, up to 400 elements, typing up to 20,000 characters, up to 20 files in one upload, and the newest 500 Drive files in a list.