Specification
Status: Draft. Version 1.0. Breaking changes are listed in the changelog. Feedback is welcome in GitHub issues.
CAP lets an agent ask a company's agent a question and get the answer back. The company's tools, prompts, and keys stay with the company. There is one object, the message, and two endpoints. How to call it: Usage.
Front door
A company publishes one text file at https://{domain}/agent.txt, next to robots.txt.
Protocol: CAP/1.0
Name: Harbor Goods
Chat: https://harborgoods.example/chat
CAP: https://harborgoods.example/cap/v1
OAuth: https://harborgoods.example/oauth
Spec: https://harborgoods.example/cap.txt
Protocol is the first line. It names this format, so a caller can tell this file apart from an unrelated agent.txt. A caller that finds no Protocol line uses the file only when it has a CAP line. Name is the public agent. Chat is the page a person opens. CAP is the base URL for the two endpoints. OAuth is the authorization server for questions about a customer's account. Leave it off when the agent answers only public questions. Spec is a short caller card. Leave it off when you have no public URL for one. The file lists no tools, MCP servers, prompts, or credentials.
One domain has one agent. A person uses the chat page. An agent fetches the file. Both reach the same agent and get the same answers.
The chat page and the CAP URL may be hosted for the company. The company's own domain still serves agent.txt.
A caller fetches /agent.txt. It treats a redirect off the company's domain, an HTML body, or a file without a CAP line as no front door.
Message
| Field | Notes |
|---|---|
id |
Assigned by the owner |
status |
working, done, or failed |
reply |
Text. Set only when status is done. Links go in the text |
error |
{ code, message }. Set only when status is failed |
created_at |
|
finished_at |
Set when status is done or failed |
| Status | Meaning | Final |
|---|---|---|
working |
The owner has the message and is answering | No |
done |
reply is set |
Yes |
failed |
error is set |
Yes |
error.message is written for the person. It never names a tool, a vendor, or an internal id. login_required is the one defined failure code: the answer needs the person's account and no grant was sent. Owners may use other codes. A caller treats any failure as final for that message.
Operations
POST /cap/v1/messages
GET /cap/v1/messages/{id}
Every request carries CAP-Version: 1.0 and CAP-From: agent:{id}, naming the calling agent. A request for a logged-in person also carries Authorization: Bearer with an OAuth access token.
Send a message
{ "message": "Black running shoes in size 12", "context": { "style": "minimal, all black" } }
message is required text. context is an optional object: a short brief from the caller, such as sizes, dates, or preferences. It is not a vendor API payload and not the person's whole profile.
200with the message when the owner finished within its own wait. The status isdoneorfailed.202with the message when it did not. The status isworking. The response setsLocation: /cap/v1/messages/{id}andRetry-Afterin seconds.
The owner picks the wait, and it SHOULD be under 30 seconds. The caller sends nothing extra to get it.
Idempotency-Key is optional. The same key from the same CAP-From, with the same grant or none, returns the original message and does not answer again.
Follow up
{ "message": "Something cheaper?", "previous_message_id": "msg_c1fac527668c6694" }
previous_message_id is optional. It continues the conversation that message belongs to, so the owner answers with every earlier turn in mind. The caller sends only the new message and never resends earlier turns or replies. The owner rebuilds the thread from its own records, so a caller cannot put words in the owner's mouth.
The previous message must be visible to the caller, under the same rules as a read. A thread may move from no grant to a grant, such as after the person logs in. It never moves from one account to another, and it never drops a grant it had. A thread lasts as long as its messages are kept.
The previous message must be final. If it is still working, the owner returns 409. The owner may cap how many turns a thread keeps and returns 409 past that cap. The caller then starts a new thread without previous_message_id.
Read a message
GET /cap/v1/messages/{id} returns 200 with the message. While it is working, the response sets Retry-After.
A message is visible only to the CAP-From that sent it, with the same grant or none. Anyone else gets 404. The owner keeps a finished message for at least 24 hours. After that, a read is 404 and the caller asks again.
Access
A public question needs no token. A question about an account needs the person to log in.
The person logs in at the OAuth server and consents. The token is bound to that person's account and to the visiting agent that received it. The visiting agent stores the token and sends it on each request. It never holds the person's password or the company's keys.
Two identities travel on an account request:
Authorization: Beareris the person and the account.CAP-From: agent:{id}is the visiting agent.
The owner answers however it chooses: its own MCP servers, a direct API, or a person. The caller does not learn which. Revoking the token stops that agent. Nothing else has to rotate.
Owner rules
An owner conforms if it:
- Returns only the fields above. Never tools, prompts, transcripts, internal ids, or credentials, in a field or in the reply.
- Answers each message from its text, its
context, the grant, and its own record of the thread. Never from another caller's messages. - Answers an account question only with a grant for that account. A name or an account number in
contextis not a grant. - Returns
202withLocationandRetry-Afterwhen it cannot finish within its wait. - Keeps its limits private. When it will not take more from a caller, it returns
429withRetry-After. - Keeps finished messages for at least 24 hours.
Caller rules
A caller conforms if it:
- Never calls the owner's tools, MCP servers, or APIs to do or check the work.
- Uses
replyonly whenstatusisdone. - Never invents a reply while a message is
working. It tells the person the owner is still answering. - Waits at least
Retry-Afterbetween reads. - Treats
failedas final for that message. It sends a new one only when something changed, such as the person logging in. - Sends a short brief in
context. Never the person's password or another company's data. - Sets
previous_message_idto the last message when the person is continuing the same conversation, and leaves it off when they start a new one.
Errors
{ "error": { "code": "rate_limited", "message": "Try again in a minute." } }
| Code | HTTP | When |
|---|---|---|
invalid_request |
400 | The body is not JSON, message is missing, or a header is malformed |
version_not_supported |
400 | CAP-Version is not implemented |
unauthorized |
401 | The bearer token is invalid, expired, or revoked |
forbidden |
403 | The token was issued to a different agent than CAP-From |
not_found |
404 | The message, or previous_message_id, does not exist, has expired, or is not visible to this caller |
conflict |
409 | previous_message_id is still working, or the thread is too long |
rate_limited |
429 | The owner will not take more from this caller now. Retry-After is set |
unavailable |
503 | The agent is not answering |
These are request errors. A message that was accepted and could not be answered is failed, with its own error.
Relationship to other protocols
| Layer | Protocol | Role |
|---|---|---|
| Tools and data | MCP | An agent reaching its own tools, inside one boundary |
| Agent integration | A2A | Agents that integrate with each other: agent cards, tasks, streaming |
| Personal agents | Personal Agent Protocol | A personal agent working with a company: sessions, sign-in, APIs, web, and the company agent |
| Public front door | CAP | Any agent asking a company's agent, from a text file and two endpoints |
CAP is the smallest of these on purpose: one file, one object, two endpoints, and common HTTP. It suits the public question that needs no session. An owner can serve CAP next to any of the others from the same agent. Each keeps its own discovery file (/.well-known/agent-card.json for A2A, /.well-known/poppy.json for Personal Agent Protocol), and agent.txt does not list them.
An owner can serve both A2A and CAP from the same agent. A CAP message maps onto an A2A task:
| CAP | A2A 1.0 |
|---|---|
| Send a message | SendMessage with returnImmediately: true |
| Read a message | GetTask |
working, done, failed |
TASK_STATE_WORKING, _COMPLETED, _FAILED |
reply |
A text part on the task's result |
previous_message_id |
Sending with the earlier task's contextId |
Version 1 ships as the /cap/v1 HTTP surface above.