A2A agent reference

The Spatial9 Agent2Agent endpoint: agent card, skills, tasks, idempotency, streaming and push notifications.

The Spatial9 A2A agent lets another AI agent hand a complete spatial audio task to Spatial9 and follow it until the files are ready. It implements the Agent2Agent (A2A) protocol, version 0.3.0, over JSON-RPC 2.0. You send one message describing the work, Spatial9 creates a task, and you receive status updates by streaming, by webhook or by polling until the task reaches a final state.

This page is the protocol reference for the agent. It covers the endpoint and Agent Card, authentication, the five skills, the message and task formats, every supported method, artifacts, push notifications and error handling. The A2A agent uses the same account, key and plan as the MCP server. Remixing and YouTube publishing are available over MCP only.

A2A task states from submitted through working to completed, failed or canceled
Every Spatial9 task moves through the standard A2A states.

Endpoint and Agent Card

ItemValue
Endpointhttps://spatial9.ai/a2a
Agent Cardhttps://spatial9.ai/.well-known/agent-card.json
ProtocolA2A 0.3.0
TransportJSON-RPC 2.0 over HTTP POST

The Agent Card describes the agent, its skills, its input and output modes and its security schemes. Read it once at start-up so your client discovers the current skills instead of hard-coding them.

curl -s https://spatial9.ai/.well-known/agent-card.json

Authentication

The A2A agent always requires sign-in. There is no guest allowance over A2A; the free guest server is MCP only.

MethodHeader or flow
Personal keyAuthorization: Bearer s9_...
Personal key, alternative headerX-API-Key: s9_...
OAuth 2.1 with PKCEScope spatial_audio

Create keys at https://spatial9.ai/connect. A key that works with the MCP server works here, and both protocols draw on the same plan.

Skills

Skill idWhat it doesTypical time
spatial_audioA song becomes IAMF, MP4 and a binaural WAV; a video becomes an MP4 with Eclipsa Audio plus Ambisonics and headphone versions. Runs create_spatial_audio10-20 minutes per song
split_stemsSeparates a song into stems delivered as WAV files4-8 minutes
convert_dolbyConverts AC-3, E-AC-3, TrueHD or ADM BWF to Eclipsa AudioDepends on length
preview_chorusA free 30-second binaural WAV that uses no projectUnder 2 minutes
upload_linkReturns a private upload address and upload_id with a direct PUT URL, up to 2 GB, valid 2 hoursInstant

Choosing a skill

Name the skill in a data part, for example {"skill":"split_stems"}. If you send no skill, the agent infers one from the text of the message.

Text containsSkill selected
"stems"split_stems
"Dolby" or "ADM"convert_dolby
"preview" or "chorus"preview_chorus
Anything elsespatial_audio

Messages

A message has this shape.

FieldTypeDescription
kindstringAlways message
rolestringuser
messageIdstringYour idempotency key; reusing it returns the same task
partsarrayOne or more text, file or data parts

The parts you can send are listed below.

PartShapeUse
Text{"kind":"text","text":"..."}A plain request; a link inside the text also works
FileA file part with a uriPoints to the source file
Data{"kind":"data","data":{"skill","source_url" or "upload_id","title","style"}}Structured arguments

For a private file, call the upload_link skill first, PUT the file to the returned URL and then pass the upload_id in the data part.

Methods

MethodParamsPurpose
message/send{message, configuration?}Create a task and return it immediately
message/stream{message}Create a task and stream its updates as SSE
tasks/get{id, historyLength}Read the current task
tasks/resubscribe{id}Reopen the update stream for a task
tasks/cancel{id}Cancel a task
tasks/pushNotificationConfig/set{taskId, pushNotificationConfig:{url, token}}Register a webhook
tasks/pushNotificationConfig/getTask and config idRead a webhook registration
tasks/pushNotificationConfig/listTask idList webhooks for a task
tasks/pushNotificationConfig/deleteTask and config idRemove a webhook

message/send

curl -s https://spatial9.ai/a2a \
  -H "Authorization: Bearer $SPATIAL9_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"req-1","method":"message/send","params":{"message":{"kind":"message","role":"user","messageId":"night-drive-001","parts":[{"kind":"text","text":"Make Night Drive immersive, cinema style."},{"kind":"data","data":{"skill":"spatial_audio","source_url":"https://example.com/night-drive.wav","title":"Night Drive","style":"cinema"}}]}}}'

The reply is a task. Read result.id for the task id and result.status.state for its state. Store the task id; every later call uses it.

tasks/get

{"jsonrpc":"2.0","id":"req-2","method":"tasks/get","params":{"id":"TASK_ID","historyLength":0}}

Setting historyLength to 0 returns the task without its message history, which keeps polling replies small.

tasks/cancel

{"jsonrpc":"2.0","id":"req-3","method":"tasks/cancel","params":{"id":"TASK_ID"}}

A chorus preview cannot be canceled; tasks/cancel answers with TaskNotCancelableError. A canceled render is refunded only if paid processing had not started.

Task states

