curl --request POST \
--url https://api.instaview.sk/agents/composer \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data @- <<EOF
{
"message": "Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they're a strong fit.",
"type": "PHONE",
"name": "Senior Developer Phone Screen",
"duration": 90.5,
"voiceId": "<string>",
"companyPhoneNumberId": "<string>",
"metadata": {},
"webhookId": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
EOFimport requests
url = "https://api.instaview.sk/agents/composer"
payload = {
"message": "Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they're a strong fit.",
"type": "PHONE",
"name": "Senior Developer Phone Screen",
"duration": 90.5,
"voiceId": "<string>",
"companyPhoneNumberId": "<string>",
"metadata": {},
"webhookId": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
message: 'Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they\'re a strong fit.',
type: 'PHONE',
name: 'Senior Developer Phone Screen',
duration: 90.5,
voiceId: '<string>',
companyPhoneNumberId: '<string>',
metadata: {},
webhookId: '3c90c3cc-0d44-4b50-8888-8dd25736052a'
})
};
fetch('https://api.instaview.sk/agents/composer', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.instaview.sk/agents/composer",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'message' => 'Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they\'re a strong fit.',
'type' => 'PHONE',
'name' => 'Senior Developer Phone Screen',
'duration' => 90.5,
'voiceId' => '<string>',
'companyPhoneNumberId' => '<string>',
'metadata' => [
],
'webhookId' => '3c90c3cc-0d44-4b50-8888-8dd25736052a'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.instaview.sk/agents/composer"
payload := strings.NewReader("{\n \"message\": \"Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they're a strong fit.\",\n \"type\": \"PHONE\",\n \"name\": \"Senior Developer Phone Screen\",\n \"duration\": 90.5,\n \"voiceId\": \"<string>\",\n \"companyPhoneNumberId\": \"<string>\",\n \"metadata\": {},\n \"webhookId\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.instaview.sk/agents/composer")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"message\": \"Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they're a strong fit.\",\n \"type\": \"PHONE\",\n \"name\": \"Senior Developer Phone Screen\",\n \"duration\": 90.5,\n \"voiceId\": \"<string>\",\n \"companyPhoneNumberId\": \"<string>\",\n \"metadata\": {},\n \"webhookId\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.instaview.sk/agents/composer")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"message\": \"Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they're a strong fit.\",\n \"type\": \"PHONE\",\n \"name\": \"Senior Developer Phone Screen\",\n \"duration\": 90.5,\n \"voiceId\": \"<string>\",\n \"companyPhoneNumberId\": \"<string>\",\n \"metadata\": {},\n \"webhookId\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}"
response = http.request(request)
puts response.read_body{
"sessionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"status": "processing",
"expiresAt": "2023-11-07T05:31:56Z"
}Design an Agent
Starts designing a complete custom agent from a plain-language brief. Returns 202 immediately with a sessionId; poll GET /agents/composer/{sessionId} until status is ready. Designing an agent is a language-model call that takes tens of seconds, which is why it does not happen inside this request. No agent is created until you post the finished agent (or composerSessionId) to POST /agents.
curl --request POST \
--url https://api.instaview.sk/agents/composer \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data @- <<EOF
{
"message": "Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they're a strong fit.",
"type": "PHONE",
"name": "Senior Developer Phone Screen",
"duration": 90.5,
"voiceId": "<string>",
"companyPhoneNumberId": "<string>",
"metadata": {},
"webhookId": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
EOFimport requests
url = "https://api.instaview.sk/agents/composer"
payload = {
"message": "Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they're a strong fit.",
"type": "PHONE",
"name": "Senior Developer Phone Screen",
"duration": 90.5,
"voiceId": "<string>",
"companyPhoneNumberId": "<string>",
"metadata": {},
"webhookId": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
message: 'Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they\'re a strong fit.',
type: 'PHONE',
name: 'Senior Developer Phone Screen',
duration: 90.5,
voiceId: '<string>',
companyPhoneNumberId: '<string>',
metadata: {},
webhookId: '3c90c3cc-0d44-4b50-8888-8dd25736052a'
})
};
fetch('https://api.instaview.sk/agents/composer', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.instaview.sk/agents/composer",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'message' => 'Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they\'re a strong fit.',
'type' => 'PHONE',
'name' => 'Senior Developer Phone Screen',
'duration' => 90.5,
'voiceId' => '<string>',
'companyPhoneNumberId' => '<string>',
'metadata' => [
],
'webhookId' => '3c90c3cc-0d44-4b50-8888-8dd25736052a'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.instaview.sk/agents/composer"
payload := strings.NewReader("{\n \"message\": \"Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they're a strong fit.\",\n \"type\": \"PHONE\",\n \"name\": \"Senior Developer Phone Screen\",\n \"duration\": 90.5,\n \"voiceId\": \"<string>\",\n \"companyPhoneNumberId\": \"<string>\",\n \"metadata\": {},\n \"webhookId\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.instaview.sk/agents/composer")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"message\": \"Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they're a strong fit.\",\n \"type\": \"PHONE\",\n \"name\": \"Senior Developer Phone Screen\",\n \"duration\": 90.5,\n \"voiceId\": \"<string>\",\n \"companyPhoneNumberId\": \"<string>\",\n \"metadata\": {},\n \"webhookId\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.instaview.sk/agents/composer")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"message\": \"Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they're a strong fit.\",\n \"type\": \"PHONE\",\n \"name\": \"Senior Developer Phone Screen\",\n \"duration\": 90.5,\n \"voiceId\": \"<string>\",\n \"companyPhoneNumberId\": \"<string>\",\n \"metadata\": {},\n \"webhookId\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}"
response = http.request(request)
puts response.read_body{
"sessionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"status": "processing",
"expiresAt": "2023-11-07T05:31:56Z"
}POST /agents. The 202 returned here created a
composer session, not an agent.It answers before the agent exists
Designing an agent is a language-model call that takes tens of seconds, so this route does not hold your request open for it. It returns 202 straight away with a session id, and the design happens in the background.POST /agents/composer
{
"type": "PHONE",
"message": "Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they're a strong fit."
}
{
"sessionId": "9f8c1d2e-4b7a-4c3d-9e1f-2a5b6c7d8e90",
"status": "processing",
"expiresAt": "2026-09-17T10:15:00.000Z"
}
GET /agents/composer/{sessionId} until it stops saying processing. It answers 202 while the work is running and 200 when it has finished, so you can branch on the status code without parsing the body:
GET /agents/composer/9f8c1d2e-4b7a-4c3d-9e1f-2a5b6c7d8e90 → 202
GET /agents/composer/9f8c1d2e-4b7a-4c3d-9e1f-2a5b6c7d8e90 → 202
GET /agents/composer/9f8c1d2e-4b7a-4c3d-9e1f-2a5b6c7d8e90 → 200, status "ready"
webhookId with the brief and we will call your
webhook when a turn finishes — composer.completed carrying the same agent the poll would
have given you, composer.failed carrying the reason and no agent.We only send the events that webhook subscribes to. Subscribing to one of the two is enough
to be accepted, and is a reasonable choice, but you then hear nothing about the other outcome:
subscribe to both, or poll for the one you did not take. Polling always works as well, and is
always authoritative.message, and the type of agent to design.
type is required and cannot change later. The channel decides which conversation
steps are legal at all — a transfer to a person exists only on PHONE — so it has to be
known before the agent is designed rather than guessed from your brief.name, language, duration, voiceId, backgroundSound, companyPhoneNumberId and metadata are for: pin the things you already know, and let the composer decide the rest.
callConfig — already in the shape
POST /agents accepts, so it survives whether you post
the preview back or just the composerSessionId. It is not a field of this route: to set one
exactly rather than describe it, send callConfig in your create call, where it wins over
whatever the composer chose. When neither of you says anything, a new agent calls Mon–Fri
09:00–18:00 in each contact’s own timezone.flow. flow, focus, guardrails, contextConfig and
analyticsConfig are what the composer writes for you, and sending any of them is
rejected with a 400 naming which. If you already have a flow, you do not need the
composer — post it straight to POST /agents.What the finished session contains
Oncestatus is ready, the poll response carries the agent and the conversation:
{
"sessionId": "9f8c1d2e-4b7a-4c3d-9e1f-2a5b6c7d8e90",
"status": "ready",
"agent": {
"name": "Senior Backend Phone Screen",
"type": "PHONE",
"language": "EN",
"flow": { "firstMessage": { "…": "…" }, "sections": [], "lastMessage": { "…": "…" } },
"contextConfig": { "…": "…" },
"analyticsConfig": { "…": "…" }
},
"messages": [
{
"id": "…",
"role": "assistant",
"content": "Your agent opens by introducing itself and confirming it's speaking with the right person. It then works through Go and Postgres experience, asks about notice period and salary expectation, and transfers strong candidates to a recruiter. Anyone who isn't a fit gets a polite close.",
"changes": ["Named it Senior Backend Phone Screen", "Set the language to EN", "Added Skills deep-dive"],
"createdAt": "2026-08-18T10:15:00.000Z"
}
],
"expiresAt": "2026-09-17T10:15:00.000Z"
}
changes is a short list of labels for what the composer did: on the first turn what it built, on later turns what changed.
When the design fails
A brief the composer cannot turn into a valid agent does not fail your request — your request was already answered. The session ends upfailed instead, and the poll tells you why:
{
"sessionId": "9f8c1d2e-4b7a-4c3d-9e1f-2a5b6c7d8e90",
"status": "failed",
"error": {
"code": "COMPOSER_INVALID_RESULT",
"message": "Could not generate a valid flow from that description. Try rephrasing or adding detail.",
"details": []
},
"expiresAt": "2026-09-17T10:15:00.000Z"
}
COMPOSER_INVALID_RESULT is worth rephrasing for. Any other code is worth retrying unchanged.
What to do with the preview
agent is a body POST /agents accepts verbatim — no ids, no schemaVersion, and a conditional’s fallthrough already named by defaultBranch. Nothing to strip, nothing to fix up.
Change something first
Create it
agent object, or just { "composerSessionId": "…" }.POST /agents
{ "composerSessionId": "9f8c1d2e-4b7a-4c3d-9e1f-2a5b6c7d8e90" }
{ "composerSessionId": "…", "name": "Backend Screen v2" } creates the previewed agent under a different name.
ready before creating. A session whose design has not finished has no agent
to create, and posting its id is a 409 telling you to poll. A session whose later
refinement is still running is fine: it still holds the last agent you were shown, and that
is what gets created.Sessions expire
A session lives 30 days. After that, a preview nobody accepted is deleted along with its conversation;expiresAt on every response tells you when. Creating the agent ends the clock — a session that became an agent is kept.
Rate limits
Every call here designs an agent with a language model, so these routes carry their own limit on top of your key’s usual allowance: 6 per minute and 60 per hour for this endpoint, 10 per minute and 120 per hour for refining. Exceeding it is a429.
Errors
These are the ways the request can fail. A design that fails after it has been accepted is not an error here — it is afailed session, described above.
| Status | When |
|---|---|
400 | message or type missing, malformed, or over 8,000 characters — or you sent flow, focus, guardrails, contextConfig or analyticsConfig, which the composer writes (property flow should not exist) |
403 | The webhookId you sent belongs to another company |
404 | The composer is not enabled for your account, or the webhookId does not exist |
422 | The webhookId you sent is not subscribed to composer.completed or composer.failed |
429 | Composer rate limit reached |
Authorizations
API key for authentication using Bearer scheme
Query Parameters
Required for ATS API keys to specify which company to access. Ignored for standard company API keys.
Body
What the agent should do, in plain language. The composer designs the whole conversation from this.
8000"Screen senior backend devs. Ask about Go and Postgres, check notice period and salary expectation, then hand off to a recruiter if they're a strong fit."
The channel the agent will run on. Required, and fixed for the life of the session: it decides which conversation steps are available at all — a transfer to a person exists only on PHONE.
ONLINE, PHONE "PHONE"
Name for the agent. Optional — the composer proposes one from your brief. Anything you send here wins, and keeps winning on later turns.
3 - 100"Senior Developer Phone Screen"
Language of the conversation. When omitted, the composer follows the language of your brief.
EN, SK, CS Target call length in minutes.
1 <= x <= 180Voice for the agent.
Ambient background sound.
OFFICE, OFF The number this agent should call from. PHONE agents only.
Custom metadata carried through to the agent you create from this session.
A webhook to notify when a turn on this session finishes, instead of polling. It must subscribe to composer.completed or composer.failed; only the events it subscribes to are sent, so subscribe to both to hear about every outcome. Pinned once here and used by every later refinement of this session.
Response
The turn was accepted and is running. Poll GET /agents/composer/{sessionId} for the result.
The session to poll, to send follow-up messages to, and to create the agent from.
Always processing here: the turn has been accepted and is running.
processing When this session is deleted if you never create the agent. Sessions live 30 days.