Skip to content

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.

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.

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 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.

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.

SkillRoleAccessDoes
chatviewer (Viewer)ReadAsk in plain language. The assistant answers from the same tools; an action it proposes waits (input-required) until you reply { "confirm": true }.
place_orderoperator (Operator)WriteStart 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_attentionviewer (Viewer)ReadWhat 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_ordersviewer (Viewer)ReadSearch orders. Totals compare in USD. Returns the count and total of all matches plus up to `limit` rows.
get_orderviewer (Viewer)ReadOne order: status, what it waits on (with where each decision leads), steps run so far and recent activity.
assess_orderviewer (Viewer)ReadAsk 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_productsviewer (Viewer)ReadProducts with stock, reserved and available quantities and reorder points.
list_flowsviewer (Viewer)ReadAll flows and their published versions.
get_flowviewer (Viewer)ReadA flow’s saved draft: its steps, connections and validation problems.
propose_flow_changeoperator (Operator)ReadDraft 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_approvalapprover (Approver)WriteApprove or reject an approval an order is waiting on.
deliver_eventoperator (Operator)WriteDeliver a business event (e.g. payment.received) to the order waiting for it.
cancel_orderoperator (Operator)WriteCancel an order and undo what it did (release stock, void payment, cancel shipments).
publish_flowoperator (Operator)WritePublish a flow’s saved draft as a new version.
run_stock_reviewoperator (Operator)WriteRun 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.

An order’s status reads as a task state:

Order statusTask stateFinal
queuedTASK_STATE_WORKINGNo
runningTASK_STATE_WORKINGNo
waitingTASK_STATE_INPUT_REQUIREDNo
completedTASK_STATE_COMPLETEDYes
declinedTASK_STATE_REJECTEDYes
cancelledTASK_STATE_CANCELEDYes
failedTASK_STATE_FAILEDYes
rolled_backTASK_STATE_CANCELEDYes

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.

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.

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: SendMessage with configuration.taskPushNotificationConfig (url, optional token, optional authentication). It applies to the task that message creates.
  • With the config methods on an existing task: CreateTaskPushNotificationConfig, GetTaskPushNotificationConfig, ListTaskPushNotificationConfigs and DeleteTaskPushNotificationConfig, each with the taskId. A config without an id gets one assigned; creating with an existing id replaces 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>" }
}
}'

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.

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.

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.

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?" }]
} }
}'

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>.

  • 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; Authorization and 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, ListTasks and 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 Origin is not an http or https origin is refused with 403.