A2A: Simp Connect as an Agent2Agent server
Talk to Simp over the A2A v1.0 protocol: the signed agent card, the JSON-RPC endpoint, the three skills (ask a Simp user, tell a Simp user, have Simp act for a signed-in user), the task states, push notifications, errors and limits.
Simp speaks A2A v1.0 as a server. Your agent can ask a Simp user a question or tell them something, and the user answers with one tap; or, signed in as a Simp user, your agent can have Simp ask or tell someone for them, exactly as an MCP connection does.
Discovery#
| What | Where |
|---|---|
| Agent card (signed) | https://voice.simpmobile.com/.well-known/agent-card.json |
| Card signing keys (JWKS) | https://voice.simpmobile.com/.well-known/a2a-jwks.json |
| Endpoint (JSON-RPC 2.0) | https://voice.simpmobile.com/api/a2a/v1 |
The card is signed with ES256 as a detached JWS over its RFC 8785 canonical form (A2A section 8.4). Verify it before trusting it: the protected header's jku is on the card's own origin and kid names the key. Several signatures may appear during a key rotation; any one that verifies is enough. The card is served with an ETag and Cache-Control: public, max-age=300.
Every request carries A2A-Version: 1.0 (a header, or ?A2A-Version=1.0). A request without it is read as version 0.3, which Simp does not serve, and is refused with VersionNotSupportedError (-32009).
Two ways to authenticate#
| You are | Credential | Skills |
|---|---|---|
| An agent acting for someone else (a business, a service) | A remote agent key simp_a2a_..., issued by Simp. Send it as Authorization: Bearer <key>. | ask_simp_user, tell_simp_user |
| An agent acting for a Simp user | An access token from Simp's authorization server, obtained exactly as an MCP client does (see Authentication). | complete_task_for_user |
A signed-in agent gets the same permission modes and rights as an MCP connection: there is one delegation model for both protocols. No credential, or a bad one, is HTTP 401 with a WWW-Authenticate challenge.
Ask or tell a Simp user#
Address the user by their simp_id (s_ followed by 24 hex characters), which they share with you themselves. Simp never looks a user up by phone number, email, name or handle over A2A, and never tells you any of those.
{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"messageId": "8c1d0d6e-5f43-4a3b-9b4c-0a1f2e3d4c5b",
"role": "ROLE_USER",
"parts": [
{ "data": {
"skill": "ask_simp_user",
"recipient": { "simpId": "s_0123456789abcdef01234567" },
"question": { "text": "Does Thursday at 10 still work?", "options": [{ "id": "yes", "label": "Yes" }, { "id": "no", "label": "No" }] },
"respondBy": "2026-10-06T17:00:00.000Z"
},
"mediaType": "application/json" }
]
},
"configuration": { "returnImmediately": true }
}
}tell_simp_user takes "message": { "text": "..." } in place of question; the user acknowledges it with a tap. The rules on the question:
textis 1 to 200 characters;optionsare 2 to 6, each label up to 40 characters, or omit them and setfreeTextAllowed: true.- No phone numbers and no links, in the question or the options. A question that breaks a rule is refused with
-32602naming the rule, and nothing is delivered. respondByis between one hour and 14 days ahead. At that time an unanswered request ends.- The user sees your agent's registered name and the question, and nothing about you beyond that.
Your text is shown as you wrote it: nothing on the user's side writes or rewrites language. The user answers in the Simp app or on Simp's answer page.
Available for invited accounts only. A simp_id that does not exist, one whose user is not in the current pilot, a user who has turned your agent away, and a user who has had enough requests today all get the same answer: a task in TASK_STATE_REJECTED with the status message "This request was not accepted." The response is built the same way in every one of those cases, so it tells you nothing about whether the id exists.
Have Simp act for a signed-in user#
The data part is skill plus exactly the input of the MCP create_task tool (see Tools): the same fields, the same rules, the same checks.
{ "skill": "complete_task_for_user",
"kind": "get_answer",
"objective": "Ask Sam whether Thursday still works",
"participants": [{ "name": "Sam", "phone": "+447700900123" }],
"respond_by": "2026-10-06T17:00:00+01:00" }kind is call, get_answer, send_update or follow_up. Request the token with resource=https://voice.simpmobile.com/api/a2a/v1 on both the authorization and the token request: Simp binds every token to one resource, so a token minted for the MCP endpoint is refused here, and the reverse. The protected resource document is at https://voice.simpmobile.com/.well-known/oauth-protected-resource/api/a2a/v1.
A participant that is a business may carry agent_domain beside its phone (for example "agent_domain": "bellanapoli.co.uk"): where the user's account allows it, Simp fetches that domain's agent card, verifies its signature, and asks the business's own A2A agent instead of calling. Only signed cards whose jku is on the same origin are used, and only cards that ask for no credential; otherwise the task goes the usual way. Simp sends that agent exactly the question, its options, the deadline, the simp's name and the label the user's sharing settings allow, and nothing else.
The connection's permission mode decides what happens next, exactly as for MCP. Under ask_every_time (the default) nothing happens until the user approves the task in the Simp app: the task stays TASK_STATE_SUBMITTED and its status message carries the approval link to show them. Under notify_only, or rights_based with matching rights, it goes ahead after a short cancel window and the user gets a heads-up push. The user's routing applies: for most users a task that asks or tells someone goes by a phone call from their own Simp number, only between 08:00 and 21:00 in the recipient's local time.
Following a task#
Simp's tasks wait for a person, so send returnImmediately: true and then poll GetTask, or register a push notification config. A blocking SendMessage waits at most 8 seconds and then returns the task as it stands, usually TASK_STATE_WORKING. That is a documented departure from A2A section 3.2.2, which asks a blocking request to wait for a terminal state.
| State | Meaning |
|---|---|
TASK_STATE_REJECTED | Not accepted (see above). Nothing was delivered. |
TASK_STATE_SUBMITTED | Accepted, not yet delivered, or waiting for the user's approval (the status message has the link). |
TASK_STATE_WORKING | Delivered, or on its way; waiting for an answer. |
TASK_STATE_COMPLETED | Answered, partly answered or declined; for a call, done (or a voicemail left). The outcome is the result artifact. |
TASK_STATE_FAILED | No answer by the deadline, or the task could not be completed. A result artifact is present when one was published. |
TASK_STATE_CANCELED | Canceled by you (CancelTask) or by the user. |
Simp never uses TASK_STATE_INPUT_REQUIRED or TASK_STATE_AUTH_REQUIRED, and a task takes one message: a second SendMessage naming its taskId is UnsupportedOperationError. Resending the same messageId returns the same task.
The result artifact has a data part (application/json) and a text part with the summary:
{ "status": "answered",
"summary": "They said Yes.",
"participants": [{ "participantId": "...", "state": "answered", "answer": { "optionId": "yes" }, "route": "native", "answeredAt": "..." }],
"derived": { "answered": 1, "total": 1 },
"publishedAt": "..." }Before an answer a participant is only ever pending or delivered; how a request travelled appears only once it was answered.
Push notifications#
Register a webhook inline (configuration.taskPushNotificationConfig) or with CreateTaskPushNotificationConfig; at most three per task. When the task reaches a terminal state Simp POSTs { "task": Task } to it. The request carries:
Content-Type: application/a2a+json;Authorization, built from the config'sauthenticationscheme and value;X-A2A-Notification-Tokenwhen you settoken.
Check both on receipt and match the task id to one you sent.
The URL must be https:// on port 443 and resolve only to public addresses; it is checked when you register it and again at every delivery, redirects are not followed, and each request times out after 10 seconds. A failed delivery is retried after 1, 5, 30 minutes, then dropped; GetTask always has the current state.
Methods#
| Method | Supported |
|---|---|
SendMessage, GetTask, ListTasks, CancelTask | Yes. ListTasks covers your 200 most recent tasks, newest status first. |
Create/Get/List/DeleteTaskPushNotificationConfig | Yes. |
SendStreamingMessage, SubscribeToTask | No (UnsupportedOperationError); the card says streaming: false. |
GetExtendedAgentCard | No (UnsupportedOperationError); the card says extendedAgentCard: false. |
Only the JSON-RPC binding is offered. A task you cannot see, yours or nobody's, is TaskNotFoundError (-32001).
Errors and limits#
| Condition | Answer |
|---|---|
| No credential, or a bad one | HTTP 401 |
| The connection lacks the scope, or the user's plan has no agent integrations | HTTP 403 |
| Your agent's daily requests are spent (200 a day; every request counts, accepted or not) | HTTP 429 |
Missing or unsupported A2A-Version | -32009 |
| Bad parameters, a refused question | -32602, with google.rpc.BadRequest field violations |
A file part (raw or url) | -32005 ContentTypeNotSupportedError |
| Cancel a finished task | -32002 TaskNotCancelableError (canceling a canceled task returns it) |
A user receives at most 10 requests a day across all agents and 3 from yours; past that, requests are rejected as described above. Request bodies are capped at 64 KB.
Getting a key#
Remote agent keys are issued by Simp during the pilot. Each one names your agent as users will see it, carries its own daily limit, and can be revoked. Store it as a secret; Simp keeps only a hash and cannot show it again.
