> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Bridge native-chat conversations to an A2A agent

> Pull real customer chat from a native_chat channel into an external A2A agent, draft a reply with an LLM, and ship it back over the native-chat write leg.

# Bridge native-chat conversations to an A2A agent

This guide wires the dashboard's native chat widget to an A2A agent that runs outside Orbit. The agent receives a push event when a customer sends a message, reads the conversation, drafts a response with an LLM, and writes the reply back to the same native-chat thread.

For the console side of the widget, see the [Native Chat channel configuration guide](/guides/native-chat-widget-channel-config). For the federation handshake, see [Set up A2A federation between tenants](/guides/a2a-federation-setup) and [A2A push notifications](/guides/a2a-push-notifications).

## 1. When to use native chat vs. A2A

Both surfaces can host an AI agent, but they differ in who owns the runtime.

| Use the dashboard native-chat widget when... | Use an A2A agent when... |
| - | - |
| Your operators work in the Orbit omnichannel inbox. | Your agent runs in your own worker or tenant. |
| You want AI-agent fallback + human handoff inside one UI. | The agent needs custom memory, tools, or compliance controls you host yourself. |
| The visitor opens chat on your site via the Web SDK. | The surface is your own app, bot, or orchestrator and you only need the message leg from Orbit. |

You can also combine them: the widget captures the conversation in Orbit, and an A2A agent subscribed to push events handles the reply logic remotely.

## 2. Register the A2A agent and subscribe to pushes

### 2.1 Register the agent

Follow [Set up A2A federation between tenants](/guides/a2a-federation-setup) to register your external agent as a peer. The short version:

1. In Orbit, set your A2A agent's discovery mode to `public` or `tenant`.
2. Copy its AgentCard share URL from **Agents → {agent} → A2A → Copy share URL**.
3. On your worker side, register that URL as a peer.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/agents/agent_abc/a2a/peers \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"peer_url": "https://your-worker.example.com/.well-known/agent.json"}'
```

### 2.2 Subscribe to push events

When your worker sends a task to the Orbit agent, attach a `pushNotification` block so every lifecycle update is POSTed to your worker. See [A2A push notifications](/guides/a2a-push-notifications) for the full contract.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/agents/agent_abc/a2a/tasks?tenant=tenant_abc \
  -H "Content-Type: application/json" \
  -H "X-A2A-Signature: t=<unix_seconds>,v1=<hmac_hex>" \
  -d '{
    "skill": "native_chat_reply",
    "message": {
      "role": "user",
      "parts": [{"kind": "text", "text": "ping"}]
    },
    "pushNotification": {
      "url": "https://your-worker.example.com/agents/a2a/push-notifications",
      "token": "your-callback-bearer-token"
    }
  }'
```

Orbit POSTs a JSON-RPC envelope to your callback on every task-state change:

```http theme={null}
POST /agents/a2a/push-notifications HTTP/1.1
Content-Type: application/json
Authorization: Bearer your-callback-bearer-token
X-Orbit-A2A-Push-Signature: <detached Ed25519 JWS>

{
  "jsonrpc": "2.0",
  "method": "taskStateUpdate",
  "params": {
    "taskId": "agentA2aTask_9f4c...",
    "status": "completed",
    "output": { "...": "..." }
  }
}
```

Verify the signature before acting on the payload. Orbit signs with `X-Orbit-A2A-Push-Signature` (Ed25519 JWS) when a signing key is provisioned, and falls back to `X-A2A-Signature` (HMAC-SHA256) otherwise.

## 3. Customer-side worker: receive, draft, and reply

### 3.1 Read the conversation

The push payload does not include the full conversation. Use the task id or the metadata you passed at send time to look up the source `conversation_id`, then call the Orbit API to fetch recent messages.

```bash theme={null}
curl -G "https://api.orbit.devotel.io/api/v1/conversations/conv_123" \
  -H "X-API-Key: dv_live_sk_..."
```

Keep the `conversation_id` from the original send. You can thread it through the task `metadata` field:

```json theme={null}
{
  "skill": "native_chat_reply",
  "message": { "role": "user", "parts": [{"kind": "text", "text": "..."}] },
  "metadata": { "conversation_id": "conv_123" },
  "pushNotification": { "url": "...", "token": "..." }
}
```

### 3.2 Send the reply back over native chat

Post the drafted reply to the conversation reply endpoint. The channel override must be `web_chat` (the API value for the dashboard's native-chat widget).

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/conversations/conv_123/reply" \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Thanks for your order. It left the warehouse this morning and should arrive Thursday.",
    "channel": "web_chat"
  }'
