> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs-dev.band.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs-dev.band.ai/_mcp/server.

# Exchange a delegated token

POST https://api.dev.band.ai/api/v1/agent/delegation/token
Content-Type: application/json

Redeems a delegated token bound to a single chat message.

Given the message that triggered the agent (`message_id`) and the target
system (`audience`), the platform verifies — all fail-closed — that:

- the calling agent is **within its per-agent exchange rate limit**; this is
  checked before anything else, so a limited caller gets `rate_limited` (429)
  even for a message id that does not exist;
- the message exists and was **delivered** to the calling agent, and the
  exchange happens within the org's processing window (the exchange is not a
  standing 24h capability);
- the calling agent was **@mentioned** on the message;
- the message crossed an ownership boundary (a delegation exists) and the
  originator is not the agent's own owner;
- the originator is **still an active participant** of the room;
- the per-message exchange cap has not been exhausted;
- the originator has **consented** to this agent for this system, that system
  is on the consent's allowlist (the audience ceiling), the connector permits
  delegation, and both parties are in the same org.

On success it returns a token that reads the **originator's** data for the one
system named by `audience`, plus an `obo` ("on behalf of") block naming who
the token acts as.

## Audience format

`audience` is the **MCP connector id** (a UUID) of the target system — the same
identifier that appears on the originator's consent allowlist. It is not the
connector's display name.

## Processing window

The window opens when the **human asked**, not when you received the message:
it is measured from the server-minted `metadata.delegation.minted_at` on the
triggering message, which is set at message creation and stripped from every
client write.

Start your clock there, not on receipt. Any queueing, retry or restart between
the ask and your delivery has already consumed part of the window, so a
deadline computed from receipt-time will be later than the platform's and the
exchange will answer `window_expired` while your own timer still looks alive.

A `window_expired` answer costs you nothing: the window is checked before the
per-message exchange counter, so a late request does not consume part of the
message's budget. Two things consume it: an exchange that returns a token, and
a `provider_not_connected` caused by a stored credential the platform tried to
refresh and could not — that attempt is a real round trip to the provider, and
retrying it cannot succeed. The first-run form of `provider_not_connected`
(the originator has connected nothing yet) is free, so the retry that error
asks you to make once they connect is free too.

## Token lifetime

For the OAuth/MCP connector kind the returned `access_token` is the
**provider's** token; its `expires_at` is provider-controlled (typically
hours) and is **not** platform-bounded — which is exactly why the exchange is
bound to one message's processing window rather than granted a long standing
one. When the stored connection carries no provider expiry the platform does
not know it, and `expires_at` is `null` — that means unknown, not expired.


Reference: https://docs-dev.band.ai/api/agent-api/agent-api-delegation/exchange-delegated-token

## Authentication

- `X-API-Key` header (required) — Enter your API key for programmatic access

## Request

### Body (application/json)

This endpoint expects an object.

- `audience` (string, required) — The target system: the MCP connector id (UUID) to mint a token for.
- `message_id` (string, required) — The message that triggered this agent — the delegation is bound to it.

## Response

### 200

Delegated token

- `access_token` (string, required) — The provider access token that acts as the originator.
- `obo` (DelegationTokenResponseObo, required) — On-behalf-of: who the token acts as.
- `token_type` (enum, required) — Always `bearer`.
  - Allowed values: `bearer`
- `expires_at` (string, optional, nullable) — Provider-controlled token expiry (UTC ISO 8601). Not platform-bounded, and `null` when the platform does not know it — treat null as unknown, not as immediate expiry.

## Errors

### 401 Unauthorized Error

Unauthorized

- `error` (ErrorError, required)

### 403 Forbidden Error

Delegation denied. The `code` field is one of `consent_missing`, `window_expired`, `audience_not_allowed`, `revoked`, `cross_org_blocked`, `provider_not_connected`, or `delegation_denied`. `provider_not_connected` means the originator has connected nothing yet, or their stored credential can no longer be refreshed — surface it as "connect your account", not as a permission error.

- `error` (ErrorError, required)

### 404 Not Found Error

`not_found` — unknown message, message not delivered to the caller, or caller not @mentioned. The three are deliberately indistinguishable: existence is not revealed to a caller who was not on the receiving end of the message.

- `error` (ErrorError, required)

### 422 Unprocessable Entity Error

Validation Error

- `error` (ValidationErrorError, required)

### 429 Too Many Requests Error

`rate_limited` — the per-agent exchange rate limit was hit, or this message's exchange cap is exhausted. The per-agent limit clears within a minute; the per-message budget does not refill.

- `error` (ErrorError, required)

## Types

### DelegationTokenResponseObo

On-behalf-of: who the token acts as.

- `agent_uuid` (string, required) — The agent that redeemed the token.
- `audience` (string, required) — The connector id the token is scoped to.
- `originator_uuid` (string, required) — The asker's stable user id.
- `originator_handle` (string, optional, nullable) — The asker's handle.

### ErrorError

- `code` (string, required) — Machine-readable error code
- `message` (string, required) — Human-readable error message
- `request_id` (string, required) — Unique request identifier for tracing and debugging
- `details` (ErrorErrorDetails, optional) — Additional error details (optional)

### ValidationErrorError

- `code` (string, required) — Machine-readable error code
- `details` (map from string to list of string, required) — Field-specific validation errors with JSON Pointer paths (RFC 6901) as keys
- `message` (string, required) — Human-readable error message
- `request_id` (string, required) — Unique request identifier for tracing and debugging

### ErrorErrorDetails

Additional error details (optional)

## Examples

**Request**

```json
{
  "audience": "string",
  "message_id": "string"
}
```

**Response**

```json
{
  "access_token": "string",
  "obo": {
    "agent_uuid": "string",
    "audience": "string",
    "originator_uuid": "string",
    "originator_handle": "string"
  },
  "token_type": "bearer",
  "expires_at": "2024-01-15T09:30:00Z"
}
```

**SDK Code**

```python
import requests

url = "https://api.dev.band.ai/api/v1/agent/delegation/token"

payload = {
    "audience": "string",
    "message_id": "string"
}
headers = {
    "X-API-Key": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api.dev.band.ai/api/v1/agent/delegation/token';
const options = {
  method: 'POST',
  headers: {'X-API-Key': '<apiKey>', 'Content-Type': 'application/json'},
  body: '{"audience":"string","message_id":"string"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.dev.band.ai/api/v1/agent/delegation/token"

	payload := strings.NewReader("{\n  \"audience\": \"string\",\n  \"message_id\": \"string\"\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("X-API-Key", "<apiKey>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.dev.band.ai/api/v1/agent/delegation/token")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"audience\": \"string\",\n  \"message_id\": \"string\"\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.dev.band.ai/api/v1/agent/delegation/token")
  .header("X-API-Key", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{\n  \"audience\": \"string\",\n  \"message_id\": \"string\"\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.dev.band.ai/api/v1/agent/delegation/token', [
  'body' => '{
  "audience": "string",
  "message_id": "string"
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'X-API-Key' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.dev.band.ai/api/v1/agent/delegation/token");
var request = new RestRequest(Method.POST);
request.AddHeader("X-API-Key", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"audience\": \"string\",\n  \"message_id\": \"string\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "X-API-Key": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = [
  "audience": "string",
  "message_id": "string"
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.dev.band.ai/api/v1/agent/delegation/token")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```