Skip to main content
You are an agent. Your job is to stand up a working Gobare integration and prove it works, without a person answering questions in between. Every other page here is written for a person who can open a second tab. This one is written for a reader that gets one pass, cannot follow a link cheaply, and has to decide rather than browse. So it is arranged as a sequence with a check after every step, and the traps are written next to the call that springs them rather than collected at the end. What it does not do is tell you what to build. That is in your task, not on this page. Choosing what to switch on maps the requirement you were given to the parts of the API that serve it; the rest is the same for every integration. It sits with the TypeScript and Python clients because it is the third one. Those two hand a caller typed methods and five guards it would otherwise have to write; this hands a caller the same guards as an order of operations, because the client here has no package manager and its runtime is a context window. There is nothing to install. Read it, and you are the client. Read this page once, top to bottom, before your first call. Sections 4 to 11 depend on decisions made in 2 and 3.
Every /v1 response carries Link: </v1/openapi.json>; rel="service-desc". When a field on this page is not enough, that document is the exact truth about every endpoint, parameter and shape, and you already have the URL in a response you made.

1. Preflight

Do this before anything else. Each check has one call, one expected answer, and one thing to do when it is not that.

1.1 Does the token work, and what may it do

/v1/health answers for any valid token whatever its scopes, so this is how you learn what you hold rather than guessing. A 401 also carries WWW-Authenticate: Bearer realm="gobare". Treat 401 as terminal: nothing about waiting makes a rejected token accepted.

1.2 Do you hold the scopes your plan needs

There are six: sessions:read, sessions:write, tools:respond, artifacts:read, credentials:write, and cli — which is the Console’s default kind and is refused by every /v1 route. Which endpoint needs which is one table in production-integration.md, and it is the only copy on purpose; each operation in GET /v1/openapi.json also carries its own. What you have to decide here is not the mapping but the combination, and there are three worth naming:
  • Drives sessions end to end — read, write, respond and artifacts together. It still cannot connect a model provider; that needs credentials:write.
  • Only answers the agent’s calls — read plus tools:respond. It cannot send a message, cancel a turn or delete anything, which is what you want a fleet of tool handlers to hold.
  • Only watches and collects — read plus artifacts:read, and it really is read-only: nothing it holds changes state, including deleting an artifact.
Ask for the narrowest one your §2 rows need. A call missing its scope is 403 permission_denied and the message names the scope it wanted, so a mistake here costs one call, not an investigation.

1.3 Is there a model to run

Take model from here. Do not invent a model name. Asking for one this organization has not connected is refused by name. An empty list means nothing can run yet. If you hold credentials:write you can fix it yourself:
The key is verified against the provider before anything is stored, so a bad key fails here rather than inside your first turn. If several vendors issue keys with that prefix the refusal says so and lists them — send provider as well. Full rules in model-credentials.md. If you do not hold credentials:write, stop and say so. It is deliberately not included in a session-creating token: a session spends sandbox minutes, a provider key decides whose bill every future turn lands on.

1.4 The two preconditions that are not visible from here

Check these only if §2 says your plan needs them, and check them before you build, because both fail late and confusingly:
  • A repository needs the organization to have a GitHub connection. Without one, a session naming environment.repo is refused up front.
  • A public URL needs the deployment to have a base domain. Without one, POST /v1/sessions/{id}/preview answers not_found saying so.

2. Choosing what to switch on

This is the only section that depends on your task. Read the requirement you were given, find its rows, and switch on exactly those. Everything else stays off — an unused capability is a thing that can fail. Write down the rows you chose. §11 turns that list into the self-test you have to pass before you are finished.

3. The five nouns, and the one state machine that matters

Ids are ours and opaque: sessions are UUIDs, turns are turn_…, artifacts are art_…, subscriptions are whsub_…. Do not parse them. Times are milliseconds since the epoch, as numbers.

3.1 The mistake that costs the most

A turn has four statuses and no fifth one. working covers everything before it settles — including a turn that is parked, waiting for you to answer something. There is no waiting. Polling turn.status for a fifth value is an infinite loop that ends when the workspace is reclaimed two hours later. What changes when the agent needs you is the session: So the rule is: poll the session to learn that you are needed; poll the turn to learn that the work is over. You do not have to wait for environment.state: "running" before sending work. A message queues against a session in any state and the workspace comes up to serve it.

4. Configure the session

One call, and everything is optional except having a model available.
Verify: the reply is 201 and carries an id. Read it back with GET /v1/sessions/{id} and confirm agent.model, agent.approval_mode and agent.permission_rules are what you sent. Everything you can set, you can read back, so nothing about what a session enforces is something you have to remember having sent.

