# Aigently API > Place calls, start chats and read every conversation from your own server. Your agents already answer phones, talk on your website and chat on WhatsApp. The API lets your own systems take part: place a call when a lead arrives, give an inbound caller's details to the agent before it says hello, run a chat from your app, and read back the transcript, the analysis and the recording of every conversation. ## Start here - **[Quickstart](/quickstart)** — a test key, a test call that rings nobody, its transcript, and its webhooks on your laptop, in about twenty minutes. - **[API reference](/reference)** — every operation, with an example in seven languages and a *Try it* that takes a test key. - **[Test mode](/guides/test-mode)** — everything you can do without a phone number or credit. ## How it works - **Address.** `https://api.aigently.ai/v1`. A company running its own copy of Aigently uses its own address with `/v1` on the end. - **Keys.** An organization API key in `Authorization: Bearer `. Keys are made on the console's *API keys* page. A test key (`ag_test_…`) sees only test data and never rings a real phone; a live key (`ag_live_…`) places real calls. See [API keys](/guides/keys). - **JSON**, with `snake_case` fields. Unknown fields in a request are refused, so a typo fails loudly. Responses may gain fields at any time; read the ones you know. - **Errors** are problem documents with a stable `code` — see [Errors](/errors). - **Retries are safe** where they matter: a call needs an `Idempotency-Key`, and sending the same request again returns the same conversation. See [Safe retries](/guides/idempotency). **curl** ```sh #!/bin/sh # Check your key curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/me" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Check your key const response = await fetch("https://api.aigently.ai/v1/me", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Check your key # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/me", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Check your key package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/me", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Check your key import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/me")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Check your key using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/me") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` ## Examples in your language Every example on this site is in curl, JavaScript, Python, Go, Java, C# and PHP, using each language's usual HTTP tool — there is nothing of ours to install. Pick a language once and every example follows. Every example is run against the platform before it is published. ## Downloads - [The OpenAPI file](/openapi.v1.json) — the whole API, for code generators and your own tools. - [A Postman collection](/aigently.postman_collection.json), which Bruno and Insomnia import too. - [`aigently.mjs`](/aigently.mjs) — the command line: every operation, an agent's fields as types, `listen` and `mcp`. See [the command line](/cli). - [`llms.txt`](/llms.txt) and [`llms-full.txt`](/llms-full.txt) for AI coding tools. Every page is also available as Markdown: add `.md` to its address. --- # Quickstart > A test call that rings nobody, its transcript and analysis, then its webhooks and a test inbound call on your own machine. In the first ten minutes you place a test call and read what happened. In the next ten you receive its webhooks on your laptop and make a test call to an agent that asks your own server who is calling. Nothing here rings a phone or spends credit. ## 1. Make a test key In the console, open **API keys** and choose **New API key**. Pick **Test** and the *Outbound calling* preset, then **Create key**. The key is shown once: keep it in your environment. ```sh export AIGENTLY_API_KEY=ag_test_... ``` ## 2. Make an agent Make an agent from a template — *Appointment reminder* places calls, *Receptionist* answers them — and **Publish** it. Its id is on **Connect → Server API**, with the fields a request can fill in. Or ask the API for your agents: **curl** ```sh #!/bin/sh # List agents curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/agents?limit=20" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // List agents const response = await fetch("https://api.aigently.ai/v1/agents?limit=20", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # List agents # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/agents", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, params={ "limit": 20, }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // List agents package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/agents?limit=20", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // List agents import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/agents?limit=20")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // List agents using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/agents?limit=20") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` ## 3. Place a test call A test key calls only [test numbers](/guides/test-mode#test-numbers). `+12025550100` answers, talks for a few seconds and hangs up — with the agent's real greeting and your values in it. **curl** ```sh #!/bin/sh # Place a call. With a test key this calls a test number and rings nobody. curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/calls" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" \ -H "Idempotency-Key: lead-20931" \ --json '{ "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "variables": { "first_name": "Sara" }, "metadata": { "crm_lead_id": "L-20931" }, "external_id": "lead-20931" }' ``` **JavaScript** ```js // Place a call. With a test key this calls a test number and rings nobody. const response = await fetch("https://api.aigently.ai/v1/calls", { method: "POST", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": "lead-20931", }, body: JSON.stringify({ "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "variables": { "first_name": "Sara" }, "metadata": { "crm_lead_id": "L-20931" }, "external_id": "lead-20931" }), }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Place a call. With a test key this calls a test number and rings nobody. # pip install httpx import os import httpx response = httpx.post( "https://api.aigently.ai/v1/calls", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", "Idempotency-Key": "lead-20931", }, json={ "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "variables": { "first_name": "Sara", }, "metadata": { "crm_lead_id": "L-20931", }, "external_id": "lead-20931", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Place a call. With a test key this calls a test number and rings nobody. package main import ( "bytes" "fmt" "io" "net/http" "os" ) func main() { payload := []byte(`{ "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "variables": { "first_name": "Sara" }, "metadata": { "crm_lead_id": "L-20931" }, "external_id": "lead-20931" }`) request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/calls", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) request.Header.Set("Content-Type", "application/json") request.Header.Set("Idempotency-Key", "lead-20931") response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Place a call. With a test key this calls a test number and rings nobody. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { String body = """ { "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "variables": { "first_name": "Sara" }, "metadata": { "crm_lead_id": "L-20931" }, "external_id": "lead-20931" } """; HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/calls")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .header("Content-Type", "application/json") .header("Idempotency-Key", "lead-20931") .method("POST", HttpRequest.BodyPublishers.ofString(body)) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Place a call. With a test key this calls a test number and rings nobody. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/calls") { Content = new StringContent( """ { "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "variables": { "first_name": "Sara" }, "metadata": { "crm_lead_id": "L-20931" }, "external_id": "lead-20931" } """, Encoding.UTF8, "application/json"), }; request.Headers.Add("Idempotency-Key", "lead-20931"); var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), "Content-Type: application/json", "Idempotency-Key: lead-20931", ], CURLOPT_POSTFIELDS => json_encode([ "agent_id" => "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number" => "+12025550100", "variables" => [ "first_name" => "Sara", ], "metadata" => [ "crm_lead_id" => "L-20931", ], "external_id" => "lead-20931", ]), ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` Send a value for each field your agent needs: its `variables_schema` lists them, and the agent's **Server API** tab shows this request with its own fields already filled in. The answer is the conversation, `pending` until the test number answers. The `Idempotency-Key` is your own id for this call: send the same request again and you get the same conversation back instead of a second call. ## 4. Read what happened About ten seconds later the call has ended. Read it with its transcript: **curl** ```sh #!/bin/sh # Read a conversation with its transcript curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Read a conversation with its transcript const response = await fetch("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Read a conversation with its transcript # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, params={ "include": "transcript", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Read a conversation with its transcript package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Read a conversation with its transcript import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Read a conversation with its transcript using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` `status` is `completed`, `disposition` is `answered`, and `transcript.entries` holds what was said. Where the agent analyses its calls, `analysis` carries the outcome and the checks. A test call also has a short sample recording: **curl** ```sh #!/bin/sh # Get a link to a recording. The link works for five minutes, so download the audio straight away. curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/recording" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Get a link to a recording. The link works for five minutes, so download the audio straight away. const response = await fetch("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/recording", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Get a link to a recording. The link works for five minutes, so download the audio straight away. # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/recording", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Get a link to a recording. The link works for five minutes, so download the audio straight away. package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/recording", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Get a link to a recording. The link works for five minutes, so download the audio straight away. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/recording")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Get a link to a recording. The link works for five minutes, so download the audio straight away. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/recording") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` ## 5. Receive its webhooks on your laptop The platform cannot reach `localhost`, so [`aigently listen`](/cli) keeps a connection open with your test key and passes each test event to a server on your machine. It needs only Node.js 22. ```sh curl -O https://developers.aigently.ai/aigently.mjs node aigently.mjs listen --forward-to localhost:3000/hooks ``` It prints a signing secret. Place another test call and your server receives `conversation.started`, `conversation.ended`, `conversation.analyzed` and `conversation.recording_ready`, signed with that secret. Check each one with the [webhook helper](/guides/webhooks#check-the-signature) before you trust it. ## 6. Make a test call to your agent Give your inbound agent a lookup address under **Connect → Phone → Look up who is calling**, then forward test lookups to your own server too: ```sh node aigently.mjs listen --forward-to localhost:3000/hooks --forward-lookups-to localhost:3000/lookup ``` Your `/lookup` answers with values for the agent's fields: ```json {"variables": {"first_name": "Sara"}, "metadata": {"crm_id": "C-1042"}} ``` Now call the agent from a test number: **curl** ```sh #!/bin/sh # Make a test call to an agent. Test keys only. It asks the agent's test lookup and plays a scripted call. curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/test/inbound-calls" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" \ --json '{ "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "from_number": "+12025550100" }' ``` **JavaScript** ```js // Make a test call to an agent. Test keys only. It asks the agent's test lookup and plays a scripted call. const response = await fetch("https://api.aigently.ai/v1/test/inbound-calls", { method: "POST", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "from_number": "+12025550100" }), }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Make a test call to an agent. Test keys only. It asks the agent's test lookup and plays a scripted call. # pip install httpx import os import httpx response = httpx.post( "https://api.aigently.ai/v1/test/inbound-calls", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, json={ "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "from_number": "+12025550100", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Make a test call to an agent. Test keys only. It asks the agent's test lookup and plays a scripted call. package main import ( "bytes" "fmt" "io" "net/http" "os" ) func main() { payload := []byte(`{ "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "from_number": "+12025550100" }`) request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/test/inbound-calls", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) request.Header.Set("Content-Type", "application/json") response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Make a test call to an agent. Test keys only. It asks the agent's test lookup and plays a scripted call. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { String body = """ { "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "from_number": "+12025550100" } """; HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/test/inbound-calls")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body)) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Make a test call to an agent. Test keys only. It asks the agent's test lookup and plays a scripted call. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/test/inbound-calls") { Content = new StringContent( """ { "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "from_number": "+12025550100" } """, Encoding.UTF8, "application/json"), }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "agent_id" => "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "from_number" => "+12025550100", ]), ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` The answer says where the values came from (`listen`, here) and the greeting they produced. See [Inbound calls](/guides/inbound-calls) for everything the lookup is sent and may answer. ## Next - [Going live](/going-live) — what changes with a live key. - [Variables](/guides/variables) — how an agent's fields are filled, checked and kept safe. - [Chat and streaming](/guides/chat) — the same agents, from your app. --- # Going live > What changes when the same code runs with a live key. The code you wrote against a test key is the code you run live: the address, the requests and the answers are the same. What changes is that calls ring real phones, conversations cost credit, and your receivers hear about real people. ## Before the first live call - **A live key.** On **API keys**, make one with **Live**, limited to the projects — or the agents — it needs. Asking for it takes your password again, and every owner of the organization is emailed. A live key never works from a browser: keep it on your server. - **A number to call from.** An agent that calls out needs a verified number on its **Connect → Caller ID** tab. Without one, a call is refused with `caller_id_unavailable`. - **Countries.** The deployment calls only the countries it allows, never emergency or service numbers, and premium-rate numbers only where it has said so. Anything else is refused with `destination_not_allowed`. Send numbers in full international format: `+`, the country code, then the number. - **Consent.** Call people who agreed to hear from you, at hours that suit them. The platform records who placed every call — the key's name is on it in the audit log. - **Live receivers.** A webhook receiver is either live or test. Add a live one for your production server; your test receivers keep getting test events only. ## Limits | Limit | Default | When it is reached | |---|---|---| | Calls at the same time, per organization | 10 | `429 concurrency_limit_reached` | | Calls a day, per organization | 2,000 | `429 daily_limit_reached` | | Requests a minute, per key | 600 reads, 120 writes, 1,200 chat messages | `429 rate_limited` | `GET /v1/me` shows your key's limits and how many of your calls are in progress. Our staff can raise an organization's limits. See [Rate limits](/guides/rate-limits). **curl** ```sh #!/bin/sh # Check your key curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/me" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Check your key const response = await fetch("https://api.aigently.ai/v1/me", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Check your key # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/me", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Check your key package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/me", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Check your key import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/me")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Check your key using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/me") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` ## Credit Live conversations are charged from the organization's credit, as conversations from the console are; the API itself costs nothing. When the credit runs out, new calls and chats are refused with `402 insufficient_credit`, and reading conversations keeps working. ## Keep it safe - Rotate a key that may have leaked — the old secret keeps working for the time you choose, so your servers can be updated first. Revoke a key nobody uses. - Check every webhook's signature before you act on it ([how](/guides/webhooks#check-the-signature)). - Build each call's `Idempotency-Key` from your own data, so a retry can never call twice. --- # The command line > Every API operation from a terminal, an agent's fields as types, test events on your own machine, and MCP over standard input. One file, and it needs only Node.js 22 or later: ```sh curl -O https://developers.aigently.ai/aigently.mjs export AIGENTLY_API_KEY=ag_test_... node aigently.mjs conversations list --limit 5 ``` ## Any API operation Every operation in the [reference](/reference) is a command: its resource, its method, its ids in order, and its fields as flags — the field's name with dashes. ```sh node aigently.mjs agents retrieve 8a1f3c2e-… node aigently.mjs conversations retrieve 5f0c9a52-… --include transcript node aigently.mjs calls create --agent-id 8a1f3c2e-… --to-number +966501234567 \ --variables '{"first_name": "Sara"}' node aigently.mjs conversations list --agent-id 8a1f3c2e-… --all > conversations.jsonl ``` - **Lists take commas** (`--include transcript,cost`), or a JSON array when an item has a comma in it (`--turns '["Hi, are you open on Sunday?"]'`). **Objects take JSON**, and a file takes its path (`--file ./faq.pdf`). `null` sends null, which is how a number is detached from its agent. - `--json '{…}'`, or `--json @body.json`, sends a whole body; flags add to it. - **`--all` walks every page of a list**, printing one JSON object a line. - **A request that takes an `Idempotency-Key` gets one**, unless you pass your own with `--idempotency-key`. - The answer is printed as JSON. A refusal prints its `code`, `detail` and request id, and exits 1. `node aigently.mjs operations` lists every command. ## An agent's fields as types ```sh node aigently.mjs types --agent 8a1f3c2e-… > agent.ts node aigently.mjs types --agent 8a1f3c2e-… --lang python > agent_fields.py ``` It writes the agent's [three lists of fields](/guides/variables) — what a conversation can start with, what it collects, and what its analysis writes — as TypeScript interfaces or Python `TypedDict`s, required fields marked. Write them again when the agent's fields change: the `agent.updated` event says when. A field named like a Python keyword, such as `from`, is written in the `TypedDict("…", {…})` form, which takes any name. ## Evaluations in a build step: `evaluate` ```sh node aigently.mjs evaluate 8a1f3c2e-… ``` It rehearses the agent's saved conversations, waits for the verdict, prints every conversation's outcome, and exits `0` when all passed, `1` when one failed, and `3` when none failed but some could not be checked. `--json` prints the whole run instead, and `--timeout` says how many seconds to wait (25 minutes unless you say). See [Evaluations in your pipeline](/guides/evaluations). ## A call, followed live: `follow` ```sh node aigently.mjs follow 5f0c9a52-… ``` It prints what is said on a call as it is said — `Caller:` and `Agent:` lines — and stops when the call ends. `--json` prints each line as the API sends it. See [Live calls](/guides/live-calls). ## Test events on your machine: `listen` The platform cannot reach `localhost`. `listen` keeps a connection open to the API with a **test key** and passes on everything a test webhook receiver would be sent — and, if you ask, the context lookups of test calls — to a server on your machine. ```sh node aigently.mjs listen --forward-to localhost:3000/hooks ``` - **Events.** Each test event is POSTed to `--forward-to` with the headers a webhook carries, signed with the session's own secret, which it prints when the connection is ready. Check them exactly as you would a receiver's ([how](/guides/webhooks#check-the-signature)). What your server answers is only printed. - **Lookups.** With `--forward-lookups-to`, a test call's context lookup is POSTed there, and your server's status and body go back to the call as its answer — so a [test inbound call](/guides/inbound-calls#in-test-mode) reaches your own code. An open session is asked before the agent's test address or its sample answer. - **Only what the key reaches.** A key limited to some projects or agents hears only about those, and is asked only their test calls' lookups. - **Only what the key may read.** Listening needs `conversations:read`, and a `conversation.transcript` event also needs `transcripts:read`. Every key preset has both. - **Test keys only.** A live key is refused when it connects: real conversations go to receivers whose addresses have been checked, never to a laptop. - **A few at a time.** A key may keep 3 sessions open, and an organization 10. - **It stops when the key does.** Revoke the key, let it expire, or rotate it without an overlap, and the session ends within half a minute. - **Stop it and its session is gone**, with everything it was holding: the next test call's events go only to your test receivers. ## An AI assistant: `mcp` `node aigently.mjs mcp` carries an assistant's [MCP](/guides/mcp) messages from standard input to the API's MCP server, and the answers back — for an assistant that runs its tools as a local program. ## Options | Option | What it does | |---|---| | `--api-key ` | Your key. Or set `AIGENTLY_API_KEY`. | | `--api ` | The API's address, for a company running its own copy. Or set `AIGENTLY_BASE_URL`. Default `https://api.aigently.ai`. | | `--all` | For a list: every page, one JSON object a line. | | `--idempotency-key ` | Your own id for a request that takes one. | | `--forward-to ` | `listen`: where to POST each test event, like `localhost:3000/hooks`. | | `--forward-lookups-to ` | `listen`: where to POST each test call's lookup. | | `--help` | The options. | Your key travels in a header, never in an address, where a proxy would log it. --- # Outbound calls > Place a call with an agent, follow it, end it, and know what each ending means. An outbound agent calls a number you give it, with values you give it, and you hear about every step. A call needs the `calls:write` permission. ## Place a call **curl** ```sh #!/bin/sh # Place a call. With a test key this calls a test number and rings nobody. curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/calls" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" \ -H "Idempotency-Key: lead-20931" \ --json '{ "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "variables": { "first_name": "Sara" }, "metadata": { "crm_lead_id": "L-20931" }, "external_id": "lead-20931" }' ``` **JavaScript** ```js // Place a call. With a test key this calls a test number and rings nobody. const response = await fetch("https://api.aigently.ai/v1/calls", { method: "POST", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": "lead-20931", }, body: JSON.stringify({ "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "variables": { "first_name": "Sara" }, "metadata": { "crm_lead_id": "L-20931" }, "external_id": "lead-20931" }), }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Place a call. With a test key this calls a test number and rings nobody. # pip install httpx import os import httpx response = httpx.post( "https://api.aigently.ai/v1/calls", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", "Idempotency-Key": "lead-20931", }, json={ "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "variables": { "first_name": "Sara", }, "metadata": { "crm_lead_id": "L-20931", }, "external_id": "lead-20931", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Place a call. With a test key this calls a test number and rings nobody. package main import ( "bytes" "fmt" "io" "net/http" "os" ) func main() { payload := []byte(`{ "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "variables": { "first_name": "Sara" }, "metadata": { "crm_lead_id": "L-20931" }, "external_id": "lead-20931" }`) request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/calls", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) request.Header.Set("Content-Type", "application/json") request.Header.Set("Idempotency-Key", "lead-20931") response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Place a call. With a test key this calls a test number and rings nobody. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { String body = """ { "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "variables": { "first_name": "Sara" }, "metadata": { "crm_lead_id": "L-20931" }, "external_id": "lead-20931" } """; HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/calls")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .header("Content-Type", "application/json") .header("Idempotency-Key", "lead-20931") .method("POST", HttpRequest.BodyPublishers.ofString(body)) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Place a call. With a test key this calls a test number and rings nobody. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/calls") { Content = new StringContent( """ { "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "variables": { "first_name": "Sara" }, "metadata": { "crm_lead_id": "L-20931" }, "external_id": "lead-20931" } """, Encoding.UTF8, "application/json"), }; request.Headers.Add("Idempotency-Key", "lead-20931"); var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), "Content-Type: application/json", "Idempotency-Key: lead-20931", ], CURLOPT_POSTFIELDS => json_encode([ "agent_id" => "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number" => "+12025550100", "variables" => [ "first_name" => "Sara", ], "metadata" => [ "crm_lead_id" => "L-20931", ], "external_id" => "lead-20931", ]), ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` | Field | What it is | |---|---| | `agent_id` | One of your outbound agents. It must be published. | | `to_number` | International format: `+`, the country code, then the number. | | `variables` | Values for the agent's fields — see [Variables](/guides/variables). | | `metadata` | Your own data, up to 20 keys. The agent never sees it; you get it back on every read and every event. | | `external_id` | Your own id for the conversation — a lead, an order. Find it again with `GET /v1/conversations?external_id=`. | | `from_number` or `phone_number_id` | Which of the agent's numbers to call from. Leave both out and the agent's own caller ID is used. | | `language` | One of the agent's language versions, like `ar`. | | `call_window` | The hours the call may start in — see [Calling hours](#calling-hours). | **`Idempotency-Key` is required.** It is your own id for this call, up to 120 characters — build it from your data, like `lead-8812-attempt-2`. The same request with the same key returns the same conversation, with `Idempotent-Replayed: true`, and never calls twice; the same key with a different request is refused with `idempotency_key_reused`. See [Safe retries](/guides/idempotency). The answer is `201` with the [conversation](/reference/objects/Conversation), `pending` until the phone is answered. Everything a call can be refused for is checked before anything is dialled. ## Follow it Either listen for [webhooks](/guides/webhooks) — `conversation.started` when it is answered and `conversation.ended` exactly once when it ends, answered or not — or read the conversation: **curl** ```sh #!/bin/sh # Read a conversation with its transcript curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Read a conversation with its transcript const response = await fetch("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Read a conversation with its transcript # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, params={ "include": "transcript", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Read a conversation with its transcript package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Read a conversation with its transcript import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Read a conversation with its transcript using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` `status` is `pending`, then `active` once answered, and finally `completed` or `failed`. The `revision` only goes up, so of two copies of a conversation the higher one is the newer. ## How a call ends Every way a call can end, what the conversation says about it, and which events you get: | What happened | `status` | `disposition` | `ended_by` | Events | Call again? | |---|---|---|---|---|---| | Answered, talked, and somebody hung up | `completed` | `answered` | `caller` or `agent` | `conversation.started`, `conversation.ended`, `conversation.analyzed`, `conversation.recording_ready` | No. | | Ended with /end after it was answered | `completed` | `answered` | `api` | `conversation.started`, `conversation.ended`, `conversation.analyzed` | No. | | An answering machine answered, and the agent left its message | `completed` | `voicemail_left` | `agent` | `conversation.started`, `conversation.ended` | Your choice. | | An answering machine answered, and no message was left | `failed` | `voicemail` | null | `conversation.started`, `conversation.ended` | Yes, later. | | The line was busy | `failed` | `busy` | null | `conversation.ended` | Yes, later. | | It rang and nobody answered | `failed` | `no_answer` | null | `conversation.ended` | Yes, later. | | The person rejected the call | `failed` | `declined` | null | `conversation.ended` | Usually not. | | The carrier would not put the call through | `failed` | `unreachable` | `failure` | `conversation.ended` | Check the number first. | | The call could not be placed at all | `failed` | — | `failure` | `conversation.ended` | Yes, in a moment. | | Cancelled with /end while it was ringing | `failed` | `no_answer` | `api` | `conversation.ended` | No — you cancelled it. | | Marked failed when its report was late, then reported as a full conversation | `completed` | `answered` | `caller` or `agent` | `conversation.started`, `conversation.ended`, `conversation.updated`, `conversation.analyzed` | No — always keep the highest revision. | - `conversation.analyzed` arrives only for an agent that analyses its conversations, and a little after the end. - `conversation.recording_ready` arrives only for a call that was recorded. - Events can arrive out of order. Keep the one with the highest `revision`. An answering machine is handled by the agent's own voicemail setting: it leaves its message, or hangs up and the call ends `voicemail`. ## End it yourself **curl** ```sh #!/bin/sh # End a call or a chat curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/end" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // End a call or a chat const response = await fetch("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/end", { method: "POST", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # End a call or a chat # pip install httpx import os import httpx response = httpx.post( "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/end", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // End a call or a chat package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/end", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // End a call or a chat import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/end")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("POST", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // End a call or a chat using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/end") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], CURLOPT_POSTFIELDS => "", ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` A call still ringing is cancelled; one in progress ends with the agent's own goodbye. Ending always answers `200` with the conversation, so it is safe to send again. ## Calling hours Name the hours a call may start in, in the time zone of the person you are calling: ```json "call_window": { "timezone": "America/New_York", "start": "09:00", "end": "17:00", "days": ["mon", "tue", "wed", "thu", "fri"] } ``` `timezone` is a name like `Asia/Riyadh`. The window includes `start` and ends just before `end`. Leave `days` out for every day of the week. A call sent outside its window is refused with `outside_call_window`, and `detail` says when the window opens next — so schedule it for then. ## Schedule a call for later **curl** ```sh #!/bin/sh # Schedule a call inside calling hours. It is placed at the first moment the window is open at or after dial_at. curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/scheduled-calls" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" \ -H "Idempotency-Key: reminder-20931" \ --json '{ "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "dial_at": "2026-10-12T13:00:00Z", "call_window": { "timezone": "America/New_York", "start": "09:00", "end": "17:00", "days": [ "mon", "tue", "wed", "thu", "fri" ] }, "variables": { "first_name": "Sara" }, "external_id": "reminder-20931" }' ``` **JavaScript** ```js // Schedule a call inside calling hours. It is placed at the first moment the window is open at or after dial_at. const response = await fetch("https://api.aigently.ai/v1/scheduled-calls", { method: "POST", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": "reminder-20931", }, body: JSON.stringify({ "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "dial_at": "2026-10-12T13:00:00Z", "call_window": { "timezone": "America/New_York", "start": "09:00", "end": "17:00", "days": [ "mon", "tue", "wed", "thu", "fri" ] }, "variables": { "first_name": "Sara" }, "external_id": "reminder-20931" }), }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Schedule a call inside calling hours. It is placed at the first moment the window is open at or after dial_at. # pip install httpx import os import httpx response = httpx.post( "https://api.aigently.ai/v1/scheduled-calls", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", "Idempotency-Key": "reminder-20931", }, json={ "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "dial_at": "2026-10-12T13:00:00Z", "call_window": { "timezone": "America/New_York", "start": "09:00", "end": "17:00", "days": ["mon", "tue", "wed", "thu", "fri"], }, "variables": { "first_name": "Sara", }, "external_id": "reminder-20931", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Schedule a call inside calling hours. It is placed at the first moment the window is open at or after dial_at. package main import ( "bytes" "fmt" "io" "net/http" "os" ) func main() { payload := []byte(`{ "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "dial_at": "2026-10-12T13:00:00Z", "call_window": { "timezone": "America/New_York", "start": "09:00", "end": "17:00", "days": [ "mon", "tue", "wed", "thu", "fri" ] }, "variables": { "first_name": "Sara" }, "external_id": "reminder-20931" }`) request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/scheduled-calls", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) request.Header.Set("Content-Type", "application/json") request.Header.Set("Idempotency-Key", "reminder-20931") response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Schedule a call inside calling hours. It is placed at the first moment the window is open at or after dial_at. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { String body = """ { "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "dial_at": "2026-10-12T13:00:00Z", "call_window": { "timezone": "America/New_York", "start": "09:00", "end": "17:00", "days": [ "mon", "tue", "wed", "thu", "fri" ] }, "variables": { "first_name": "Sara" }, "external_id": "reminder-20931" } """; HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/scheduled-calls")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .header("Content-Type", "application/json") .header("Idempotency-Key", "reminder-20931") .method("POST", HttpRequest.BodyPublishers.ofString(body)) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Schedule a call inside calling hours. It is placed at the first moment the window is open at or after dial_at. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/scheduled-calls") { Content = new StringContent( """ { "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number": "+12025550100", "dial_at": "2026-10-12T13:00:00Z", "call_window": { "timezone": "America/New_York", "start": "09:00", "end": "17:00", "days": [ "mon", "tue", "wed", "thu", "fri" ] }, "variables": { "first_name": "Sara" }, "external_id": "reminder-20931" } """, Encoding.UTF8, "application/json"), }; request.Headers.Add("Idempotency-Key", "reminder-20931"); var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), "Content-Type: application/json", "Idempotency-Key: reminder-20931", ], CURLOPT_POSTFIELDS => json_encode([ "agent_id" => "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "to_number" => "+12025550100", "dial_at" => "2026-10-12T13:00:00Z", "call_window" => [ "timezone" => "America/New_York", "start" => "09:00", "end" => "17:00", "days" => ["mon", "tue", "wed", "thu", "fri"], ], "variables" => [ "first_name" => "Sara", ], "external_id" => "reminder-20931", ]), ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` The body is the one a call takes, plus `dial_at`: the earliest the call may be placed, at most 30 days ahead. Leave it out, or send a time already past, and the call is placed now. With a `call_window`, it is placed at the first moment the window is open at or after `dial_at`, and the `dial_at` in the answer is already moved there. `Idempotency-Key` is required, as it is for a call. Everything a call can be refused for is checked when you schedule it, so a mistake is said at once, and checked again when it is placed, because an agent can be unpublished or a number listed as do-not-call in the meantime. If the window has closed by then, the call waits for its next opening. If every line is busy, or the organization has placed today's calls, it waits and tries again: a minute later for a busy line, an hour later for the day's limit. A [scheduled call](/reference/objects/ScheduledCall) is `scheduled` until its time comes, and then: | `status` | What happened | |---|---| | `placed` | It became a conversation. `conversation_id` names it, and it sends the usual events. | | `failed` | It was refused when its time came, and no call was made. `error_code` says why — see below. | | `cancelled` | You called it off. | `error_code` on a failed call is the code `POST /v1/calls` would have answered, like `agent_not_published` or `do_not_call`, or one of two more: `key_revoked` when the key that scheduled it no longer works, and `lines_busy` when it was put off 30 times because every line was busy or the day's calls were used up. List the calls still waiting: **curl** ```sh #!/bin/sh # List the calls still waiting curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/scheduled-calls?status=scheduled" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // List the calls still waiting const response = await fetch("https://api.aigently.ai/v1/scheduled-calls?status=scheduled", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # List the calls still waiting # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/scheduled-calls", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, params={ "status": "scheduled", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // List the calls still waiting package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/scheduled-calls?status=scheduled", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // List the calls still waiting import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/scheduled-calls?status=scheduled")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // List the calls still waiting using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/scheduled-calls?status=scheduled") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` Cancel one before its time: **curl** ```sh #!/bin/sh # Cancel a scheduled call. Only before its time: once placed, it is a conversation, which you end instead. curl -sS --fail-with-body -X DELETE "https://api.aigently.ai/v1/scheduled-calls/4b6d8f0a-2c4e-4a6c-9e8a-0c2e4a6c8e0b" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Cancel a scheduled call. Only before its time: once placed, it is a conversation, which you end instead. const response = await fetch("https://api.aigently.ai/v1/scheduled-calls/4b6d8f0a-2c4e-4a6c-9e8a-0c2e4a6c8e0b", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Cancel a scheduled call. Only before its time: once placed, it is a conversation, which you end instead. # pip install httpx import os import httpx response = httpx.delete( "https://api.aigently.ai/v1/scheduled-calls/4b6d8f0a-2c4e-4a6c-9e8a-0c2e4a6c8e0b", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Cancel a scheduled call. Only before its time: once placed, it is a conversation, which you end instead. package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("DELETE", "https://api.aigently.ai/v1/scheduled-calls/4b6d8f0a-2c4e-4a6c-9e8a-0c2e4a6c8e0b", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Cancel a scheduled call. Only before its time: once placed, it is a conversation, which you end instead. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/scheduled-calls/4b6d8f0a-2c4e-4a6c-9e8a-0c2e4a6c8e0b")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("DELETE", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Cancel a scheduled call. Only before its time: once placed, it is a conversation, which you end instead. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Delete, "https://api.aigently.ai/v1/scheduled-calls/4b6d8f0a-2c4e-4a6c-9e8a-0c2e4a6c8e0b") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php "DELETE", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` Once its time has come it can no longer be cancelled. A call being placed right now answers `scheduled_call_placing`, and a call already placed is a conversation, which you [end](#end-it-yourself) instead. A test key's scheduled calls become test calls that ring nobody, and only test keys see them. A scheduled call is kept for 30 days after it is placed, cancelled or fails; the conversation it made is kept like any other. ## Calls refused before they start | Code | Why | |---|---| | `agent_not_outbound` | The agent answers calls; it does not place them. | | `agent_not_published` | Publish the agent first. | | `invalid_variables` | A value is missing or does not fit — `errors` names each. | | `destination_not_allowed` | A number this deployment does not call. | | `do_not_call` | The number is on your organization's [do-not-call list](#numbers-that-must-not-be-called). | | `caller_id_unavailable` | The agent has no number it may call from. | | `concurrency_limit_reached` | The organization already has as many calls going as it may. | | `daily_limit_reached` | The organization has placed today's calls. | | `test_number_required` | A test key calls only [test numbers](/guides/test-mode#test-numbers). | | `outside_call_window` | The call's [window](#calling-hours) is closed. `detail` says when it opens. | | `feature_disabled` | This deployment has phone calls switched off. A test key's calls are not affected. | ## Numbers that must not be called Your organization keeps one do-not-call list, and no agent rings a number on it: a call through the API, a campaign's contact, an appointment reminder, a transfer to a person or a test dial from the console is refused before anything is dialled, with `do_not_call`. Keep it from your own systems — when somebody opts out in your CRM, add them: **curl** ```sh #!/bin/sh # Add a number to the do-not-call list. With a live key: the list decides which real people are called. curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/do-not-call" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" \ --json '{ "number": "+12025550188", "reason": "Asked not to be called again" }' ``` **JavaScript** ```js // Add a number to the do-not-call list. With a live key: the list decides which real people are called. const response = await fetch("https://api.aigently.ai/v1/do-not-call", { method: "POST", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "number": "+12025550188", "reason": "Asked not to be called again" }), }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Add a number to the do-not-call list. With a live key: the list decides which real people are called. # pip install httpx import os import httpx response = httpx.post( "https://api.aigently.ai/v1/do-not-call", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, json={ "number": "+12025550188", "reason": "Asked not to be called again", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Add a number to the do-not-call list. With a live key: the list decides which real people are called. package main import ( "bytes" "fmt" "io" "net/http" "os" ) func main() { payload := []byte(`{ "number": "+12025550188", "reason": "Asked not to be called again" }`) request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/do-not-call", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) request.Header.Set("Content-Type", "application/json") response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Add a number to the do-not-call list. With a live key: the list decides which real people are called. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { String body = """ { "number": "+12025550188", "reason": "Asked not to be called again" } """; HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/do-not-call")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body)) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Add a number to the do-not-call list. With a live key: the list decides which real people are called. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/do-not-call") { Content = new StringContent( """ { "number": "+12025550188", "reason": "Asked not to be called again" } """, Encoding.UTF8, "application/json"), }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "number" => "+12025550188", "reason" => "Asked not to be called again", ]), ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` This needs `do_not_call:write` and a live key: the list decides which real people are called, so a test key may read it but not change it. The list belongs to the whole organization, so reading or changing it also takes a key that reaches every project. Numbers are matched by their digits, so `+1 (202) 555-0100` and `12025550100` are one entry, and adding a listed number again answers `200` with the entry — taking the new `reason`, if you sent one. A number listed without its country code — `0798 798 906`, `(202) 555-0100` — also stops a call that dials it with one, and the other way round: the national forms are worked out from the number and from the line calling it. That can now and then refuse a number in another country that ends the same way. The refusal's `detail` names the entry, so you can check it. Ask whether a number is listed with `do_not_call:read`: **curl** ```sh #!/bin/sh # Ask whether a number may be called. An empty list means it is not on it. curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/do-not-call?number=+12025550188" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Ask whether a number may be called. An empty list means it is not on it. const response = await fetch("https://api.aigently.ai/v1/do-not-call?number=+12025550188", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Ask whether a number may be called. An empty list means it is not on it. # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/do-not-call", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, params={ "number": "+12025550188", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Ask whether a number may be called. An empty list means it is not on it. package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/do-not-call?number=+12025550188", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Ask whether a number may be called. An empty list means it is not on it. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/do-not-call?number=+12025550188")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Ask whether a number may be called. An empty list means it is not on it. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/do-not-call?number=+12025550188") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` Taking a number off lets it be called again, and is recorded in your organization's audit log under the key: **curl** ```sh #!/bin/sh # Take a number off the do-not-call list. With a live key. Recorded in the audit log under it. curl -sS --fail-with-body -X DELETE "https://api.aigently.ai/v1/do-not-call/+12025550188" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Take a number off the do-not-call list. With a live key. Recorded in the audit log under it. const response = await fetch("https://api.aigently.ai/v1/do-not-call/+12025550188", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Take a number off the do-not-call list. With a live key. Recorded in the audit log under it. # pip install httpx import os import httpx response = httpx.delete( "https://api.aigently.ai/v1/do-not-call/+12025550188", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Take a number off the do-not-call list. With a live key. Recorded in the audit log under it. package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("DELETE", "https://api.aigently.ai/v1/do-not-call/+12025550188", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Take a number off the do-not-call list. With a live key. Recorded in the audit log under it. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/do-not-call/+12025550188")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("DELETE", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Take a number off the do-not-call list. With a live key. Recorded in the audit log under it. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Delete, "https://api.aigently.ai/v1/do-not-call/+12025550188") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php "DELETE", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` A test key's own calls never ring anybody, so the list does not apply to them. --- # Variables > How an agent's fields are declared, filled, checked, and kept from rewriting the agent. An agent's instructions and its first message can name values it is given when a conversation starts: `Hello {{first_name}}, I'm calling about your appointment on {{appointment_date}}.` Each name is a **variable**. A call, a chat, a campaign row and the widget all fill them the same way and are checked by the same rules. ## Which variables an agent has Writing `{{first_name}}` in the instructions or the first message *is* declaring it. Read an agent's variables from its `variables_schema`, a JSON Schema: **curl** ```sh #!/bin/sh # Read an agent and its variables curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/agents/8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Read an agent and its variables const response = await fetch("https://api.aigently.ai/v1/agents/8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Read an agent and its variables # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/agents/8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Read an agent and its variables package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/agents/8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Read an agent and its variables import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/agents/8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Read an agent and its variables using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/agents/8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` ```json { "version": "3f9c1e07", "type": "object", "properties": { "first_name": {"type": "string", "maxLength": 500, "title": "First name"}, "party_size": {"type": "integer", "minimum": 1, "maximum": 12, "default": 2, "title": "Party size"}, "visit_date": {"type": "string", "format": "date", "title": "Visit date"} }, "required": ["first_name", "visit_date"], "additionalProperties": true } ``` `version` changes when the variables change and at no other time, so you can tell an edit to the fields from any other edit to the agent. When it changes, your receivers are sent [`agent.updated`](/guides/webhooks#events-about-an-agent) with the new list. ## Pin a version An agent's fields change when somebody publishes. To keep an integration on the version it was built against, send `agent_version` when you start a call, a scheduled call, a chat or a browser call: the conversation is answered by that published version from its first turn to its last, and its values are checked against that version's fields, not today's. `GET /v1/agents/{id}/versions` lists every version the agent was published as, newest first, each with its own `variables_schema` and the note its publisher wrote. A version the agent never had is refused with `unknown_agent_version`. The conversation's `agent_version` says which one answered. ## Types | Type | Send | Notes | |---|---|---| | Text | `"Sara"` | Long text is shortened where it is put into the instructions. | | Number | `4` or `4.5` | A whole-number field refuses `4.5`. Minimum and maximum apply. | | Yes / no | `true` or `false` | | | Email | `"sara@aigently.ai"` | | | Phone | `"+12025550123"` | Spaces and dashes are taken out first. | | Date | `"2027-03-04"` | `YYYY-MM-DD`. A field may set the earliest and latest date. | | Time | `"14:30"` | 24-hour `HH:MM`. | | Choice | `"terrace"` | One of the field's options. | Numbers and `true`/`false` may be sent as JSON values or as text; each comes back in its own type. ## Required, optional and defaults A variable the agent says out loud is required — unless it has a **default**, which stands in when nothing is sent. A field marked optional may simply be left out. A required value that is missing, or one of the wrong type, refuses the request with `422 invalid_variables`, and `errors` names each one: ```json { "code": "invalid_variables", "errors": [ {"field": "variables.first_name", "code": "required", "message": "First name is required"}, {"field": "variables.visit_date", "code": "invalid", "message": "Visit date is not a valid date"} ] } ``` **A name the agent does not use is not refused.** It is listed in the conversation's `ignored_variables`, so a renamed field shows up there rather than failing every call. Names starting `system_` are reserved for the platform. ## Where each value came from Every conversation records the source of each value in `variable_sources`: `api` for one your request sent, `lookup` for one your [context lookup](/guides/inbound-calls) answered, `campaign`, `identity_token` for a signed widget token, `widget` or `link` for what a visitor's page claimed. Values from a lookup or a visitor's page are never treated as the caller's own answers. ## Variables or metadata? | | `variables` | `metadata` | |---|---|---| | The agent sees it | Yes | Never | | Checked against the agent's fields | Yes | No | | Returned on every read and event | Yes | Yes | | Use it for | What the agent should know and say | Your own ids: a CRM record, a campaign, a lead | ## Values cannot rewrite the agent A value is put into the agent's instructions inside a block of its own, marked as data. A `first_name` of `Ignore your instructions and …` is a strange name, not a new instruction. --- # Inbound calls > Tell an agent who is calling — your server is asked while the phone is answered, and the values reach the agent before its first reply. When somebody calls one of your agents, the agent can know who they are before it says a word beyond hello. Give the agent a **context lookup**: an address on your own server that is asked about each call as it is answered, and answers with values for the agent's [variables](/guides/variables). ## Set it up In the console, open the agent's **Connect → Phone** tab and fill in **Look up who is calling**: - **Live address** — asked by real calls. It must be `https://` and reachable from the internet. - **Test address** — asked by [test calls](#in-test-mode) instead, never by real ones. - **Time limit** — how long your server has to answer: 1,000 ms unless you change it, at most 2,500. - **Sample answer** — what a test call uses when there is no test address, so you can try the agent before your server exists. Saving shows each address's **signing secret** once. Keep them on your server. ## What your server is sent A `POST` with the same headers a webhook carries, signed with the lookup's own secret: ```json { "event": "conversation.context", "created_at": "2026-10-07T09:14:03.120394+00:00", "data": { "conversation_id": "5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d", "livemode": true, "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "project_id": "0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e", "channel": "phone", "direction": "inbound", "from_number": "+12025550123", "caller": "+12025550123", "to_number": "+12025550199", "phone_number_id": "9e8d7c6b-5a4f-3e2d-1c0b-a9f8e7d6c5b4" } } ``` Check the signature before you trust it — the [webhook helper](/guides/webhooks#check-the-signature) checks a lookup too, with the lookup's secret. ## What it answers `200` with JSON, at most 16 KB: ```json {"variables": {"first_name": "Sara", "plan": "gold"}, "metadata": {"crm_id": "C-1042"}} ``` `variables` fill the agent's fields and are checked like any others. `metadata` is yours, up to 20 keys, and is kept on the conversation. Answer `{"variables": {}}` for a caller you do not know. ## When it is asked, and who waits The lookup starts the moment the call is answered, and your server answers while the greeting plays — so an answer within the time limit costs the caller nothing. Only two things wait: - **A greeting that names a value**, like `Hello {{first_name}}`, waits for your answer — at most the time limit — so it can say it. The console's greeting editor says so when this is the case. - **A caller who talks before the greeting ends** waits, at most the time limit, for the values to arrive before the agent answers. The values are in the agent's instructions before its first reply either way. ## When it fails A slow or broken server never stops a call: the agent answers without the values. Nothing is retried — somebody is on the line. The conversation's `context` says what happened: | `context.status` | Means | |---|---| | `pending` | Your server is still being asked. | | `ok` | The values were used. | | `timeout` | No complete answer within the time limit. | | `bad_status` | Your server answered something other than `200`. | | `too_large` | The answer was over 16 KB. | | `invalid_answer` | The answer was not the JSON above, or a value did not fit — `problems` names each. | | `refused` | The address is not one the platform will send to. | | `failed` | The request could not be made. | | `busy` | Your organization had too many lookups waiting at once. | `context.ms` is how long the lookup took, and `applied_before_first_reply` whether the values were in time for the first reply. ## Fill fields from the phonebook For values that rarely change — a name, a plan, a member number — you need no server at all. Turn on **Fill fields from the phonebook** on the agent's **Connect → Phone** tab, and keep the project's phonebook in step with [the contacts API](/guides/contacts). When a call arrives, the entry with the caller's number fills the agent's fields before the call is answered, with the source `phonebook`. Only the agent's own fields are filled, and only with values that fit them. When the agent also has a lookup, the lookup's answer replaces what the phonebook said, since it is newer. ## Details a call brings with it A phone system that transfers callers to your agents can send what it already knows in `X-` headers on the call. Name each header and the field it fills on the trunk's **Settings** in the console — **Fields from SIP headers**, like `X-Customer-Id=customer_id` — and the agent that answers starts with those values, with the source `sip_header`. Only the headers you name are read, and a value that does not fit the field is left out rather than refusing the call. They are the call's own values: they replace what the phonebook says, and the lookup's answer does not replace them. ## A caller ID is not proof The number a call comes from is whatever the network was told, and anybody can set it. Use the lookup and the phonebook to *personalise* a call — the name, the plan, the last order — never to decide that the caller is who the number says. Values from either are never treated as the caller's own answers, and an agent that needs to know who it is talking to should ask, the way a person would. ## In test mode A test call to your agent asks its **test address**, or uses its sample answer, and never the live one. Make one from a test number with a test key: **curl** ```sh #!/bin/sh # Make a test call to an agent. Test keys only. It asks the agent's test lookup and plays a scripted call. curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/test/inbound-calls" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" \ --json '{ "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "from_number": "+12025550100" }' ``` **JavaScript** ```js // Make a test call to an agent. Test keys only. It asks the agent's test lookup and plays a scripted call. const response = await fetch("https://api.aigently.ai/v1/test/inbound-calls", { method: "POST", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "from_number": "+12025550100" }), }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Make a test call to an agent. Test keys only. It asks the agent's test lookup and plays a scripted call. # pip install httpx import os import httpx response = httpx.post( "https://api.aigently.ai/v1/test/inbound-calls", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, json={ "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "from_number": "+12025550100", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Make a test call to an agent. Test keys only. It asks the agent's test lookup and plays a scripted call. package main import ( "bytes" "fmt" "io" "net/http" "os" ) func main() { payload := []byte(`{ "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "from_number": "+12025550100" }`) request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/test/inbound-calls", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) request.Header.Set("Content-Type", "application/json") response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Make a test call to an agent. Test keys only. It asks the agent's test lookup and plays a scripted call. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { String body = """ { "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "from_number": "+12025550100" } """; HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/test/inbound-calls")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body)) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Make a test call to an agent. Test keys only. It asks the agent's test lookup and plays a scripted call. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/test/inbound-calls") { Content = new StringContent( """ { "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "from_number": "+12025550100" } """, Encoding.UTF8, "application/json"), }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "agent_id" => "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "from_number" => "+12025550100", ]), ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` The answer says what the lookup made of it — where the values came from, what was accepted and ignored, and the greeting they produced — and the call then plays out like any test call. With [`aigently listen`](/cli) and `--forward-lookups-to`, the lookup reaches your own machine. --- # Chat and streaming > Hold a conversation with an agent from your own app — one message at a time, as a whole reply or as it is written. The same agents your visitors talk to on your website can chat from your app, your back office or a channel we do not support yet. A chat needs the `chats:write` permission. ## Start a chat The first message starts it: **curl** ```sh #!/bin/sh # Start a chat curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/chats" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" \ -H "Idempotency-Key: chat-20931" \ --json '{ "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "message": "Hello, I would like to book a table.", "variables": { "first_name": "Sara" } }' ``` **JavaScript** ```js // Start a chat const response = await fetch("https://api.aigently.ai/v1/chats", { method: "POST", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": "chat-20931", }, body: JSON.stringify({ "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "message": "Hello, I would like to book a table.", "variables": { "first_name": "Sara" } }), }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Start a chat # pip install httpx import os import httpx response = httpx.post( "https://api.aigently.ai/v1/chats", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", "Idempotency-Key": "chat-20931", }, json={ "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "message": "Hello, I would like to book a table.", "variables": { "first_name": "Sara", }, }, timeout=60, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Start a chat package main import ( "bytes" "fmt" "io" "net/http" "os" ) func main() { payload := []byte(`{ "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "message": "Hello, I would like to book a table.", "variables": { "first_name": "Sara" } }`) request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/chats", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) request.Header.Set("Content-Type", "application/json") request.Header.Set("Idempotency-Key", "chat-20931") response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Start a chat import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { String body = """ { "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "message": "Hello, I would like to book a table.", "variables": { "first_name": "Sara" } } """; HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/chats")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .header("Content-Type", "application/json") .header("Idempotency-Key", "chat-20931") .method("POST", HttpRequest.BodyPublishers.ofString(body)) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Start a chat using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/chats") { Content = new StringContent( """ { "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "message": "Hello, I would like to book a table.", "variables": { "first_name": "Sara" } } """, Encoding.UTF8, "application/json"), }; request.Headers.Add("Idempotency-Key", "chat-20931"); var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), "Content-Type: application/json", "Idempotency-Key: chat-20931", ], CURLOPT_POSTFIELDS => json_encode([ "agent_id" => "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "message" => "Hello, I would like to book a table.", "variables" => [ "first_name" => "Sara", ], ]), ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` The answer is a `chat.turn`: the agent's greeting in `opening` (first turn only — show it above the reply), its `reply`, and the `conversation_id` to send the next message to. `variables` are read only when a chat starts, so nothing said later can change what the agent was told about the person. An `Idempotency-Key` is optional here: the same request with the same key returns the same chat. ## Send the next message **curl** ```sh #!/bin/sh # Send the next message curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/conversations/2e4a6c8d-0f1b-4d3e-a5c7-9e1b3d5f7a9c/messages" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" \ --json '{ "message": "For four people, at seven." }' ``` **JavaScript** ```js // Send the next message const response = await fetch("https://api.aigently.ai/v1/conversations/2e4a6c8d-0f1b-4d3e-a5c7-9e1b3d5f7a9c/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "message": "For four people, at seven." }), }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Send the next message # pip install httpx import os import httpx response = httpx.post( "https://api.aigently.ai/v1/conversations/2e4a6c8d-0f1b-4d3e-a5c7-9e1b3d5f7a9c/messages", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, json={ "message": "For four people, at seven.", }, timeout=60, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Send the next message package main import ( "bytes" "fmt" "io" "net/http" "os" ) func main() { payload := []byte(`{ "message": "For four people, at seven." }`) request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/conversations/2e4a6c8d-0f1b-4d3e-a5c7-9e1b3d5f7a9c/messages", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) request.Header.Set("Content-Type", "application/json") response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Send the next message import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { String body = """ { "message": "For four people, at seven." } """; HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations/2e4a6c8d-0f1b-4d3e-a5c7-9e1b3d5f7a9c/messages")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body)) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Send the next message using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/conversations/2e4a6c8d-0f1b-4d3e-a5c7-9e1b3d5f7a9c/messages") { Content = new StringContent( """ { "message": "For four people, at seven." } """, Encoding.UTF8, "application/json"), }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "message" => "For four people, at seven.", ]), ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` The agent keeps the history; you keep the id. There is nothing else to keep in step — no turn to count, no transcript to send. **One message at a time.** A message sent while the agent is still answering the previous one is refused with `409 conversation_busy`: wait for the reply. A message sent again with the same `Idempotency-Key` returns the same reply without asking the agent twice — streamed, if the retry asks for a stream. If the first attempt's reply failed, the retry gets that failure again (`502 upstream_unavailable`); send the message under a new key to ask once more. A chat follows its agent: once the agent is unpublished, archived or deleted, the chat takes no more messages (`409 agent_not_published`), and each message is answered by the agent's published version, never by edits that have not been published. ## Stream the reply Send `stream: true` — or `Accept: text/event-stream` — and the reply arrives as it is written, as server-sent events: **curl** ```sh #!/bin/sh # Stream the reply as it is written curl -sS --fail-with-body -N -X POST "https://api.aigently.ai/v1/conversations/2e4a6c8d-0f1b-4d3e-a5c7-9e1b3d5f7a9c/messages" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" \ --json '{ "message": "Can I bring my dog?", "stream": true }' ``` **JavaScript** ```js // Stream the reply as it is written const response = await fetch("https://api.aigently.ai/v1/conversations/2e4a6c8d-0f1b-4d3e-a5c7-9e1b3d5f7a9c/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "message": "Can I bring my dog?", "stream": true }), }); if (!response.ok) { const problem = await response.json(); throw new Error(`${problem.code}: ${problem.detail}`); } // Each event is an "event:" line, a "data:" line of JSON, and a blank line. const decoder = new TextDecoder(); let buffer = ""; for await (const chunk of response.body) { buffer += decoder.decode(chunk, { stream: true }); let end; while ((end = buffer.indexOf("\n\n")) >= 0) { const [eventLine, dataLine] = buffer.slice(0, end).split("\n"); buffer = buffer.slice(end + 2); const event = eventLine.slice("event: ".length); const data = JSON.parse(dataLine.slice("data: ".length)); if (event === "delta") process.stdout.write(data.text); if (event === "done") console.log("\n", data); if (event === "error") throw new Error(`${data.code}: ${data.detail}`); } } ``` **Python** ```python # Stream the reply as it is written # pip install httpx import json import os import httpx with httpx.stream( "POST", "https://api.aigently.ai/v1/conversations/2e4a6c8d-0f1b-4d3e-a5c7-9e1b3d5f7a9c/messages", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, json={ "message": "Can I bring my dog?", "stream": True, }, timeout=60, ) as response: if response.is_error: raise SystemExit(response.read().decode()) # Each event is an "event:" line, a "data:" line of JSON, and a blank line. event = None for line in response.iter_lines(): if line.startswith("event: "): event = line.removeprefix("event: ") elif line.startswith("data: "): data = json.loads(line.removeprefix("data: ")) if event == "delta": print(data["text"], end="", flush=True) elif event == "done": print("\n", data) elif event == "error": raise SystemExit(f"{data['code']}: {data['detail']}") ``` **Go** ```go // Stream the reply as it is written package main import ( "bufio" "bytes" "encoding/json" "fmt" "io" "net/http" "os" "strings" ) func main() { payload := []byte(`{ "message": "Can I bring my dog?", "stream": true }`) request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/conversations/2e4a6c8d-0f1b-4d3e-a5c7-9e1b3d5f7a9c/messages", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) request.Header.Set("Content-Type", "application/json") response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() if response.StatusCode >= 400 { body, _ := io.ReadAll(response.Body) fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } // Each event is an "event:" line, a "data:" line of JSON, and a blank line. scanner := bufio.NewScanner(response.Body) scanner.Buffer(make([]byte, 64*1024), 1024*1024) event := "" for scanner.Scan() { line := scanner.Text() if name, found := strings.CutPrefix(line, "event: "); found { event = name continue } data, found := strings.CutPrefix(line, "data: ") if !found { continue } switch event { case "delta": var delta struct { Text string `json:"text"` } json.Unmarshal([]byte(data), &delta) fmt.Print(delta.Text) case "done": fmt.Println("\n" + data) case "error": fmt.Fprintln(os.Stderr, data) os.Exit(1) } } } ``` **Java** ```java // Stream the reply as it is written import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.stream.Stream; public class Main { public static void main(String[] args) throws Exception { String body = """ { "message": "Can I bring my dog?", "stream": true } """; HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations/2e4a6c8d-0f1b-4d3e-a5c7-9e1b3d5f7a9c/messages")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body)) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse> response = client.send(request, HttpResponse.BodyHandlers.ofLines()); if (response.statusCode() >= 400) { response.body().forEach(System.err::println); System.exit(1); } // Each event is an "event:" line, a "data:" line of JSON, and a blank line. // Read each data line with your JSON library. response.body().filter(line -> !line.isEmpty()).forEach(System.out::println); } } ``` **C#** ```csharp // Stream the reply as it is written using System.Net.Http.Headers; using System.Text; using System.Text.Json; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/conversations/2e4a6c8d-0f1b-4d3e-a5c7-9e1b3d5f7a9c/messages") { Content = new StringContent( """ { "message": "Can I bring my dog?", "stream": true } """, Encoding.UTF8, "application/json"), }; using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(await response.Content.ReadAsStringAsync()); return 1; } // Each event is an "event:" line, a "data:" line of JSON, and a blank line. using var reader = new StreamReader(await response.Content.ReadAsStreamAsync()); var eventName = ""; while (await reader.ReadLineAsync() is { } line) { if (line.StartsWith("event: ")) { eventName = line["event: ".Length..]; continue; } if (!line.StartsWith("data: ")) { continue; } var data = line["data: ".Length..]; if (eventName == "delta") { Console.Write(JsonDocument.Parse(data).RootElement.GetProperty("text").GetString()); } else if (eventName == "done") { Console.WriteLine("\n" + data); } else if (eventName == "error") { Console.Error.WriteLine(data); return 1; } } return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "message" => "Can I bring my dog?", "stream" => true, ]), // Each event is an "event:" line, a "data:" line of JSON, and a blank line. CURLOPT_WRITEFUNCTION => function ($curl, $chunk) use (&$event, &$buffer, &$failed) { $buffer .= $chunk; while (($end = strpos($buffer, "\n")) !== false) { $line = substr($buffer, 0, $end); $buffer = substr($buffer, $end + 1); if (str_starts_with($line, "event: ")) { $event = substr($line, 7); } elseif (str_starts_with($line, "data: ")) { $data = substr($line, 6); if ($event === "delta") { echo json_decode($data, true)["text"]; } elseif ($event === "done") { echo "\n", $data, "\n"; } elseif ($event === "error") { fwrite(STDERR, $data . "\n"); $failed = true; } } } return strlen($chunk); }, ]); curl_exec($curl); if ($failed || curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $buffer . "\n"); exit(1); } ``` | Event | `data` | |---|---| | `delta` | `{"text": "…"}` — the next words of the reply. | | `status` | `{"text": "…"}` — the agent started using one of its tools, in words for a person. | | `done` | The whole `chat.turn`, exactly as the answer without streaming. | | `error` | A [problem document](/errors) — the headers are long gone, so this is how a stream fails. | **Read `done` rather than gluing the deltas together.** It carries the conversation id, what was collected and whether the agent has finished, which the deltas do not. ## Send a picture Where the agent accepts images, upload one first, then name it in the next message's `attachment_ids` — up to four a message: **curl** ```sh #!/bin/sh # Upload a picture to send. Send the id it returns in a message's attachment_ids. curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/attachments" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" \ -F "agent_id=3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f" \ -F "file=@photo.jpg;type=image/jpeg" ``` **JavaScript** ```js import { readFile } from "node:fs/promises"; // Upload a picture to send. Send the id it returns in a message's attachment_ids. const form = new FormData(); form.append("agent_id", "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f"); form.append("file", new Blob([await readFile("photo.jpg")], { type: "image/jpeg" }), "photo.jpg"); const response = await fetch("https://api.aigently.ai/v1/attachments", { method: "POST", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, body: form, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Upload a picture to send. Send the id it returns in a message's attachment_ids. # pip install httpx import os import httpx with open("photo.jpg", "rb") as upload: response = httpx.post( "https://api.aigently.ai/v1/attachments", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, data={ "agent_id": "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", }, files={"file": ("photo.jpg", upload, "image/jpeg")}, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Upload a picture to send. Send the id it returns in a message's attachment_ids. package main import ( "bytes" "fmt" "io" "mime/multipart" "net/http" "os" ) func main() { var form bytes.Buffer writer := multipart.NewWriter(&form) writer.WriteField("agent_id", "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f") part, _ := writer.CreateFormFile("file", "photo.jpg") contents, err := os.ReadFile("photo.jpg") if err != nil { panic(err) } part.Write(contents) writer.Close() request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/attachments", &form) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) request.Header.Set("Content-Type", writer.FormDataContentType()) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Upload a picture to send. Send the id it returns in a message's attachment_ids. import java.io.ByteArrayOutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { String boundary = Long.toHexString(System.nanoTime()); ByteArrayOutputStream form = new ByteArrayOutputStream(); form.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"agent_id\"\r\n\r\n3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f\r\n--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"photo.jpg\"\r\n" + "Content-Type: image/jpeg\r\n\r\n").getBytes()); form.write(Files.readAllBytes(Path.of("photo.jpg"))); form.write(("\r\n--" + boundary + "--\r\n").getBytes()); HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/attachments")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .header("Content-Type", "multipart/form-data; boundary=" + boundary) .method("POST", HttpRequest.BodyPublishers.ofByteArray(form.toByteArray())) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Upload a picture to send. Send the id it returns in a message's attachment_ids. using System.Net.Http.Headers; using System.Text; var form = new MultipartFormDataContent(); form.Add(new StringContent("3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f"), "agent_id"); var upload = new ByteArrayContent(File.ReadAllBytes("photo.jpg")); upload.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg"); form.Add(upload, "file", "photo.jpg"); using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/attachments") { Content = form, }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], CURLOPT_POSTFIELDS => [ "agent_id" => "3c7d9e1f-5a2b-4c6d-8e0f-1a3b5c7d9e2f", "file" => new CURLFile("photo.jpg", "image/jpeg"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` A picture nobody sends is deleted after six hours. ## End it **curl** ```sh #!/bin/sh # End a call or a chat curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/end" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // End a call or a chat const response = await fetch("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/end", { method: "POST", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # End a call or a chat # pip install httpx import os import httpx response = httpx.post( "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/end", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // End a call or a chat package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/end", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // End a call or a chat import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/end")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("POST", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // End a call or a chat using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/end") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], CURLOPT_POSTFIELDS => "", ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` A chat also ends on its own after a while without messages. A message to an ended chat is refused with `409 conversation_ended`; start a new one. ## Limits | Limit | | |---|---| | A message | 4,000 characters. | | Messages in one conversation | 20 a minute, and a ceiling on the whole conversation — `conversation_limit_reached` past it. | | Messages per key | 1,200 a minute. | Chats are charged like chats in the console, from the organization's credit — chats made with a test key too, because they use the agent's real model. When the credit runs out, a message is refused with `402 insufficient_credit`. --- # Reading and syncing conversations > List, search and read conversations, their transcripts and recordings, keep your own copy up to date, and delete them. Every call and chat is a **conversation**, read the same way whichever door it came through: the console, the widget, a phone line, a campaign or this API. Reading needs `conversations:read`; a transcript also needs `transcripts:read`, and a recording `recordings:read`. ## List them **curl** ```sh #!/bin/sh # List conversations curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/conversations?agent_id=8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c&status=completed&limit=10" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // List conversations const response = await fetch("https://api.aigently.ai/v1/conversations?agent_id=8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c&status=completed&limit=10", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # List conversations # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/conversations", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, params={ "agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c", "status": "completed", "limit": 10, }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // List conversations package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/conversations?agent_id=8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c&status=completed&limit=10", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // List conversations import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations?agent_id=8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c&status=completed&limit=10")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // List conversations using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/conversations?agent_id=8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c&status=completed&limit=10") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` Newest first, 20 a page unless you ask for up to 100. Filter by agent, project, channel, direction, status (repeat it for several), disposition, the analysis's outcome, who ended it, mode, numbers, campaign, your own `external_id`, and when it started or changed. Each page has `has_more` and a `next_cursor`: send it back as `cursor`, with the same filters, for the next page. A cursor works only for the key, the mode and the filters that made it. ## Find one by what was said **curl** ```sh #!/bin/sh # Find conversations by what was said. Each one comes with the passage that matched. curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/conversations?q=refund&limit=10" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Find conversations by what was said. Each one comes with the passage that matched. const response = await fetch("https://api.aigently.ai/v1/conversations?q=refund&limit=10", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Find conversations by what was said. Each one comes with the passage that matched. # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/conversations", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, params={ "q": "refund", "limit": 10, }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Find conversations by what was said. Each one comes with the passage that matched. package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/conversations?q=refund&limit=10", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Find conversations by what was said. Each one comes with the passage that matched. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations?q=refund&limit=10")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Find conversations by what was said. Each one comes with the passage that matched. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/conversations?q=refund&limit=10") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` `q` searches the transcripts, the same search as the console's, and needs `transcripts:read` as well. A word also finds longer words that start with it, so `refund` finds `refunded`. Put a phrase in quotes to find it as written, and write `-word` to leave out conversations that have the word. Every other filter still applies. Each result carries `match`: the passage that matched, as plain text. A key may search 30 times a minute. A conversation can be found about a minute after it ends. ## Read one **curl** ```sh #!/bin/sh # Read a conversation with its transcript curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Read a conversation with its transcript const response = await fetch("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Read a conversation with its transcript # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, params={ "include": "transcript", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Read a conversation with its transcript package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Read a conversation with its transcript import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Read a conversation with its transcript using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` The [conversation](/reference/objects/Conversation) carries its status and how it ended, its numbers, the values it started with and where each came from, your metadata, the lookup's result, the answers collected, the analysis, the recording's state and its times. `include=transcript` adds what was said. The transcript alone: **curl** ```sh #!/bin/sh # Read a transcript curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/transcript" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Read a transcript const response = await fetch("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/transcript", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Read a transcript # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/transcript", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Read a transcript package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/transcript", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Read a transcript import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/transcript")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Read a transcript using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/transcript") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` Entries are in order: `user` is the person, `assistant` the agent, `tool` a tool the agent used, and `note` something the platform recorded, like a handover. ## The recording A link to the audio works for **five minutes**, so download it straight away rather than storing the link: **curl** ```sh #!/bin/sh # Get a link to a recording. The link works for five minutes, so download the audio straight away. curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/recording" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Get a link to a recording. The link works for five minutes, so download the audio straight away. const response = await fetch("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/recording", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Get a link to a recording. The link works for five minutes, so download the audio straight away. # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/recording", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Get a link to a recording. The link works for five minutes, so download the audio straight away. package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/recording", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Get a link to a recording. The link works for five minutes, so download the audio straight away. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/recording")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Get a link to a recording. The link works for five minutes, so download the audio straight away. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/recording") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` The file is stereo Ogg: the caller on the left, the agent on the right. Each key may ask for 60 links a minute and 2,000 a day, and every link is recorded in your organization's audit log. ## What it cost Ask for `include=cost`, which needs `billing:read` as well: **curl** ```sh #!/bin/sh # Read what a conversation cost curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=cost" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Read what a conversation cost const response = await fetch("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=cost", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Read what a conversation cost # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, params={ "include": "cost", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Read what a conversation cost package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=cost", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Read what a conversation cost import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=cost")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Read what a conversation cost using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=cost") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` `cost.credits` is what the conversation cost: its own charge and every analysis of it, less any refund. A chat is charged as it goes, so an open chat's cost can still grow. Every charge behind the number is in the [credit history](/guides/billing). ## Run the analysis again Run the agent's analysis on a conversation that has ended: again, after you change the agent's analysis settings, or for the first time on one that was never analysed. This needs `analysis:run`: **curl** ```sh #!/bin/sh # Analyse a conversation again. It costs credit, like any analysis. curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/analysis" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Analyse a conversation again. It costs credit, like any analysis. const response = await fetch("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/analysis", { method: "POST", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Analyse a conversation again. It costs credit, like any analysis. # pip install httpx import os import httpx response = httpx.post( "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/analysis", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Analyse a conversation again. It costs credit, like any analysis. package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/analysis", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Analyse a conversation again. It costs credit, like any analysis. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/analysis")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("POST", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Analyse a conversation again. It costs credit, like any analysis. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/analysis") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], CURLOPT_POSTFIELDS => "", ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` The answer is the conversation with its new `analysis`, and `conversation.analyzed` is sent again. Each run costs credit, like the first one, and an organization may run 10 a minute. A conversation still in progress is refused with `409 conversation_in_progress`, and one where too little was said with `422 conversation_too_short`. ## Keep your copy in sync Ask for what changed instead of reading everything again: **curl** ```sh #!/bin/sh # Sync what changed. Send each page's next_cursor back as cursor= until has_more is false. curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/conversations?order=changed&changed_after=2026-10-01T00:00:00Z" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Sync what changed. Send each page's next_cursor back as cursor= until has_more is false. const response = await fetch("https://api.aigently.ai/v1/conversations?order=changed&changed_after=2026-10-01T00:00:00Z", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Sync what changed. Send each page's next_cursor back as cursor= until has_more is false. # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/conversations", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, params={ "order": "changed", "changed_after": "2026-10-01T00:00:00Z", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Sync what changed. Send each page's next_cursor back as cursor= until has_more is false. package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/conversations?order=changed&changed_after=2026-10-01T00:00:00Z", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Sync what changed. Send each page's next_cursor back as cursor= until has_more is false. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations?order=changed&changed_after=2026-10-01T00:00:00Z")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Sync what changed. Send each page's next_cursor back as cursor= until has_more is false. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/conversations?order=changed&changed_after=2026-10-01T00:00:00Z") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` With `order=changed`, the oldest change comes first. Read until `has_more` is `false`, keep the last page's `next_cursor`, and next time start from it: you get every conversation that changed since, once, in order. Changes from the last minute wait for a later page, so a conversation still being written is never skipped. Every conversation has a `revision` that only goes up: of two copies, keep the higher. ## Delete one Deleting needs `conversations:delete`, a permission no starting point in the console includes: **curl** ```sh #!/bin/sh # Delete a conversation. With its recording, for good. curl -sS --fail-with-body -X DELETE "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // Delete a conversation. With its recording, for good. const response = await fetch("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # Delete a conversation. With its recording, for good. # pip install httpx import os import httpx response = httpx.delete( "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // Delete a conversation. With its recording, for good. package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("DELETE", "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // Delete a conversation. With its recording, for good. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("DELETE", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // Delete a conversation. With its recording, for good. using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Delete, "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php "DELETE", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` The transcript, the recording and any pictures go with it, for good. A call still in progress is refused with `409 conversation_in_progress`: end it first. A chat that is still open is charged for what it used, then deleted. What it cost stays in the credit history. It is listed below with the reason `api`, `conversation.deleted` is sent to your receivers, and the organization's audit log records which key deleted it. ## Deleted conversations A conversation can be deleted — by the organization's retention, from the console, through this API, or with its agent or project. Ask for those too, and remove them from your copy: **curl** ```sh #!/bin/sh # List deleted conversations curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/conversations/deleted" \ -H "Authorization: Bearer $AIGENTLY_API_KEY" ``` **JavaScript** ```js // List deleted conversations const response = await fetch("https://api.aigently.ai/v1/conversations/deleted", { method: "GET", headers: { Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`, }, }); const answer = await response.json(); if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`); console.log(answer); ``` **Python** ```python # List deleted conversations # pip install httpx import os import httpx response = httpx.get( "https://api.aigently.ai/v1/conversations/deleted", headers={ "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}", }, ) if response.is_error: raise SystemExit(response.text) print(response.json()) ``` **Go** ```go // List deleted conversations package main import ( "fmt" "io" "net/http" "os" ) func main() { request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/conversations/deleted", nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode >= 400 { fmt.Fprintln(os.Stderr, string(body)) os.Exit(1) } fmt.Println(string(body)) } ``` **Java** ```java // List deleted conversations import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/conversations/deleted")) .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY")) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every // server allows. HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { System.err.println(response.body()); System.exit(1); } System.out.println(response.body()); } } ``` **C#** ```csharp // List deleted conversations using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY")); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/conversations/deleted") { }; var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine(body); return 1; } Console.WriteLine(body); return 0; ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"), ], ]); $body = curl_exec($curl); if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) { fwrite(STDERR, $body . "\n"); exit(1); } echo $body, "\n"; ``` Deletions are listed for 30 days. ## What a test key sees A test key reads, searches and deletes only conversations made with test keys, and a live key never sees them. A real conversation asked for with a test key is `404`, as if it did not exist. Running the analysis again on a test call gives its sample analysis again, at no cost; a test chat is analysed by the agent's real model, and charged. --- # Webhooks > Hear about every conversation as it happens, signed so you know it came from Aigently. A webhook receiver is an address on your server that the platform POSTs events to. Add one in the console under **Webhooks**, choose its events, and keep its signing secret, which is shown once. ## Events about a conversation | Event | When | |---|---| | `conversation.started` | A call was answered, a browser caller joined, or a chat's first message arrived. | | `conversation.ended` | Once, the first time a conversation ends — answered or not. | | `conversation.updated` | Something changed after the end — an outcome corrected, for one. | | `conversation.analyzed` | The analysis finished: outcome, sentiment, the checks it scored. | | `conversation.recording_ready` | The recording is stored. It carries no link: [ask for one](/guides/conversations#the-recording) when you need it. | | `conversation.deleted` | Removed by retention, from the console, [through the API](/guides/conversations#delete-one), or with its agent or project. | | `conversation.transcript` | The whole transcript. It carries what was said, so it is off until you choose it. | ## Events about an agent `agent.updated` is sent when an agent's fields change: the [variables](/guides/variables) a conversation can start with, the answers it collects, or what its analysis writes. An edit to a published agent takes effect from the next conversation, so an edit that adds a required variable is one your integration needs to hear about before its next call. It carries the agent as [Get an agent](/reference/getAgent) describes it, with the three lists, `analysis_enabled`, and `changed`, which names the lists that changed. Each list has a `version`: keep it, and you can tell a change to the fields from any other edit. An edit that changes no field — a new name, reworded instructions with the same placeholders — sends nothing. It goes to live receivers only: an agent's fields are not test data. A receiver made before the event existed hears it once `agent.updated` is added to its `events`. ## Events about a campaign A [campaign](/guides/campaigns) sends `campaign.started`, `campaign.paused`, `campaign.completed` and `campaign.cancelled` as it moves — whether you moved it, somebody did in the console, or it paused itself — and `campaign.contact_finished` with one person's final result. They go to live receivers only. There are more — forms, workflow steps, appointments, handovers — listed beside each receiver in the console. Every event of the last 30 days is also in the [event feed](/guides/events), whether or not a receiver heard it. Which events a call sends as it ends is in the [endings table](/guides/outbound-calls#how-a-call-ends). ## What you receive ```json { "id": "0f9a7c2e-5b1d-4e8a-9c3f-2d6b8e1a4c7f", "event": "conversation.ended", "created_at": "2026-10-07T09:14:03.120394+00:00", "livemode": true, "revision": 6, "data": { "object": "conversation", "id": "5f0c9a52-…", "status": "completed", "…": "…" } } ``` `data` is the [conversation](/reference/objects/Conversation), as the API reads it. `id` is the event's own id: the same event delivered twice has the same `id`, so keep the ones you have seen. Events can arrive out of order — keep the copy with the higher `revision`. Answer `2xx` quickly and do the work afterwards; anything else is retried with growing waits. ## Check the signature Every delivery is signed with your receiver's secret, the [Standard Webhooks](https://www.standardwebhooks.com/) way — so any of that standard's libraries checks it — in three headers: `webhook-id`, `webhook-timestamp` and `webhook-signature`. Check it against the body **exactly as it arrived**, before parsing it, and refuse anything older than five minutes. This function does all of it, in your language: **curl** ```sh #!/bin/sh # Check a saved delivery's signature by hand, with openssl: the body on standard input, the three # headers in WEBHOOK_ID, WEBHOOK_TIMESTAMP and WEBHOOK_SIGNATURE. A server should use one of the # other languages, which compare in constant time. set -eu body="$(mktemp)" trap 'rm -f "$body"' EXIT cat > "$body" now="$(date +%s)" age=$((now - WEBHOOK_TIMESTAMP)) if [ "${age#-}" -gt 300 ]; then echo "timestamp outside the tolerance" >&2 exit 1 fi key="$(printf '%s' "${AIGENTLY_WEBHOOK_SECRET#whsec_}" | base64 -d | od -An -v -tx1 | tr -d ' \n')" expected="$({ printf '%s.%s.' "$WEBHOOK_ID" "$WEBHOOK_TIMESTAMP"; cat "$body"; } \ | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$key" -binary | base64)" for entry in $WEBHOOK_SIGNATURE; do if [ "$entry" = "v1,$expected" ]; then echo "verified" exit 0 fi done echo "signature does not match" >&2 exit 1 ``` **JavaScript** ```js // Check a webhook's signature, then read its event. // The same function checks a context lookup request, with the lookup's secret. import { createHmac, timingSafeEqual } from "node:crypto"; import { text } from "node:stream/consumers"; const TOLERANCE_SECONDS = 5 * 60; // `body` is the request body exactly as it arrived: parse it only after this has checked it. // `headers` has lowercase names, as Node gives them. export function unwrap(body, headers, secret) { const id = headers["webhook-id"]; const timestamp = headers["webhook-timestamp"]; const signatures = headers["webhook-signature"]; if (!id || !timestamp || !signatures) throw new Error("missing webhook headers"); if (!(Math.abs(Date.now() / 1000 - Number(timestamp)) <= TOLERANCE_SECONDS)) { throw new Error("timestamp outside the tolerance"); } const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest(); const matched = signatures.split(" ").some((entry) => { const [version, signature] = entry.split(","); if (version !== "v1" || !signature) return false; const offered = Buffer.from(signature, "base64"); return offered.length === expected.length && timingSafeEqual(offered, expected); }); if (!matched) throw new Error("signature does not match"); return JSON.parse(body); } // Try it with a delivery: the body on standard input, the three headers in the environment. const event = unwrap( await text(process.stdin), { "webhook-id": process.env.WEBHOOK_ID, "webhook-timestamp": process.env.WEBHOOK_TIMESTAMP, "webhook-signature": process.env.WEBHOOK_SIGNATURE, }, process.env.AIGENTLY_WEBHOOK_SECRET, ); console.log(`verified ${event.event} ${event.id}`); ``` **Python** ```python # Check a webhook's signature, then read its event. # The same function checks a context lookup request, with the lookup's secret. import base64 import hashlib import hmac import json import os import sys import time TOLERANCE_SECONDS = 5 * 60 def unwrap(body: bytes, headers, secret: str) -> dict: """The event, once its signature is checked. Raises ValueError otherwise. `body` is the request body exactly as it arrived: parse it only after this has checked it. `headers` is any mapping of the request's headers; most frameworks read theirs in any case. """ message_id = headers.get("webhook-id") timestamp = headers.get("webhook-timestamp") signatures = headers.get("webhook-signature") if not message_id or not timestamp or not signatures: raise ValueError("missing webhook headers") if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS: raise ValueError("timestamp outside the tolerance") key = base64.b64decode(secret.removeprefix("whsec_")) signed = f"{message_id}.{timestamp}.".encode() + body expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode() for entry in signatures.split(" "): version, _, signature = entry.partition(",") if version == "v1" and hmac.compare_digest(signature, expected): return json.loads(body) raise ValueError("signature does not match") if __name__ == "__main__": # Try it with a delivery: the body on standard input, the three headers in the environment. event = unwrap( sys.stdin.buffer.read(), { "webhook-id": os.environ["WEBHOOK_ID"], "webhook-timestamp": os.environ["WEBHOOK_TIMESTAMP"], "webhook-signature": os.environ["WEBHOOK_SIGNATURE"], }, os.environ["AIGENTLY_WEBHOOK_SECRET"], ) print("verified", event["event"], event["id"]) ``` **Go** ```go // Check a webhook's signature, then read its event. // The same function checks a context lookup request, with the lookup's secret. package main import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "os" "strconv" "strings" "time" ) const toleranceSeconds = 5 * 60 // Unwrap checks the signature on a webhook and returns its event. body is the request body exactly // as it arrived: parse it only after this has checked it. func Unwrap(body []byte, headers http.Header, secret string) (map[string]any, error) { id := headers.Get("webhook-id") timestamp := headers.Get("webhook-timestamp") signatures := headers.Get("webhook-signature") if id == "" || timestamp == "" || signatures == "" { return nil, errors.New("missing webhook headers") } sent, err := strconv.ParseInt(timestamp, 10, 64) if err != nil || time.Since(time.Unix(sent, 0)).Abs() > toleranceSeconds*time.Second { return nil, errors.New("timestamp outside the tolerance") } key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_")) if err != nil { return nil, errors.New("not a webhook signing secret") } mac := hmac.New(sha256.New, key) mac.Write([]byte(id + "." + timestamp + ".")) mac.Write(body) expected := mac.Sum(nil) for _, entry := range strings.Split(signatures, " ") { version, signature, _ := strings.Cut(entry, ",") offered, err := base64.StdEncoding.DecodeString(signature) if version == "v1" && err == nil && hmac.Equal(offered, expected) { var event map[string]any if err := json.Unmarshal(body, &event); err != nil { return nil, err } return event, nil } } return nil, errors.New("signature does not match") } func main() { // Try it with a delivery: the body on standard input, the three headers in the environment. body, _ := io.ReadAll(os.Stdin) headers := http.Header{} headers.Set("webhook-id", os.Getenv("WEBHOOK_ID")) headers.Set("webhook-timestamp", os.Getenv("WEBHOOK_TIMESTAMP")) headers.Set("webhook-signature", os.Getenv("WEBHOOK_SIGNATURE")) event, err := Unwrap(body, headers, os.Getenv("AIGENTLY_WEBHOOK_SECRET")) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println("verified", event["event"], event["id"]) } ``` **Java** ```java // Check a webhook's signature. Once it passes, read the event with your JSON library. // The same method checks a context lookup request, with the lookup's secret. import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.Base64; import java.util.Map; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; public class Main { static final long TOLERANCE_SECONDS = 5 * 60; /** * The body, once its signature is checked; throws otherwise. `body` is the request body exactly * as it arrived, and `headers` has lowercase names. */ static String unwrap(byte[] body, Map headers, String secret) throws Exception { String id = headers.get("webhook-id"); String timestamp = headers.get("webhook-timestamp"); String signatures = headers.get("webhook-signature"); if (id == null || timestamp == null || signatures == null) { throw new SecurityException("missing webhook headers"); } long now = System.currentTimeMillis() / 1000; if (!timestamp.matches("\\d{1,12}") || Math.abs(now - Long.parseLong(timestamp)) > TOLERANCE_SECONDS) { throw new SecurityException("timestamp outside the tolerance"); } byte[] key = Base64.getDecoder().decode(secret.replaceFirst("^whsec_", "")); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(key, "HmacSHA256")); mac.update((id + "." + timestamp + ".").getBytes(StandardCharsets.UTF_8)); byte[] expected = mac.doFinal(body); for (String entry : signatures.split(" ")) { String[] parts = entry.split(",", 2); if (parts.length != 2 || !parts[0].equals("v1")) { continue; } byte[] offered; try { offered = Base64.getDecoder().decode(parts[1]); } catch (IllegalArgumentException malformed) { continue; } if (MessageDigest.isEqual(offered, expected)) { return new String(body, StandardCharsets.UTF_8); } } throw new SecurityException("signature does not match"); } public static void main(String[] args) throws Exception { // Try it with a delivery: the body on standard input, the three headers in the environment. String event = unwrap( System.in.readAllBytes(), Map.of( "webhook-id", System.getenv("WEBHOOK_ID"), "webhook-timestamp", System.getenv("WEBHOOK_TIMESTAMP"), "webhook-signature", System.getenv("WEBHOOK_SIGNATURE")), System.getenv("AIGENTLY_WEBHOOK_SECRET")); System.out.println("verified " + event); } } ``` **C#** ```csharp // Check a webhook's signature, then read its event. // The same function checks a context lookup request, with the lookup's secret. using System.Security.Cryptography; using System.Text; using System.Text.Json; // Try it with a delivery: the body on standard input, the three headers in the environment. using var input = new MemoryStream(); Console.OpenStandardInput().CopyTo(input); using var webhookEvent = Webhooks.Unwrap( input.ToArray(), new Dictionary { ["webhook-id"] = Environment.GetEnvironmentVariable("WEBHOOK_ID"), ["webhook-timestamp"] = Environment.GetEnvironmentVariable("WEBHOOK_TIMESTAMP"), ["webhook-signature"] = Environment.GetEnvironmentVariable("WEBHOOK_SIGNATURE"), }, Environment.GetEnvironmentVariable("AIGENTLY_WEBHOOK_SECRET") ?? ""); var root = webhookEvent.RootElement; Console.WriteLine($"verified {root.GetProperty("event")} {root.GetProperty("id")}"); static class Webhooks { const long ToleranceSeconds = 5 * 60; // The event, once its signature is checked; throws otherwise. `body` is the request body // exactly as it arrived: parse it only after this has checked it. public static JsonDocument Unwrap( byte[] body, IReadOnlyDictionary headers, string secret) { if (headers.GetValueOrDefault("webhook-id") is not { Length: > 0 } id || headers.GetValueOrDefault("webhook-timestamp") is not { Length: > 0 } timestamp || headers.GetValueOrDefault("webhook-signature") is not { Length: > 0 } signatures) { throw new CryptographicException("missing webhook headers"); } if (!long.TryParse(timestamp, out var sent) || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - sent) > ToleranceSeconds) { throw new CryptographicException("timestamp outside the tolerance"); } var key = Convert.FromBase64String(secret.StartsWith("whsec_") ? secret[6..] : secret); var signed = Encoding.UTF8.GetBytes($"{id}.{timestamp}.").Concat(body).ToArray(); var expected = Encoding.ASCII.GetBytes(Convert.ToBase64String(HMACSHA256.HashData(key, signed))); foreach (var entry in signatures.Split(' ')) { var parts = entry.Split(',', 2); if (parts.Length == 2 && parts[0] == "v1" && CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(parts[1]), expected)) { return JsonDocument.Parse(body); } } throw new CryptographicException("signature does not match"); } } ``` **PHP** ```php TOLERANCE_SECONDS) { throw new RuntimeException("timestamp outside the tolerance"); } $key = base64_decode(preg_replace('/^whsec_/', "", $secret), true); $expected = base64_encode(hash_hmac("sha256", "$id.$timestamp.$body", (string) $key, true)); foreach (explode(" ", $signatures) as $entry) { [$version, $signature] = array_pad(explode(",", $entry, 2), 2, ""); if ($version === "v1" && hash_equals($expected, $signature)) { return json_decode($body, true, flags: JSON_THROW_ON_ERROR); } } throw new RuntimeException("signature does not match"); } // Try it with a delivery: the body on standard input, the three headers in the environment. $event = aigently_unwrap( file_get_contents("php://stdin"), [ "webhook-id" => (string) getenv("WEBHOOK_ID"), "webhook-timestamp" => (string) getenv("WEBHOOK_TIMESTAMP"), "webhook-signature" => (string) getenv("WEBHOOK_SIGNATURE"), ], (string) getenv("AIGENTLY_WEBHOOK_SECRET"), ); echo "verified {$event["event"]} {$event["id"]}\n"; ``` The same function checks a [context lookup](/guides/inbound-calls) request, with the lookup's secret. Receivers made before this signature existed also get `X-Agently-Signature: t=