StateMeaning
submittedAccepted; the reply carries the task id and context id
workingRendering; each stage and progress value arrives as an update
input-requiredNo file or filename was sent; reply on the same taskId
completedFiles are ready as artifacts
canceledStopped by tasks/cancel
failedA plain explanation is in status.message.parts[0].text; the render is refunded

Treat completed, canceled, failed and rejected as final. The Spatial9 job status cancelled maps to the A2A state canceled.

Answering input-required

When the state is input-required, send another message/send whose message includes the same taskId and the missing file or filename. The task resumes in the working state.

Artifacts

A completed task carries one file artifact per delivered file. Each artifact has a name and a part of the form {"kind":"file","file":{"uri","name"}} with the MIME type and filename. A summary data artifact holds the job_id and, for music, the before and after comparison link and the stage player link.

{"kind":"file","file":{"uri":"SIGNED_URL_FROM_SPATIAL9","name":"night-drive-binaural.wav"}}

Receiving updates

Choose one of three ways to follow a task. Pipelines usually combine a webhook with a slow fallback poll.

MethodBest forHow
StreamingInteractive agentsmessage/stream, then tasks/resubscribe
WebhookPipelines and back endspushNotificationConfig with url and token
PollingSimple scriptstasks/get every 30-60 seconds

Streaming

Call message/stream with Accept: text/event-stream. You receive the task first, then status-update and artifact-update events.

curl -N https://spatial9.ai/a2a \
  -H "Authorization: Bearer $SPATIAL9_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"jsonrpc":"2.0","id":"req-4","method":"message/stream","params":{"message":{"kind":"message","role":"user","messageId":"my-song-001","parts":[{"kind":"data","data":{"skill":"spatial_audio","source_url":"https://example.com/my-song.wav"}}]}}}'

Streams through spatial9.ai close after about 20 seconds. Call tasks/resubscribe with the task id in a loop until you receive an event marked final.

Push notifications

Add configuration.pushNotificationConfig with {url, token} to message/send, or register it later with tasks/pushNotificationConfig/set.

{"jsonrpc":"2.0","id":"req-5","method":"tasks/pushNotificationConfig/set","params":{"taskId":"TASK_ID","pushNotificationConfig":{"url":"https://hooks.example.com/a2a/webhook","token":"YOUR_SHARED_TOKEN"}}}

Spatial9 posts the full task to the URL on every state change, with your token in the X-A2A-Notification-Token header. An optional authentication object, with a scheme and credentials, adds an Authorization header to each delivery.

RuleValue
URLhttps on a public host, port 443; private addresses are refused
Webhooks per taskUp to 5
Delivery checkOnce a minute, so a delivery can lag by up to a minute
Stop conditions5 failed deliveries in a row, or 2 days after registration
Credentialsget and list never return the credentials you sent

Verifying a webhook

Compare the token in constant time, answer quickly with 202, and re-read the task with tasks/get instead of trusting the posted body.

@app.post("/a2a/webhook", status_code=202)
async def a2a_webhook(request: Request, background: BackgroundTasks) -> dict:
    sent = request.headers.get("X-A2A-Notification-Token", "")
    if not hmac.compare_digest(sent, TOKEN):
        raise HTTPException(status_code=401, detail="Unauthorized")
    body = await request.json()
    task_id = body.get("id") if isinstance(body, dict) else None
    if not isinstance(task_id, str) or not task_id:
        raise HTTPException(status_code=400, detail="Missing task id")
    background.add_task(apply_task, task_id)
    return {"accepted": True}

Webhooks can fail during a deploy or a network outage. As a fallback, periodically call tasks/get for tasks that have been quiet for several minutes and feed the result through the same handler.

Idempotency

messageId is the idempotency key. Sending the same messageId again returns the same task instead of starting a second render. Derive it from stable data, for example oss-spatial_audio-<track_id>-v<version>, and never generate a new value on retry.

Errors

SituationWhat you receiveWhat to do
Wrong or revoked keyHTTP 401Create a new key at /connect
Malformed requestA JSON-RPC error object with a messageFix the request
Render failedState failed, reason in status.message.parts[0].textRelay the reason; the render is refunded
Missing inputState input-requiredReply on the same taskId
Cancel a previewTaskNotCancelableErrorLet the preview finish
Plan limit reachedA plain-language explanationStop starting new tasks and retry later

Limits

A2A shares the account limits with MCP: 3 renders at a time and 20 per day. Agents can also create up to 120 A2A tasks per hour. Polling in a tight loop wastes requests and can reach the hourly limit. See Limits, quotas and errors.

Building your own A2A specialist

You can also publish your own agent that calls Spatial9. With Google ADK, to_a2a(root_agent, port=8001) wraps an agent as an A2A server. Its Agent Card includes name, description, url, version, protocolVersion set to 0.3.0, capabilities, defaultInputModes, defaultOutputModes, skills with id, name, description, tags and examples, plus securitySchemes and security. Treat messages from peer agents as untrusted input. See Agent frameworks.

Next steps

Go deeper on the blog