4.1 Field notes that are not guessable

  • metadata is your only durable handle. Put your own job id there at creation. After a restart, GET /v1/sessions?metadata=job:8842 finds the session and ?status=requires_action finds everything owing you an answer — each listed session carries its own required_actions, so one call is enough.
  • instructions are not a permission system. They shape behaviour; they do not constrain it. approval_mode and permission_rules constrain it. Where your instructions and the product’s own rules disagree, the product’s win.
  • instructions apply from the session’s next workspace, not mid-turn. A PATCH during a running turn is not ignored — it takes effect on the turn after.
  • An unknown field is refused, never ignored, and the refusal names the nearest accepted field and lists the rest. modelCredentialId finds model_credential_id; read-only finds read_only. Take the suggestion, but read the list beside it — the suggestion travels with the full vocabulary precisely so a wrong guess is recoverable from one sentence.
  • environment.branch does not exist. The clone reports which branch it landed on in environment.repo.branch; it cannot be told which to use.
  • input at creation is all or nothing. A 201 means the session exists and the opening message was accepted. A non-2xx normally means nothing exists — except when the message failed and we could not clean up, in which case the error names the session id you now have to delete. Read the message.

4.2 What you can change afterwards

PATCH /v1/sessions/{id} takes title, metadata, and — nested under agentinstructions, approval_mode and permission_rules:
A flat {"approval_mode":"per_step"} is invalid_request naming the field. A PATCH with no recognised field at all is also refused rather than accepted as a no-op.

4.3 Reusing one configuration

If more than one session in your integration starts the same way, save it once:
name is a slug you choose; posting the same name again replaces that agent in place and keeps its id. A replacement really replaces — a field you leave out is removed, not carried over. Inline fields beat the agent, and how depends on the field: model, model_credential_id and instructions override per field, so overriding the model keeps the agent’s instructions. tools and text are replaced whole, never merged. Do not read an agent back and post it straight in again. A read-back carries redacted, naming the secrets withheld, and redacted is not a request field — you get a 400 rather than a success that quietly blanked your credentials. Build the body from your own source, or re-send the secrets. Full rules in saved-agents.md.

5. Give it tools

PUT, not POST: send the complete list every time. At most 32 entries, each name 1–64 characters. You can pass the same list as agent.tools at creation.
Verify: GET …/tools returns the same vocabulary you sent — one tools array, each entry carrying its type. Count it. A tool you believe you declared and did not is the cause of an agent that “ignores” your function. Everything else the agent can do — shell, reading and writing files, starting services, publishing a preview, committing — is always present and is not configured here. You are choosing what it can reach outside the workspace.

5.1 Function tools

The agent calls it; the turn parks; you answer. That loop is §7. Two halves, both required: the declaration is what the model is told exists, and your handler is what your process answers with. Declaring without a handler parks the turn on a call nobody answers. Handling without a declaration means the call never happens. Set timeout_seconds on every function in production. The default is no deadline at all: a process that dies mid-answer holds a workspace until its two-hour cap with nothing anywhere saying so. Past the deadline the call fails and the turn carries on — so guessing too short costs you one failed call, not a lost turn. Accepted range is 1 to 7200.

5.2 MCP servers

Exactly one of url (a server the sandbox connects out to) or command (a process started inside the sandbox, spoken to over stdio). The connection is made from the sandbox, not from the control plane. Decide required deliberately. With required: false — the default — a server that will not connect is skipped, the agent silently has fewer tools, and you get an mcp_unavailable item and an mcp.unavailable event. With required: true the session refuses to run and the error names the server and the reason. It arrives as invalid_request on the first call that needs the workspace, deliberately not a retryable code: waiting will not make the server reachable. If a command server reads a file, do not put that file in environment.files. Seeded files are written when the workspace comes up, which can be after the MCP client has already tried to start the process. Pass the program inline as an argument, or write it with POST /files and declare the tool afterwards.

5.3 read_only refuses MCP tools that are not annotated

This is the interaction that looks like a broken model and is not. read_only allows an MCP tool whose server declares readOnlyHint in tools/list, and refuses one that does not — “no annotation” is not “harmless”, and an organization’s private plugin has been reviewed by nobody. The obvious shape, a read-only investigation against a read-only runbook server, therefore fails unless the server annotates. Two ways out, and a rule beats the mode in both directions:
A refusal is visible, and you should watch for it. It arrives as approval.resolved with approved: false, the tool’s name, a code of denied_read_only or denied_by_rule, and the sentence the agent was given. Match on code; it is the difference between a tool the platform blocked and one the model never tried.

6. Send work

All input goes to one endpoint, one event per request, and the answer is 202 — the agent has the work, not the answer.
Full field lists are in input.md.

6.1 Finding the turn your message produced

