# 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<String> 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
<?php
// Make a test call to an agent. Test keys only. It asks the agent's test lookup and plays a scripted call.
$curl = curl_init("https://api.aigently.ai/v1/test/inbound-calls");
curl_setopt_array($curl, [
    CURLOPT_POST => 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.
