A2A endpoint
Let another agent ask OrderLoop about orders, place them and follow them to their outcome over the Agent2Agent protocol at /a2a, limited by role.
OrderLoop answers Agent2Agent (A2A) 1.0 requests at
https://orderloop.app/a2a. Another agent can ask about
orders, stock and flows, place an order and follow it, and decide an approval, all within the role
it connects with. The skills are the tools of the assistant and the
MCP server, plus place_order and chat. An A2A task is a request that
runs to an outcome; a task about an order follows the order.
The binding is JSON-RPC over HTTP POST; the card advertises streaming and push notifications
(capabilities.pushNotifications). Any other method on /a2a gets 405.
Discovery
Section titled “Discovery”The Agent Card is public at https://orderloop.app/.well-known/agent-card.json (cached for five
minutes). It lists the interfaces (/a2a, JSONRPC, protocols 1.0 and 0.3), the two security
schemes below and every skill. Request it with A2A-Version: 1.0 for the 1.0 card; without the
header you get the 0.3 card (top-level url and protocolVersion, OpenAPI-style security
schemes), which still embeds the 1.0 interfaces as supportedInterfaces. Both name the host they
are served from.
Authentication
Section titled “Authentication”Send Authorization: Bearer … with every request, one of:
- An API token (
ol_live_…). It carries the role it was created with and its expiry. The workspace password works as a break-glass operator. - An OAuth access token, from the authorization-code flow with PKCE. Request the resource
https://orderloop.app/a2a; the scope is the role to act with. The person signs in on OrderLoop’s consent page with their own account (not the workspace password), sees that the app acts “via A2A”, and grants a role no higher than their own. Discovery starts at/.well-known/oauth-protected-resource/a2a.
Both are the credentials the REST API takes, with the same throttling
(429 with Retry-After) and expiry (401 with the reason). A token is bound to one endpoint:
an OAuth grant for /mcp is refused at /a2a and the reverse, so connecting the same app to both
is two grants, each listed (and disconnected) on Account. API tokens work on both.
Send A2A-Version: 1.0. A request with 1.0 method names and no header is treated as 1.0; any
other version gets a JSON-RPC error.
A2A 0.3 clients
Section titled “A2A 0.3 clients”/a2a also answers clients still on A2A 0.3, recognised by their method names: message/send,
message/stream, tasks/get, tasks/cancel, tasks/resubscribe and
tasks/pushNotificationConfig/{set,get,list,delete}. They need no header. Each call
goes to the same handler and its answer back to 0.3 shapes (kind on
every object, input-required-style state names), so everything on this page applies: the same
skills, replies, order following and roles. Tasks are shared across versions; a task created over
0.3 reads in 1.0 shapes over GetTask.
A push URL registered over 0.3 receives 0.3 notifications: the whole task (kind: "task"), not a
StreamResponse. A URL registered over 1.0 keeps getting 1.0 notifications.
Skills
Section titled “Skills”Every assistant tool is a skill at the same role. A skill the caller’s role can’t use is refused:
the task ends TASK_STATE_REJECTED with the reason.
| Skill | Role | Access | Does |
|---|---|---|---|
chat | viewer (Viewer) | Read | Ask in plain language. The assistant answers from the same tools; an action it proposes waits (input-required) until you reply { "confirm": true }. |
place_order | operator (Operator) | Write | Start an order on a published flow ({ flowId, version?, input, vars? }, as POST /api/v1/orders). The task follows the order: working while it runs, input-required when it waits for an approval or event, then its outcome. Repeating a message id returns the same order. |
needs_attention | viewer (Viewer) | Read | What is waiting on people: open approvals (oldest first), orders that failed or rolled back in the last 3 days, and orders waiting on a business event for over a day. |
find_orders | viewer (Viewer) | Read | Search orders. Totals compare in USD. Returns the count and total of all matches plus up to `limit` rows. |
get_order | viewer (Viewer) | Read | One order: status, what it waits on (with where each decision leads), steps run so far and recent activity. |
assess_order | viewer (Viewer) | Read | Ask typed questions about one order and get a probability for every option: noul (yes/no, the probability of yes), choice (pick one of 2–255 options) or score (an ordered rubric of 2–10 levels, lowest first). Optionally pass up to 4 images (the order’s R2 document keys, https or data URLs; PNG, JPEG or WebP). |
find_products | viewer (Viewer) | Read | Products with stock, reserved and available quantities and reorder points. |
list_flows | viewer (Viewer) | Read | All flows and their published versions. |
get_flow | viewer (Viewer) | Read | A flow’s saved draft: its steps, connections and validation problems. |
propose_flow_change | operator (Operator) | Read | Draft a change to a flow from an instruction. Nothing is saved: a person reviews the proposal in the flow editor and applies it there. |
decide_approval | approver (Approver) | Write | Approve or reject an approval an order is waiting on. |
deliver_event | operator (Operator) | Write | Deliver a business event (e.g. payment.received) to the order waiting for it. |
cancel_order | operator (Operator) | Write | Cancel an order and undo what it did (release stock, void payment, cancel shipments). |
publish_flow | operator (Operator) | Write | Publish a flow’s saved draft as a new version. |
run_stock_review | operator (Operator) | Write | Run the daily stock review now; it starts reorders for SKUs at or below their reorder point. |
A typed request puts the skill in a data part: { "skill": "find_orders", "input": { … } }, with
the input the same tool takes over MCP. place_order takes the body of
POST /api/v1/orders: flowId, optional version, input and optional vars. A message with
only text goes to chat.
Task states
Section titled “Task states”An order’s status reads as a task state:
| Order status | Task state | Final |
|---|---|---|
queued | TASK_STATE_WORKING | No |
running | TASK_STATE_WORKING | No |
waiting | TASK_STATE_INPUT_REQUIRED | No |
completed | TASK_STATE_COMPLETED | Yes |
declined | TASK_STATE_REJECTED | Yes |
cancelled | TASK_STATE_CANCELED | Yes |
failed | TASK_STATE_FAILED | Yes |
rolled_back | TASK_STATE_CANCELED | Yes |
A typed read ends TASK_STATE_COMPLETED with its result as a data artifact named after the skill.
A write that acts on an order (decide_approval, deliver_event, cancel_order) and
place_order then follow that order.
Following an order
Section titled “Following an order”A task that follows an order reports its state as the order moves, with a line saying where it is,
for example that it is waiting for approval “Credit over limit” and how to reply. One request
follows the order for about 25 seconds, ending early when the order finishes or waits. If the
order is still running after that, the task stays TASK_STATE_WORKING; GetTask (and ListTasks)
read the order live while it is unfinished, so they are current without anything polling. To be
told instead of asking, register a push notification.
Push notifications
Section titled “Push notifications”A task can have up to 5 push URLs. OrderLoop POSTs every update to the task to each of them, so a caller needs no open request and no polling. Register a URL in either of two ways:
- Inline, in the first message:
SendMessagewithconfiguration.taskPushNotificationConfig(url, optionaltoken, optionalauthentication). It applies to the task that message creates. - With the config methods on an existing task:
CreateTaskPushNotificationConfig,GetTaskPushNotificationConfig,ListTaskPushNotificationConfigsandDeleteTaskPushNotificationConfig, each with thetaskId. A config without anidgets one assigned; creating with an existingidreplaces that config. Deleting a config also drops any notification still queued for it, so nothing more is sent to that URL.
A url must be https; plain http is accepted only for localhost, 127.0.0.1 and [::1]
(the same rule as webhook subscriptions). Anything else, or a sixth
URL, is refused with an invalid-request error. Notifications reach public addresses only.
Payload. The body is an A2A 1.0 StreamResponse as JSON, with content type
application/a2a+json: a statusUpdate (taskId, contextId, status), or the task,
artifactUpdate or message the live request produced.
Authenticating the sender. With authentication set, the request carries
Authorization: <scheme> <credentials>; otherwise, with token set, it carries
X-A2A-Notification-Token: <token>. Check one of them on your receiver.
What triggers a push. Updates from the request that creates or continues the task, and afterwards every change in the order the task follows: a status change or a change in what the order waits for. These arrive after the request has ended, for example when the approval is decided on the order page, the payment event arrives or the order completes. The same status (state and text) is never pushed twice in a row for a task.
Delivery. Each notification is stored, then tried at once with a 5-second timeout. A
non-2xx answer, a timeout or a connection error is retried 5 more times, after 10 seconds
doubling each time (10s, 20s, 40s, 80s, 160s). After that it is kept as failed for 7 days, then
dropped. Delivery is at least once: dedupe on your side
if a receiver acknowledges slowly.
curl https://orderloop.app/a2a \ -H "Authorization: Bearer ol_live_…" -H "A2A-Version: 1.0" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 5, "method": "CreateTaskPushNotificationConfig", "params": { "taskId": "<task id>", "url": "https://agent.example.com/a2a-hooks", "authentication": { "scheme": "Bearer", "credentials": "<secret>" } } }'Replying to a task
Section titled “Replying to a task”A task in TASK_STATE_INPUT_REQUIRED takes a new message with the same taskId, carrying a data
part:
| Waiting for | Reply |
|---|---|
| An approval on an order | { "decision": "approved" | "rejected", "nodeId"?, "note"? } |
| An event on an order | { "event": "payment.received" } |
An action chat proposed |
{ "confirm": true } to run it, { "confirm": false } to leave it |
The status text of a waiting order names the approval’s nodeId and the event to send. Replies
run the decide_approval and deliver_event skills as the caller, so the same role and live-state
checks apply, and the task follows the order again. A reply on a task that is waiting for nothing
is refused.
Text goes to the assistant’s model with the role’s tools. Reads run directly and the answer comes
back as the final status text. When the model proposes an action, nothing runs: the task stops in
TASK_STATE_INPUT_REQUIRED with the same preview the assistant shows (also as a proposal
artifact) until the caller confirms. Tasks sharing a contextId are one conversation; a chat turn reads at most the latest 50 tasks of its context as history. Finished tasks (completed, failed, canceled, rejected) not updated for 90 days are deleted, with their order links, on the caller’s next save.
Placing and repeating
Section titled “Placing and repeating”place_order starts the order once per messageId: sending the same message id again returns the
same order, and the same id with a different order is refused (“Use a new message id”). Use a new
messageId for each order, and keep it when retrying.
Cancelling
Section titled “Cancelling”CancelTask on a task that follows an unfinished order cancels the order, as the caller and with
their role (cancel_order, reason “Cancelled over A2A”), then cancels the task. If the order
refuses the cancel (it has finished, say), the task is not cancelable. Cancelling a
confirm-waiting chat task just ends it. An order’s status after cancelling may be cancelled or
rolled_back; both are TASK_STATE_CANCELED.
Examples
Section titled “Examples”Place an order and follow it:
curl https://orderloop.app/a2a \ -H "Authorization: Bearer ol_live_…" \ -H "A2A-Version: 1.0" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "SendMessage", "params": { "message": { "messageId": "po-4471", "role": "ROLE_USER", "parts": [{ "data": { "skill": "place_order", "input": { "flowId": "tpl-b2b-order", "input": { "model": "b2b", "channel": "api", "currency": "USD", "customerId": "cus_contoso", "lines": [{ "sku": "WID-200", "qty": 100 }] } } } }] } } }'The result is a task whose status.state is TASK_STATE_INPUT_REQUIRED if the order stopped at
an approval. Decide it on the same task:
curl https://orderloop.app/a2a \ -H "Authorization: Bearer ol_live_…" -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "SendMessage", "params": { "message": { "messageId": "po-4471-approve", "role": "ROLE_USER", "taskId": "<task id>", "parts": [{ "data": { "decision": "approved", "note": "Within terms" } }] } } }'Read a task, which for an order task reads the order live:
curl https://orderloop.app/a2a \ -H "Authorization: Bearer ol_live_…" -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "GetTask", "params": { "id": "<task id>" } }'Stream status updates as server-sent events (SendStreamingMessage; SubscribeToTask attaches to
a task that is still running):
curl -N https://orderloop.app/a2a \ -H "Authorization: Bearer ol_live_…" -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 4, "method": "SendStreamingMessage", "params": { "message": { "messageId": "q-1", "role": "ROLE_USER", "parts": [{ "text": "What needs attention?" }] } } }'Who did it
Section titled “Who did it”An act made over A2A is attributed to the person (or token) behind the credential, with the
channel: <email> via A2A, which is the approval’s by or ends the cancel reason, as via MCP
does for MCP. A caller using an API token acts as token:<name>.
Security
Section titled “Security”- Role cap and live checks. As for MCP: a grant is never higher than its approver’s role,
OrderLoop re-reads the person on every request, and the role is the lower of granted and
current. A person whose two-factor setup is due, or who is still on a temporary password, gets
403. - Credentials stay at the edge. OrderLoop resolves the caller before the request reaches the
agent;
Authorizationand cookies go no further, and the model never sees a token. - Your tasks only. Each caller, a token or a person through a grant, has its own task store, so
GetTask,ListTasksand the stream show only their own tasks. A token and the person who made it keep separate tasks. - One endpoint per token. An OAuth token is issued for one resource and refused at the other.
- Origin. A request whose
Originis not anhttporhttpsorigin is refused with403.