The reply carries no turn_id, because when it is written there is no turn yet. If you stream or take a webhook, the id arrives with them. If you poll, this is a trap with a specific shape:
Remember the latest turn id before you send, then wait for one that differs. A fresh session has no previous turn and needs none of this.

6.2 Queueing is not steering

A message sent while a turn is running is accepted and queued — the reply says "queued": true with a position. To change what a running turn is doing you must say so by name with input.steer, which is a conflict when nothing is running rather than quietly becoming a new turn. Five queued messages per session is the ceiling; past it, queue_full.

7. Watch, and answer what it asks

Pick exactly one primary mechanism. Mixing them is fine, but one of them owns the decision “is this done”.

7.1 The event stream

GET /v1/events is the same thing for every session the organization owns.
  • Subscribe before you send. The other order loses the opening frames whenever the agent starts quickly, which is to say intermittently and never on your machine.
  • Only persisted events carry an id: line. Reconnect with Last-Event-ID: <seq> and you get everything persisted after that point, then live frames. Transient events — agent.text, agent.thinking, progress — carry seq: null, are not replayed, and are decoration. Build on the persisted ones.
  • No cursor and 0 are different requests. No cursor is live frames only, which is what “subscribe first, then send” wants. 0 is everything, because zero is a real cursor.
  • Honour the retry: hint rather than reconnecting instantly. It is deliberately longer than the time it takes us to notice your last connection went away, so a client following it is not racing us for its own stream slot.
  • A : ping comment every 15 seconds keeps the connection open. Ignore it.
The persisted vocabulary is an allowlist: turn.started, turn.ended, agent.message, agent.tool_call, agent.tool_result, agent.compaction, agent.error, agent.todos, user.message, message.queued, message.dequeued, file.changed, approval.requested, approval.resolved, question.asked, question.answered, tool.required, tool.resolved, mcp.unavailable, sandbox.created, sandbox.paused, sandbox.resumed, preview.ready, workspace.recovery_failed, artifact.created. turn.ended is deliberately neutral: the run finished, and whether it succeeded is a property of the turn it names. Read the turn.

7.2 Webhooks

Seven event types: session.created, session.action_required, session.working, session.idle, session.failed, turn.completed, turn.failed. The secret comes back once. Store it from this response; it is never shown again. Verification is HMAC-SHA256(secret, "{timestamp}.{body}"), hex, compared against x-gobare-signature. Three rules, and each has a failure that looks like something else:
  1. Verify the raw body, before any parse and re-serialise. A round trip changes key order and will not match.
  2. x-gobare-timestamp is milliseconds, the same units as Date.now(). Dividing by 1000 rejects every delivery and fails looking exactly like a bad signature.
  3. Reject a timestamp far from your clock — a few minutes is reasonable.
data names the object; it never embeds it. Read the object afterwards and you see current truth. Delivery is at least once and can arrive out of order, so make the handler idempotent. Answer inside 10 seconds and do the work afterwards. Subscribing the same URL to exactly the same event set twice is 409 conflict naming the subscription already doing it — a duplicate buys you every delivery twice, permanently. More in webhooks.md.

7.3 Answering a required action

When the session reports requires_action, read required_actions. Every entry carries type, turn_id, call_id, and expires_at (or null).
All three types are answerable through this API. Branch on type: approval and question can also be resolved by a person in the Console, which is often what you want. What matters is that an API-only integration is not stuck waiting for one.
output must be a string. Serialise it yourself; nothing will guess whether your object was meant as JSON text. Never put a thrown exception’s message in error. It goes into the model’s context, and a stack trace is an efficient way to put your database host in a prompt. Send a fixed string and log the real one. The reply’s outcome is the thing to branch on: not_delivered is the one that needs handling rather than logging. The sandbox was not holding that call when your answer arrived — usually because it was restarted underneath the turn. Nothing fails, nothing times out: the session goes back to reading working with an empty required_actions, which is indistinguishable from healthy on every surface including webhooks. Treat it as “this turn lost my answer”: start a fresh turn with the same information, or fail the job and say why. Do not wait.

8. Collect the result

8.1 Artifacts

Anything written under /workspace/outputs is published when the turn settles, and outlives the workspace. status: "completed" is not yet a promise that the artifact list is filled in — publication runs after the turn settles so that a storage problem can never delay your work. Wait on turn.artifacts, not on turn.status: With webhooks you can skip this: turn.completed is sent once publication has settled. If it has not settled after 30 seconds the notification is sent anyway with the turn still reading pending, so read the field rather than assuming.
The archive is a streamed tar. With ?turn_id= the entries carry the workspace’s own paths; without it you get the whole session and each entry is prefixed with the turn that published it, because two turns writing report.md are two files and a flat archive would extract as one.