```

The reply endpoint returns `202` and the message is delivered to the visitor's widget on the next poll. The `web_chat` channel is loopback delivery: persistence in the messages table is the delivery event, so there is no carrier DLR.

## 4. Failure handling

### 4.1 Delivery retries

A2A push delivery is best-effort and does not retry. Your worker must tolerate missed callbacks: poll `GET /api/v1/agents/{agentId}/a2a/tasks/{taskId}` after a silence window to catch up.

For the native-chat reply itself, handle these synchronous errors:

| Error | Likely cause | Fix |
| - | - | - |
| `400 MISSING_IDENTITY` | The conversation has no visitor session id, email, or phone on file. | Ensure the widget session is active or the visitor has identified themselves. |
| `422 CHANNEL_NOT_REPLYABLE` | An inbox-only label was selected (for example `agent` or `video`). | Use `web_chat` as the reply channel override. |
| `422 OUTSIDE_SESSION_WINDOW` | Not applicable to `web_chat`. | N/A. |

### 4.2 Dedupe on event id

A2A push callbacks may arrive more than once during network blips. Store the `taskId`/`status` pair you have already processed and return `200` early for duplicates. Do not replay the native-chat reply for the same task transition.

### 4.3 Replay from the DLQ

If your worker is down when a push arrives, the task state is still recorded on the Orbit side. When your worker recovers:

1. List recent tasks: `GET /api/v1/agents/{agentId}/a2a/tasks`.
2. Filter for tasks your worker owns and has not yet processed.
3. Re-fetch the conversation, draft the reply, and call `POST /conversations/{id}/reply` with `channel: "web_chat"`.

There is no separate A2A DLQ to drain — the task ledger is the source of truth.

## 5. E2E code sample: minimal Node worker

This sample receives an A2A push, reads the conversation, drafts a response with an LLM, and writes it back to native chat. It skips signature verification for brevity; production code must verify `X-Orbit-A2A-Push-Signature` or `X-A2A-Signature`.

```js theme={null}
import express from "express";

const app = express();
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));

const ORBIT_API_KEY = process.env.ORBIT_API_KEY;
const ORBIT_BASE = "https://api.orbit.devotel.io";
const processed = new Set();

async function orbit(method, path, body) {
  const res = await fetch(`${ORBIT_BASE}${path}`, {
    method,
    headers: {
      "X-API-Key": ORBIT_API_KEY,
      "Content-Type": "application/json",
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!res.ok) throw new Error(`Orbit ${method} ${path} failed: ${res.status}`);
  return res.json();
}

async function draftReply(messages) {
  // Replace with your own LLM call. Input is the conversation history
  // from GET /conversations/{id} as an array of { direction, body } rows.
  const prompt = messages
    .map((m) => `${m.direction}: ${m.body}`)
    .join("\n");
  const response = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "gpt-4o-mini",
      messages: [
        { role: "system", content: "You are a concise support agent." },
        { role: "user", content: prompt },
      ],
    }),
  });
  const json = await response.json();
  return json.choices[0].message.content;
}

app.post("/agents/a2a/push-notifications", async (req, res) => {
  // TODO: verify X-Orbit-A2A-Push-Signature or X-A2A-Signature here.
  const { taskId, status, output } = req.body.params ?? {};
  if (!taskId || status !== "completed") {
    return res.sendStatus(200);
  }
  if (processed.has(taskId)) {
    return res.sendStatus(200);
  }

  const conversationId = output?.conversation_id ?? req.body.params?.metadata?.conversation_id;
  if (!conversationId) {
    return res.sendStatus(200);
  }

  const conv = await orbit("GET", `/api/v1/conversations/${conversationId}`);
  const replyText = await draftReply(conv.data.messages ?? []);

  await orbit("POST", `/api/v1/conversations/${conversationId}/reply`, {
    body: replyText,
    channel: "web_chat",
  });

  processed.add(taskId);
  res.sendStatus(200);
});

app.listen(3000, () => console.log("A2A native-chat bridge listening on :3000"));
```

Key points in the sample:

* The push callback is verified before use in production.
* `processed` dedupes on `taskId` so a retry does not double-reply.
* The reply is sent with `channel: "web_chat"`, which is the API value for the dashboard native-chat channel.
* If the LLM or reply call fails, the worker returns a non-2xx status and relies on polling the task ledger on recovery.

## 6. Dashboard wiring

In the dashboard:

1. Enable native chat under **Settings → Channels → Native Chat**. See [Native Chat channel configuration](/guides/native-chat-widget-channel-config).
2. Create an AI agent or use an existing one.
3. Open **Agents → {agent} → A2A**, set discovery to `public` or `tenant`, and copy the share URL.
4. Register the share URL as a peer from your worker.
5. Send tasks from your worker with `pushNotification.url` pointing at your callback endpoint.

## 7. Security and compliance notes

* Sign and verify A2A callbacks. An unverified push endpoint is a write primitive into your agent.
* Keep `conversation_id` scoped to the tenant that owns it. Your worker should not accept a conversation id from an unauthenticated source.
* The native-chat widget is an owned channel: replies are loopback-delivered through Orbit, so no third-party carrier handles the egress. This is the `web_chat` channel in the API.
* Tenant-owned controls govern AI disclosure, quiet hours, and data retention. Configure them in the dashboard; your worker should not attempt to bypass them.

## See also

* [Set up A2A federation between tenants](/guides/a2a-federation-setup)
* [A2A push notifications](/guides/a2a-push-notifications)
* [Native Chat channel configuration](/guides/native-chat-widget-channel-config)
* [Inbox setup](/guides/inbox-setup)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.