Sikio API · v1
API reference
Version 1 · HTTP Basic authentication · workspace-scoped credentials
Production base: https://api.ictlife.com/v1
Staging base: https://stage.api.ictlife.com/v1
Make your first request
Copy a request and replace YOUR_API_KEY and YOUR_API_SECRET. Channel discovery, contact listing and profile-field discovery need no other inputs. Other endpoints show the IDs, message details or file you need to replace.
Examples use staging. For production, change stage.api.ictlife.com to api.ictlife.com and use production credentials. Response values are illustrative; use IDs returned by your own workspace.
Running examples and keeping credentials secure
Paste cURL into your terminal, save JavaScript as request.mjs and run node request.mjs (Node.js 22+), or save Python as request.py and run python3 request.py (Python 3, no extra packages). Choose one language per operation. Before substituting real credentials, read the private-folder, owner-only file permissions, shell-history and temporary-file cleanup guidance. Sends and contact writes change your workspace, so use designated test data.
GET/v1/integrations/whatsapp/channels
List connected WhatsApp channels
Required scope: whatsapp:read
Request
curl --fail-with-body \
--user 'YOUR_API_KEY:YOUR_API_SECRET' \
--write-out '\nHTTP %{http_code}\n' \
'https://stage.api.ictlife.com/v1/integrations/whatsapp/channels'const credentials = Buffer.from('YOUR_API_KEY:YOUR_API_SECRET').toString('base64');
const response = await fetch("https://stage.api.ictlife.com/v1/integrations/whatsapp/channels", {
method: 'GET',
headers: {
Authorization: 'Basic ' + credentials,
},
});
const text = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
console.log(response.status, text ? JSON.parse(text) : null);import base64, json
import urllib.request, urllib.error
credentials = base64.b64encode(b'YOUR_API_KEY:YOUR_API_SECRET').decode()
headers = {'Authorization': 'Basic ' + credentials}
request = urllib.request.Request("https://stage.api.ictlife.com/v1/integrations/whatsapp/channels", method='GET', headers=headers)
try:
with urllib.request.urlopen(request, timeout=30) as response:
text = response.read().decode()
print(response.status, json.loads(text) if text else None)
except urllib.error.HTTPError as error:
raise SystemExit(f'HTTP {error.code}: {error.read().decode()}')Response
{
"data": [
{
"id": 42,
"name": "Customer service",
"phone_number_id": "WHATSAPP_PHONE_NUMBER_ID"
}
]
}Eligible native WhatsApp channels in this workspace.
Parameters, fields and errors
Response fields: WhatsappChannelList.
GET/v1/integrations/whatsapp/channels/{channel_id}/templates
List WhatsApp templates
Inspect status, components and placeholder names. Only an APPROVED template in this channel’s WhatsApp Business Account can be sent.
Required scope: whatsapp:read
Request
Replace CHANNEL_ID with your channel’s id.
curl --fail-with-body \
--user 'YOUR_API_KEY:YOUR_API_SECRET' \
--write-out '\nHTTP %{http_code}\n' \
'https://stage.api.ictlife.com/v1/integrations/whatsapp/channels/CHANNEL_ID/templates'const credentials = Buffer.from('YOUR_API_KEY:YOUR_API_SECRET').toString('base64');
const response = await fetch("https://stage.api.ictlife.com/v1/integrations/whatsapp/channels/CHANNEL_ID/templates", {
method: 'GET',
headers: {
Authorization: 'Basic ' + credentials,
},
});
const text = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
console.log(response.status, text ? JSON.parse(text) : null);import base64, json
import urllib.request, urllib.error
credentials = base64.b64encode(b'YOUR_API_KEY:YOUR_API_SECRET').decode()
headers = {'Authorization': 'Basic ' + credentials}
request = urllib.request.Request("https://stage.api.ictlife.com/v1/integrations/whatsapp/channels/CHANNEL_ID/templates", method='GET', headers=headers)
try:
with urllib.request.urlopen(request, timeout=30) as response:
text = response.read().decode()
print(response.status, json.loads(text) if text else None)
except urllib.error.HTTPError as error:
raise SystemExit(f'HTTP {error.code}: {error.read().decode()}')Response
{
"data": [
{
"id": "YOUR_APPROVED_TEMPLATE_ID",
"name": "order_ready",
"language": "en",
"status": "APPROVED",
"components": [
{
"type": "BODY",
"text": "Your order is ready for collection."
}
]
}
],
"paging": {
"has_more": false
}
}Template definitions and cursor pagination.
Parameters, fields and errors
Parameters
| Name | In | Required | Description |
|---|---|---|---|
channel_id | path | Yes | integer |
after | query | No | Cursor from paging.cursors.after. Continue while paging.has_more is true. string |
Response fields: TemplateList.
POST/v1/integrations/whatsapp/channels/{channel_id}/messages
Send an approved template
Uses the discovered numeric Sikio channel ID. Maximum JSON body is 32 KiB. Unsupported template components, missing or surplus variables fail before provider sending.
Required scope: whatsapp:send
Request
Replace CHANNEL_ID with your channel’s id. Use your recipient and approved template ID. Add variables or media only when that template needs them. Replace UNIQUE_MESSAGE_KEY for each new message; keep it unchanged when retrying that message.
This request uses a fixed-text template with no variables.
curl --fail-with-body \
--user 'YOUR_API_KEY:YOUR_API_SECRET' \
--write-out '\nHTTP %{http_code}\n' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: UNIQUE_MESSAGE_KEY' \
--data-binary @- \
'https://stage.api.ictlife.com/v1/integrations/whatsapp/channels/CHANNEL_ID/messages' <<'JSON'
{
"recipient": "+254700000001",
"template_id": "YOUR_APPROVED_TEMPLATE_ID"
}
JSONconst credentials = Buffer.from('YOUR_API_KEY:YOUR_API_SECRET').toString('base64');
const response = await fetch("https://stage.api.ictlife.com/v1/integrations/whatsapp/channels/CHANNEL_ID/messages", {
method: 'POST',
headers: {
Authorization: 'Basic ' + credentials,
'Content-Type': 'application/json',
'Idempotency-Key': 'UNIQUE_MESSAGE_KEY',
},
body: JSON.stringify({
"recipient": "+254700000001",
"template_id": "YOUR_APPROVED_TEMPLATE_ID"
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
console.log(response.status, text ? JSON.parse(text) : null);import base64, json
import urllib.request, urllib.error
credentials = base64.b64encode(b'YOUR_API_KEY:YOUR_API_SECRET').decode()
headers = {'Authorization': 'Basic ' + credentials}
body = json.dumps({
"recipient": "+254700000001",
"template_id": "YOUR_APPROVED_TEMPLATE_ID"
}).encode()
headers['Content-Type'] = 'application/json'
headers['Idempotency-Key'] = 'UNIQUE_MESSAGE_KEY'
request = urllib.request.Request("https://stage.api.ictlife.com/v1/integrations/whatsapp/channels/CHANNEL_ID/messages", method='POST', headers=headers, data=body)
try:
with urllib.request.urlopen(request, timeout=30) as response:
text = response.read().decode()
print(response.status, json.loads(text) if text else None)
except urllib.error.HTTPError as error:
raise SystemExit(f'HTTP {error.code}: {error.read().decode()}')Response
{
"id": 1042,
"channel_id": 42,
"status": "accepted",
"conversation_id": "CONVERSATION_ID",
"message_id": "MESSAGE_ID",
"provider_message_id": "wamid.EXAMPLE",
"created_at": "2026-10-16T09:00:00Z",
"updated_at": "2026-10-16T09:00:01Z"
}Request is accepted, processing or uncertain. Inspect status; HTTP acceptance does not confirm delivery.
Parameters, fields and errors
Parameters
| Name | In | Required | Description |
|---|---|---|---|
channel_id | path | Yes | integer |
Idempotency-Key | header | Yes | Unique per intended send. Retry with the same credential, channel, key and identical payload. string |
Request fields: TemplateSend.
Response fields: WhatsappMessage.
| HTTP | Meaning | Body |
|---|---|---|
| 409 | This credential already used the idempotency key for a different payload or channel. | — |
| 422 | Preparation failed before a send or WhatsApp explicitly rejected it. The failed request retains its ID and retry key. | WhatsappMessage |
| 429 | New-send limit reached: 60 per minute per credential across replicas. Identical replays do not consume another slot. | — |
POST/v1/integrations/whatsapp/channels/{channel_id}/media
Upload an optional media asset
Only needed when supplying a new asset for a media template. Exactly one multipart file: JPEG/PNG up to 5 MiB, MP4 up to 16 MiB, PDF up to 100 MiB. No URL upload or extra fields. Uploads are not idempotent.
Required scope: whatsapp:send
Request
Replace CHANNEL_ID with your channel’s id. Use a local PNG file at ./header-image.png, or change the file and MIME type together.
curl --fail-with-body \
--user 'YOUR_API_KEY:YOUR_API_SECRET' \
--write-out '\nHTTP %{http_code}\n' \
--request POST \
--form 'file=@./header-image.png;type=image/png' \
'https://stage.api.ictlife.com/v1/integrations/whatsapp/channels/CHANNEL_ID/media'import { readFile } from 'node:fs/promises';
import { basename } from 'node:path';
const filePath = './header-image.png';
const mimeType = 'image/png';
const file = await readFile(filePath);
const form = new FormData();
form.append('file', new Blob([file], { type: mimeType }), basename(filePath));
const credentials = Buffer.from('YOUR_API_KEY:YOUR_API_SECRET').toString('base64');
const response = await fetch("https://stage.api.ictlife.com/v1/integrations/whatsapp/channels/CHANNEL_ID/media", {
method: 'POST',
headers: {
Authorization: 'Basic ' + credentials,
},
body: form,
});
const text = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
console.log(response.status, text ? JSON.parse(text) : null);import base64, json
import urllib.request, urllib.error
from pathlib import Path
import uuid
credentials = base64.b64encode(b'YOUR_API_KEY:YOUR_API_SECRET').decode()
headers = {'Authorization': 'Basic ' + credentials}
file = Path('./header-image.png')
mime_type = 'image/png'
boundary = 'sikio-' + uuid.uuid4().hex
filename = file.name.replace('"', '').replace('\r', '').replace('\n', '')
prefix = (f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="{filename}"\r\n'
f'Content-Type: {mime_type}\r\n\r\n').encode()
body = prefix + file.read_bytes() + f'\r\n--{boundary}--\r\n'.encode()
headers['Content-Type'] = 'multipart/form-data; boundary=' + boundary
request = urllib.request.Request("https://stage.api.ictlife.com/v1/integrations/whatsapp/channels/CHANNEL_ID/media", method='POST', headers=headers, data=body)
try:
with urllib.request.urlopen(request, timeout=30) as response:
text = response.read().decode()
print(response.status, json.loads(text) if text else None)
except urllib.error.HTTPError as error:
raise SystemExit(f'HTTP {error.code}: {error.read().decode()}')Response
{
"media_id": "WHATSAPP_MEDIA_ID",
"channel_id": 42,
"mime_type": "image/png",
"size_bytes": 24576
}Sendable WhatsApp media ID; upload alone sends no message.
Parameters, fields and errors
Parameters
| Name | In | Required | Description |
|---|---|---|---|
channel_id | path | Yes | integer |
Request fields
| Field | Type | Required in object | Description |
|---|---|---|---|
file | string | Yes |
Response fields: UploadedMedia.
| HTTP | Meaning | Body |
|---|---|---|
| 413 | The file or request is too large. | — |
| 429 | Upload concurrency exceeded; retry after the supplied delay. | — |
| 502 | Provider upload failed; no media ID was confirmed. | — |
GET/v1/integrations/whatsapp/messages/{request_id}
Get a message request and receipts
Use the original sending credential with whatsapp:read. A rotated key cannot retrieve the previous credential’s requests. Polling is supported; outbound webhook subscriptions are not included.
Required scope: whatsapp:read
Request
Replace REQUEST_ID with the send response’s id.
curl --fail-with-body \
--user 'YOUR_API_KEY:YOUR_API_SECRET' \
--write-out '\nHTTP %{http_code}\n' \
'https://stage.api.ictlife.com/v1/integrations/whatsapp/messages/REQUEST_ID'const credentials = Buffer.from('YOUR_API_KEY:YOUR_API_SECRET').toString('base64');
const response = await fetch("https://stage.api.ictlife.com/v1/integrations/whatsapp/messages/REQUEST_ID", {
method: 'GET',
headers: {
Authorization: 'Basic ' + credentials,
},
});
const text = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
console.log(response.status, text ? JSON.parse(text) : null);import base64, json
import urllib.request, urllib.error
credentials = base64.b64encode(b'YOUR_API_KEY:YOUR_API_SECRET').decode()
headers = {'Authorization': 'Basic ' + credentials}
request = urllib.request.Request("https://stage.api.ictlife.com/v1/integrations/whatsapp/messages/REQUEST_ID", method='GET', headers=headers)
try:
with urllib.request.urlopen(request, timeout=30) as response:
text = response.read().decode()
print(response.status, json.loads(text) if text else None)
except urllib.error.HTTPError as error:
raise SystemExit(f'HTTP {error.code}: {error.read().decode()}')Response
{
"id": 1042,
"channel_id": 42,
"status": "accepted",
"conversation_id": "CONVERSATION_ID",
"message_id": "MESSAGE_ID",
"provider_message_id": "wamid.EXAMPLE",
"external_reference": "ORDER-1042",
"created_at": "2026-10-16T09:00:00Z",
"updated_at": "2026-10-16T09:00:01Z"
}Request belonging to this workspace and the original API credential.
Parameters, fields and errors
Parameters
| Name | In | Required | Description |
|---|---|---|---|
request_id | path | Yes | integer |
Response fields: WhatsappMessage.
GET/v1/integrations/contacts
List and search contacts
Required scope: contacts:read
Request
curl --fail-with-body \
--user 'YOUR_API_KEY:YOUR_API_SECRET' \
--write-out '\nHTTP %{http_code}\n' \
'https://stage.api.ictlife.com/v1/integrations/contacts'const credentials = Buffer.from('YOUR_API_KEY:YOUR_API_SECRET').toString('base64');
const response = await fetch("https://stage.api.ictlife.com/v1/integrations/contacts", {
method: 'GET',
headers: {
Authorization: 'Basic ' + credentials,
},
});
const text = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
console.log(response.status, text ? JSON.parse(text) : null);import base64, json
import urllib.request, urllib.error
credentials = base64.b64encode(b'YOUR_API_KEY:YOUR_API_SECRET').decode()
headers = {'Authorization': 'Basic ' + credentials}
request = urllib.request.Request("https://stage.api.ictlife.com/v1/integrations/contacts", method='GET', headers=headers)
try:
with urllib.request.urlopen(request, timeout=30) as response:
text = response.read().decode()
print(response.status, json.loads(text) if text else None)
except urllib.error.HTTPError as error:
raise SystemExit(f'HTTP {error.code}: {error.read().decode()}')Response
{
"contacts": [
{
"id": 91,
"display_name": "Amina Customer",
"whatsapp_number": "+254700000001",
"email": "amina@example.com",
"consent_status": "unknown"
}
],
"pagination": {
"count": 1,
"page": 1,
"per": 20
}
}Contacts and page-based pagination.
Parameters, fields and errors
Parameters
| Name | In | Required | Description |
|---|---|---|---|
page | query | No | Page number. integer |
per | query | No | Page size; capped at 100. integer; maximum 100 |
search | query | No | Contact search text. string |
Response fields: ContactList.
POST/v1/integrations/contacts
Create a contact
Supply a name or usable identity from which a display name can be derived. Creating a contact is not consent to send a message.
Required scope: contacts:write
Request
Replace the sample name, WhatsApp number and email with your contact’s details. Set consent_status from recorded consent; creating a contact does not grant permission to message them.
curl --fail-with-body \
--user 'YOUR_API_KEY:YOUR_API_SECRET' \
--write-out '\nHTTP %{http_code}\n' \
--request POST \
--header 'Content-Type: application/json' \
--data-binary @- \
'https://stage.api.ictlife.com/v1/integrations/contacts' <<'JSON'
{
"display_name": "Amina Customer",
"whatsapp_number": "+254700000001",
"email": "amina@example.com",
"consent_status": "unknown"
}
JSONconst credentials = Buffer.from('YOUR_API_KEY:YOUR_API_SECRET').toString('base64');
const response = await fetch("https://stage.api.ictlife.com/v1/integrations/contacts", {
method: 'POST',
headers: {
Authorization: 'Basic ' + credentials,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"display_name": "Amina Customer",
"whatsapp_number": "+254700000001",
"email": "amina@example.com",
"consent_status": "unknown"
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
console.log(response.status, text ? JSON.parse(text) : null);import base64, json
import urllib.request, urllib.error
credentials = base64.b64encode(b'YOUR_API_KEY:YOUR_API_SECRET').decode()
headers = {'Authorization': 'Basic ' + credentials}
body = json.dumps({
"display_name": "Amina Customer",
"whatsapp_number": "+254700000001",
"email": "amina@example.com",
"consent_status": "unknown"
}).encode()
headers['Content-Type'] = 'application/json'
request = urllib.request.Request("https://stage.api.ictlife.com/v1/integrations/contacts", method='POST', headers=headers, data=body)
try:
with urllib.request.urlopen(request, timeout=30) as response:
text = response.read().decode()
print(response.status, json.loads(text) if text else None)
except urllib.error.HTTPError as error:
raise SystemExit(f'HTTP {error.code}: {error.read().decode()}')Response
{
"id": 91,
"display_name": "Amina Customer",
"whatsapp_number": "+254700000001",
"email": "amina@example.com",
"consent_status": "unknown"
}Created contact.
POST/v1/integrations/contacts/upsert
Create or update by contact identity
Match by normalized phone, WhatsApp or email identity. Supply at least one matching identity. Conflicting identities are rejected instead of merging contacts automatically.
Required scope: contacts:write
Request
Replace the sample name, WhatsApp number and email with your contact’s details. Set consent_status from recorded consent; creating a contact does not grant permission to message them.
curl --fail-with-body \
--user 'YOUR_API_KEY:YOUR_API_SECRET' \
--write-out '\nHTTP %{http_code}\n' \
--request POST \
--header 'Content-Type: application/json' \
--data-binary @- \
'https://stage.api.ictlife.com/v1/integrations/contacts/upsert' <<'JSON'
{
"display_name": "Amina Customer",
"whatsapp_number": "+254700000001",
"email": "amina@example.com",
"consent_status": "unknown"
}
JSONconst credentials = Buffer.from('YOUR_API_KEY:YOUR_API_SECRET').toString('base64');
const response = await fetch("https://stage.api.ictlife.com/v1/integrations/contacts/upsert", {
method: 'POST',
headers: {
Authorization: 'Basic ' + credentials,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"display_name": "Amina Customer",
"whatsapp_number": "+254700000001",
"email": "amina@example.com",
"consent_status": "unknown"
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
console.log(response.status, text ? JSON.parse(text) : null);import base64, json
import urllib.request, urllib.error
credentials = base64.b64encode(b'YOUR_API_KEY:YOUR_API_SECRET').decode()
headers = {'Authorization': 'Basic ' + credentials}
body = json.dumps({
"display_name": "Amina Customer",
"whatsapp_number": "+254700000001",
"email": "amina@example.com",
"consent_status": "unknown"
}).encode()
headers['Content-Type'] = 'application/json'
request = urllib.request.Request("https://stage.api.ictlife.com/v1/integrations/contacts/upsert", method='POST', headers=headers, data=body)
try:
with urllib.request.urlopen(request, timeout=30) as response:
text = response.read().decode()
print(response.status, json.loads(text) if text else None)
except urllib.error.HTTPError as error:
raise SystemExit(f'HTTP {error.code}: {error.read().decode()}')Response
{
"action": "create",
"contact": {
"id": 91,
"display_name": "Amina Customer",
"whatsapp_number": "+254700000001",
"email": "amina@example.com",
"consent_status": "unknown"
}
}Created or updated contact.
GET/v1/integrations/contacts/{contact_id}
Get a contact
Required scope: contacts:read
Request
Replace CONTACT_ID with your contact’s id.
curl --fail-with-body \
--user 'YOUR_API_KEY:YOUR_API_SECRET' \
--write-out '\nHTTP %{http_code}\n' \
'https://stage.api.ictlife.com/v1/integrations/contacts/CONTACT_ID'const credentials = Buffer.from('YOUR_API_KEY:YOUR_API_SECRET').toString('base64');
const response = await fetch("https://stage.api.ictlife.com/v1/integrations/contacts/CONTACT_ID", {
method: 'GET',
headers: {
Authorization: 'Basic ' + credentials,
},
});
const text = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
console.log(response.status, text ? JSON.parse(text) : null);import base64, json
import urllib.request, urllib.error
credentials = base64.b64encode(b'YOUR_API_KEY:YOUR_API_SECRET').decode()
headers = {'Authorization': 'Basic ' + credentials}
request = urllib.request.Request("https://stage.api.ictlife.com/v1/integrations/contacts/CONTACT_ID", method='GET', headers=headers)
try:
with urllib.request.urlopen(request, timeout=30) as response:
text = response.read().decode()
print(response.status, json.loads(text) if text else None)
except urllib.error.HTTPError as error:
raise SystemExit(f'HTTP {error.code}: {error.read().decode()}')Response
{
"id": 91,
"display_name": "Amina Customer",
"whatsapp_number": "+254700000001",
"email": "amina@example.com",
"consent_status": "unknown"
}Contact including available channel identities.
Parameters, fields and errors
Parameters
| Name | In | Required | Description |
|---|---|---|---|
contact_id | path | Yes | integer |
Response fields: Contact.
PATCH/v1/integrations/contacts/{contact_id}
Partially update a contact
Omitted fields remain unchanged. Use the profile-field schema when updating profile_data.
Required scope: contacts:write
Request
Replace CONTACT_ID with your contact’s id. Replace the sample name with the value you want to save. Include only fields you intend to change.
curl --fail-with-body \
--user 'YOUR_API_KEY:YOUR_API_SECRET' \
--write-out '\nHTTP %{http_code}\n' \
--request PATCH \
--header 'Content-Type: application/json' \
--data-binary @- \
'https://stage.api.ictlife.com/v1/integrations/contacts/CONTACT_ID' <<'JSON'
{
"display_name": "Amina Updated"
}
JSONconst credentials = Buffer.from('YOUR_API_KEY:YOUR_API_SECRET').toString('base64');
const response = await fetch("https://stage.api.ictlife.com/v1/integrations/contacts/CONTACT_ID", {
method: 'PATCH',
headers: {
Authorization: 'Basic ' + credentials,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"display_name": "Amina Updated"
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
console.log(response.status, text ? JSON.parse(text) : null);import base64, json
import urllib.request, urllib.error
credentials = base64.b64encode(b'YOUR_API_KEY:YOUR_API_SECRET').decode()
headers = {'Authorization': 'Basic ' + credentials}
body = json.dumps({
"display_name": "Amina Updated"
}).encode()
headers['Content-Type'] = 'application/json'
request = urllib.request.Request("https://stage.api.ictlife.com/v1/integrations/contacts/CONTACT_ID", method='PATCH', headers=headers, data=body)
try:
with urllib.request.urlopen(request, timeout=30) as response:
text = response.read().decode()
print(response.status, json.loads(text) if text else None)
except urllib.error.HTTPError as error:
raise SystemExit(f'HTTP {error.code}: {error.read().decode()}')Response
{
"id": 91,
"display_name": "Amina Updated",
"whatsapp_number": "+254700000001",
"email": "amina@example.com",
"consent_status": "unknown"
}Updated contact.
Parameters, fields and errors
Parameters
| Name | In | Required | Description |
|---|---|---|---|
contact_id | path | Yes | integer |
Request fields: ContactInput.
Response fields: Contact.
DELETE/v1/integrations/contacts/{contact_id}
Soft-delete a contact
Required scope: contacts:delete
Request
Replace CONTACT_ID with your contact’s id.
curl --fail-with-body --include \
--user 'YOUR_API_KEY:YOUR_API_SECRET' \
--request DELETE \
'https://stage.api.ictlife.com/v1/integrations/contacts/CONTACT_ID'const credentials = Buffer.from('YOUR_API_KEY:YOUR_API_SECRET').toString('base64');
const response = await fetch("https://stage.api.ictlife.com/v1/integrations/contacts/CONTACT_ID", {
method: 'DELETE',
headers: {
Authorization: 'Basic ' + credentials,
},
});
const text = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
console.log(response.status, text ? JSON.parse(text) : null);import base64, json
import urllib.request, urllib.error
credentials = base64.b64encode(b'YOUR_API_KEY:YOUR_API_SECRET').decode()
headers = {'Authorization': 'Basic ' + credentials}
request = urllib.request.Request("https://stage.api.ictlife.com/v1/integrations/contacts/CONTACT_ID", method='DELETE', headers=headers)
try:
with urllib.request.urlopen(request, timeout=30) as response:
text = response.read().decode()
print(response.status, json.loads(text) if text else None)
except urllib.error.HTTPError as error:
raise SystemExit(f'HTTP {error.code}: {error.read().decode()}')Response
HTTP/1.1 204 No ContentContact soft-deleted. No response body.
Parameters, fields and errors
Parameters
| Name | In | Required | Description |
|---|---|---|---|
contact_id | path | Yes | integer |
GET/v1/integrations/contact_profile_fields
List contact profile-field definitions
Required scope: contacts:read
Request
curl --fail-with-body \
--user 'YOUR_API_KEY:YOUR_API_SECRET' \
--write-out '\nHTTP %{http_code}\n' \
'https://stage.api.ictlife.com/v1/integrations/contact_profile_fields'const credentials = Buffer.from('YOUR_API_KEY:YOUR_API_SECRET').toString('base64');
const response = await fetch("https://stage.api.ictlife.com/v1/integrations/contact_profile_fields", {
method: 'GET',
headers: {
Authorization: 'Basic ' + credentials,
},
});
const text = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
console.log(response.status, text ? JSON.parse(text) : null);import base64, json
import urllib.request, urllib.error
credentials = base64.b64encode(b'YOUR_API_KEY:YOUR_API_SECRET').decode()
headers = {'Authorization': 'Basic ' + credentials}
request = urllib.request.Request("https://stage.api.ictlife.com/v1/integrations/contact_profile_fields", method='GET', headers=headers)
try:
with urllib.request.urlopen(request, timeout=30) as response:
text = response.read().decode()
print(response.status, json.loads(text) if text else None)
except urllib.error.HTTPError as error:
raise SystemExit(f'HTTP {error.code}: {error.read().decode()}')Response
{
"profile_fields": [
{
"id": 7,
"field_key": "customer_tier",
"label": "Customer tier",
"field_type": "text",
"is_required": false,
"is_default": false,
"display_order": 1
}
]
}The workspace’s custom contact schema.
Parameters, fields and errors
Response fields: ProfileFieldList.
Common errors
| HTTP | Meaning |
|---|---|
| 400 | Invalid request or unavailable channel; inspect the error response. |
| 401 | Missing, invalid or revoked integration credentials. |
| 403 | The credential does not have the required permission. |
| 404 | Resource unavailable to this credential. The platform can also mask unauthenticated requests with 404; inspect error_code and error_message. |
| 500 | A server error occurred. Reconcile message sends before retrying. |
Object schemas
WhatsappChannelList
| Field | Type | Required in object | Description |
|---|---|---|---|
data | array of WhatsappChannel | Yes |
ContactUpsertResult
| Field | Type | Required in object | Description |
|---|---|---|---|
action | string | Yes | Allowed: create, update. |
contact | Contact | Yes |
ProfileFieldList
| Field | Type | Required in object | Description |
|---|---|---|---|
profile_fields | array of ProfileField | No |
TemplateSend
| Field | Type | Required in object | Description |
|---|---|---|---|
recipient | string | Yes | Consenting recipient in E.164 format, including + and country code. |
template_id | string | Yes | Discovered approved template ID; language and components come from the template definition. Maximum 128 characters; server byte limits also apply. |
template_variables | object | No | Template-defined string values. Keys: body:1, body:name, header:1, button_1:1, button_1:payload, button_1:flow_token, header:media_id; carousel keys use card_1: prefix. Keys 1–128 characters. Omit for fixed text; use only keys required or supported by the chosen template. Up to 100 fields. |
external_reference | string | No | Optional private business reference, shown in Sikio; not automatically sent to WhatsApp. Maximum 256 characters; server byte limits also apply. |
context | object | No | Optional private message-level business context; keys 1–64 characters. Not a contact profile update. Up to 25 fields. |
WhatsappChannel
| Field | Type | Required in object | Description |
|---|---|---|---|
id | integer | No | |
name | string | No | |
phone_number_id | string | No | Provider ID for information; use id in Sikio request paths. |
TemplateList
| Field | Type | Required in object | Description |
|---|---|---|---|
data | array of object | No | |
data[].id | string | No | |
data[].name | string | No | |
data[].language | string | No | |
data[].status | string | No | |
data[].components | array of object | No | Owned template definitions including headers, body, footer, buttons and supported carousel cards. |
paging | object | No | |
paging.has_more | boolean | No | |
paging.cursors | object | No | |
paging.cursors.after | string | No |
WhatsappMessage
| Field | Type | Required in object | Description |
|---|---|---|---|
id | integer | No | Sikio integration request ID. Use for status lookup. |
channel_id | integer | No | |
status | string | No | Allowed: processing, accepted, failed, uncertain. |
conversation_id | string | No | |
message_id | string | No | |
provider_message_id | string | No | |
external_reference | string | No | |
error | string | No | Failure detail or bookkeeping reconciliation warning; do not automatically resend an accepted request. |
delivery_status | string | No | May be absent before a persisted message is available. Acceptance is not delivery; a missing receipt is not proof of failure. Allowed: unknown, delivered, read. |
created_at | string | No | |
updated_at | string | No | |
delivered_at | string | No | |
read_at | string | No |
UploadedMedia
| Field | Type | Required in object | Description |
|---|---|---|---|
media_id | string | No | |
channel_id | integer | No | |
mime_type | string | No | |
size_bytes | integer | No |
ContactInput
| Field | Type | Required in object | Description |
|---|---|---|---|
display_name | string | No | Display name; otherwise derived from a supplied name or identity. |
first_name | string | null | No | |
last_name | string | null | No | |
phone_number | string | null | No | |
whatsapp_number | string | null | No | |
email | string | null | No | |
language | string | null | No | |
acquisition_source | string | null | No | |
preferred_channel | string | null | No | Allowed: whatsapp, sms, email. |
consent_status | string | No | Allowed: unknown, opted_in, opted_out. |
consented_at | string | null | No | |
last_seen_at | string | null | No | |
profile_data | object | No | Values matching the workspace’s profile-field definitions. |
Contact
| Field | Type | Required in object | Description |
|---|---|---|---|
display_name | string | No | Display name; otherwise derived from a supplied name or identity. |
first_name | string | null | No | |
last_name | string | null | No | |
phone_number | string | null | No | |
whatsapp_number | string | null | No | |
email | string | null | No | |
language | string | null | No | |
acquisition_source | string | null | No | |
preferred_channel | string | null | No | Allowed: whatsapp, sms, email. |
consent_status | string | No | Allowed: unknown, opted_in, opted_out. |
consented_at | string | null | No | |
last_seen_at | string | null | No | |
profile_data | object | No | Values matching the workspace’s profile-field definitions. |
id | integer | No | |
group_id | integer | No | Read-only owning workspace; never supplied to integration endpoints. |
identities | array of object | No | |
created_at | string | No | |
updated_at | string | No |
ContactList
| Field | Type | Required in object | Description |
|---|---|---|---|
contacts | array of Contact | No | |
pagination | object | No | |
pagination.count | integer | No | |
pagination.page | integer | No | |
pagination.per | integer | No |
ProfileField
| Field | Type | Required in object | Description |
|---|---|---|---|
id | integer | No | |
field_key | string | No | |
label | string | No | |
field_type | string | No | Allowed: text, number, date, select, boolean. |
options | JSON value | No | |
is_required | boolean | No | |
is_default | boolean | No | |
display_order | integer | No |