8.2 The workspace itself

Artifacts only ever cover /workspace/outputs. To answer “the agent said it wrote that — did it?”, read the workspace:
A read is of the last snapshot, not the live sandbox. captured_at says which moment; state: "missing" means none has been taken, which is not the same as an empty workspace. refresh takes a fresh one — a separate call, and a write scope, because it wakes a paused sandbox. GET /files reads no query parameters at all. Sending ?limit= is a 400 rather than a silent success, which is the general rule: an ignored filter hands back everything looking exactly like a filter that matched everything.

8.3 The transcript

GET /v1/sessions/{id}/items is the durable record and it outlives the workspace. Item types: message, tool_call, command_execution, file_change, approval, question, mcp_unavailable, error. error is the one to look for when a session says failed and there are no turns at all. A run that could not start leaves no turn behind; this item says why, in detail.message. A seeded file that could not be written appears here too.

8.4 Publishing what it built

Publishing needs the agent to have started a server and called preview(port) — ask for it in the same sentence as the work. preview.port on the session is the signal that there is something to publish; until it is set, publishing is refused rather than answered with an address that returns errors.
Omit subdomain and one is derived from the session id, which is what you want when the address is for a machine rather than a person. Publishing twice to the same address answers 200 with the same URL; unpublishing twice answers 200 too. Both are retry-safe on purpose. Details and every refusal in preview.md. A published address is a stable route, not a permanently running computer: an idle workspace is still paused, and the first public request wakes it. A visitor waits a few seconds and then sees the site.

9. Rules you must not break

Each one prevents a plausible wrong outcome rather than an error. That is why they are worth the lines.
  1. Poll the session for requires_action; poll the turn for completion. There is no waiting turn status. (§3.1)
  2. Remember the previous turn id before you send, or you will read the previous turn’s result. (§6.1)
  3. Subscribe before you send. (§7.1)
  4. Wait for turn.artifacts, not turn.status. (§8.1)
  5. Branch on error.code, never on the HTTP status. Three codes share 429 and one of them never clears. (§10)
  6. Honour Retry-After, and give up when there is none. Its absence is the signal, not an omission. (§10)
  7. output in a tool result is a string, and never a thrown exception’s message. (§7.3)
  8. PUT /tools replaces, and a read-back cannot be posted straight in — redacted is refused. (§5)
  9. If nothing in your integration can answer a question, tell the agent not to ask one. One line in instructions: “Never ask the user a clarifying question: if something is ambiguous, state your assumption and continue.” Otherwise it parks on a question and waits. (§7.3)
  10. Delete every session you create. Twenty-five concurrent per organization, a fork spends one too, and a paused sandbox still holds its slot. (§10.2)
  11. Send Idempotency-Key on every write, and a fresh one per logical operation. It is not scoped to the body: reusing a key with different content replays the first answer rather than doing the second thing. (§6)
  12. Never put a gbr_pat_ token in browser JavaScript. There is no CORS on /v1, deliberately; call it from your backend.

10. When something is refused

Every failure has one shape:
request_id is also on x-request-id, on every response including the successful ones. Log it next to whatever you record about the call.

10.1 Code to action

Everything marked retryable carries Retry-After; the rest carry none. A client that honours the header and gives up without one is doing the right thing on every row above without knowing any of them. The three 429s are the reason rule 5 exists: a client that retries all of them identically spins forever on project_limit_exceeded. If you have a symptom rather than a code, troubleshooting.md is indexed by what you are looking at; the full table with statuses is errors.md.

10.2 Budget, and the ledger you must keep

Pace yourself from the headers rather than discovering the wall. Every authenticated response carries them:
resource names which bucket: sessions (10 per minute, for POST /v1/sessions) or general (120 per minute, everything else). The same token legitimately holds 9 of one and 119 of the other at once, so without the name one of those numbers reads as a bug. The ceilings you are most likely to meet: Every number, including the ones not here, is in limits.md. Keep a ledger. Before you finish, every one of these must be accounted for:
An integration that creates and never deletes stops being able to create, and the failure arrives as project_limit_exceeded on somebody else’s work.

11. Prove it, then stop

Do not report the integration as working until this passes. Run the rows you chose in §2 and nothing else; a check you skipped is not a check that passed. Two rules about reporting:
  • A check that cannot pass is a result, not a reason to narrow the scope. Say which one, what you saw, and what you think it means. Quote the request_id.
  • Distinguish “the API refused me” from “my request was wrong”. The refusal message names the field, the scope or the ceiling. If it named one, it was yours.

12. Where the truth is

This page is a build order. When you need a field rather than a sequence: If this page and the OpenAPI document disagree, the OpenAPI document is right and this page has a bug. Say so in your report.