curl --request POST \
--url https://api.instaview.sk/runs/{id}/contacts \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"contactIds": [
"123e4567-e89b-12d3-a456-426614174000"
]
}
'import requests
url = "https://api.instaview.sk/runs/{id}/contacts"
payload = { "contactIds": ["123e4567-e89b-12d3-a456-426614174000"] }
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({contactIds: ['123e4567-e89b-12d3-a456-426614174000']})
};
fetch('https://api.instaview.sk/runs/{id}/contacts', 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/runs/{id}/contacts",
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([
'contactIds' => [
'123e4567-e89b-12d3-a456-426614174000'
]
]),
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/runs/{id}/contacts"
payload := strings.NewReader("{\n \"contactIds\": [\n \"123e4567-e89b-12d3-a456-426614174000\"\n ]\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/runs/{id}/contacts")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"contactIds\": [\n \"123e4567-e89b-12d3-a456-426614174000\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.instaview.sk/runs/{id}/contacts")
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 \"contactIds\": [\n \"123e4567-e89b-12d3-a456-426614174000\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"attached": 8,
"skipped": 2,
"totalContacts": 10,
"queued": 8
}{
"statusCode": 402,
"message": "Insufficient minutes available to schedule 10 interviews (50 minutes total). Please purchase additional minutes.",
"error": "PAYMENT_REQUIRED",
"billing": {
"billingSystem": "minutes",
"requiredMinutes": 50,
"availableMinutes": 12,
"requiredCredits": 50,
"availableCredits": 12,
"suggestedAction": "purchase_minutes"
},
"traceId": "4b1f0c9d2e6a47f8b3c5d7e9a1b2c3d4"
}{
"statusCode": 400,
"message": "Validation failed",
"error": "Bad Request"
}Attach Contacts
Attaches existing contacts to a run. Idempotent - an id already on the run is skipped, so a repeated or overlapping batch attaches nothing twice. Allowed in every status except CANCELLED. On an already-launched run the new contacts are dispatched immediately and a COMPLETED run reopens to RUNNING; on a paused run nothing is dialled until you resume, and on a scheduled run they wait and go out with the rest at scheduledAt. More than the per-request cap is a 400 rather than a silent truncation - chunk it, which is safe.
curl --request POST \
--url https://api.instaview.sk/runs/{id}/contacts \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"contactIds": [
"123e4567-e89b-12d3-a456-426614174000"
]
}
'import requests
url = "https://api.instaview.sk/runs/{id}/contacts"
payload = { "contactIds": ["123e4567-e89b-12d3-a456-426614174000"] }
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({contactIds: ['123e4567-e89b-12d3-a456-426614174000']})
};
fetch('https://api.instaview.sk/runs/{id}/contacts', 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/runs/{id}/contacts",
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([
'contactIds' => [
'123e4567-e89b-12d3-a456-426614174000'
]
]),
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/runs/{id}/contacts"
payload := strings.NewReader("{\n \"contactIds\": [\n \"123e4567-e89b-12d3-a456-426614174000\"\n ]\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/runs/{id}/contacts")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"contactIds\": [\n \"123e4567-e89b-12d3-a456-426614174000\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.instaview.sk/runs/{id}/contacts")
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 \"contactIds\": [\n \"123e4567-e89b-12d3-a456-426614174000\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"attached": 8,
"skipped": 2,
"totalContacts": 10,
"queued": 8
}{
"statusCode": 402,
"message": "Insufficient minutes available to schedule 10 interviews (50 minutes total). Please purchase additional minutes.",
"error": "PAYMENT_REQUIRED",
"billing": {
"billingSystem": "minutes",
"requiredMinutes": 50,
"availableMinutes": 12,
"requiredCredits": 50,
"availableCredits": 12,
"suggestedAction": "purchase_minutes"
},
"traceId": "4b1f0c9d2e6a47f8b3c5d7e9a1b2c3d4"
}{
"statusCode": 400,
"message": "Validation failed",
"error": "Bad Request"
}Overview
Contacts arrive here rather than at create, and this endpoint is idempotent: an id already on the run is skipped, not attached twice. That is what makes retries and overlapping chunks safe.POST /runs/789e0123-.../contacts
{ "contactIds": ["contact-a", "contact-b"] }
→ { "attached": 2, "skipped": 0, "totalContacts": 2, "queued": 0 }
{ "attached": 0, "skipped": 2, "totalContacts": 2 }. The counts report rows actually changed, so they tell you what your request did rather than what it asked for.
Attaching to a run that is already going
This is the point of the endpoint, not an edge case. A run is a rolling sequence, not a frozen batch:| Run status | What happens |
|---|---|
DRAFT | Contacts are added. Nothing is dialled until you launch. |
SCHEDULED | Contacts are added. Nothing is dialled; they go out with the rest at scheduledAt. |
RUNNING | Contacts are added and dispatched immediately — queued says how many. |
PAUSED | Contacts are added. Nothing is dialled; they go out when you resume. |
COMPLETED | Contacts are added, and the run reopens to RUNNING. |
CANCELLED | 422. A cancelled run is deliberately dead. |
COMPLETED run reopens, do not treat COMPLETED as “this run is finished forever”
if anything in your system still attaches to it.RUNNING run spends money, so it runs the same all-or-nothing billing admission a launch does. Without that, attach would simply be the way around the gate: launch one contact, attach 999.
Batch bounds
Up to 500 contact ids per request. More is a400, not a silent truncation — a caller told “attached 500” out of 900 has no way to know which 400 were dropped.
Chunking is yours to do and safe to do, precisely because attaching an id twice is a no-op:
for (const chunk of chunks(contactIds, 500)) {
await attach(runId, chunk); // overlapping or repeated chunks cost nothing
}
Contacts must already exist
This endpoint attaches existing contacts; it does not create them. Create them first with Create Contact. An id that is not an active contact in your company is a422 naming the ids it could not find, and nothing is attached.
Error Scenarios
- 400 Bad Request: more than 500 ids, an empty list, or a malformed id
- 402 Payment Required: attaching into a live run the company cannot afford. Nothing is attached and nothing is dispatched
- 404 Not Found: the run does not exist, or belongs to another company
- 422 Unprocessable Entity: the run is cancelled, or an id is not a contact in this company
- 403 Forbidden: the API key does not hold
write:runsandread:contacts
Related Resources
Detach Contact
Launch Run
Create Contact
Runs Resource Guide
Authorizations
API key for authentication using Bearer scheme
Path Parameters
Query Parameters
Required for ATS API keys to specify which company to access. Ignored for standard company API keys.
Body
Contact ids to attach. Max 500 per request; send more in separate requests, which is safe because attaching an id twice is a no-op.
1 - 500 elements["123e4567-e89b-12d3-a456-426614174000"]
Response
Contacts attached
Contacts newly attached by this request
8
Ids already on the run, so this request did nothing for them. Repeating a batch is safe.
2
Contacts on the run after this request
10
Conversations queued by this attach. Non-zero only on an already-launched run; zero on a draft, which dials nothing until launched.
8