Integrating your system
An online store end to end: confirm an order, post an event, hear the reply.
This page walks one integration end to end - an online store - so you can see how the pieces fit before reading any of them in depth. A POS, an ERP, a booking engine or a helpdesk follows the same loop: identify the customer, message them, tell the platform what happened, and hear back in realtime.
Before you start
An administrator has created an application for your store in Settings → Developer with the scopes below and the store's WhatsApp number on its allow-list, and handed you an API key. Every request in this guide sends it as Authorization: Bearer … and every POST sends an Idempotency-Key (why).
| Step | Scope | Endpoint |
|---|---|---|
| Create the customer | contacts.write |
POST /contacts |
| Confirm the order | templates.send (or messages.send for text) |
POST /messages |
| Post an event | events.send |
POST /events |
| Receive replies and receipts | webhooks.receive |
your endpoint |
| Read history | messages.read, contacts.read |
GET /conversations, GET /contacts |
Step 1 - Identify the customer
You never need the platform's ids to start. A message is addressed by channel and handle - whatsapp and a phone number in E.164, email and an address - and if the platform has never seen that handle, the first message creates the contact and the conversation for you.
Create the contact explicitly when you want a name, tags and your own reference on it before any message goes out - on sign-up, or at checkout:
curl -X POST "https://communication-api.artofluminaire.com/api/v1/contacts" \
-H "Authorization: Bearer omni_live_YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: CRM-CUSTOMER-88213" \
-d '{
"first_name": "Rahul",
"last_name": "Verma",
"phone": "919876543210",
"email": "rahul@example.com",
"company": "Verma Traders",
"lifecycle_stage": "customer",
"tags": [
"wholesale",
"delhi"
]
}'// Node.js 18+ (built-in fetch), saved as request.mjs for top-level await. Replace the key with your own.
const response = await fetch("https://communication-api.artofluminaire.com/api/v1/contacts", {
method: "POST",
headers: {
"Authorization": "Bearer omni_live_YOUR_API_KEY",
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "CRM-CUSTOMER-88213",
},
body: JSON.stringify({
"first_name": "Rahul",
"last_name": "Verma",
"phone": "919876543210",
"email": "rahul@example.com",
"company": "Verma Traders",
"lifecycle_stage": "customer",
"tags": [
"wholesale",
"delhi"
]
}),
});
const payload = await response.json();
if (!response.ok) {
// Every error is { error: { code, message, details, request_id } }.
throw new Error(`${payload.error.code}: ${payload.error.message} (request ${payload.error.request_id})`);
}
console.log(payload.data);# pip install requests
import requests
response = requests.post(
"https://communication-api.artofluminaire.com/api/v1/contacts",
headers={
"Authorization": "Bearer omni_live_YOUR_API_KEY",
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "CRM-CUSTOMER-88213",
},
json={
"first_name": "Rahul",
"last_name": "Verma",
"phone": "919876543210",
"email": "rahul@example.com",
"company": "Verma Traders",
"lifecycle_stage": "customer",
"tags": [
"wholesale",
"delhi",
],
},
timeout=30,
)
payload = response.json()
if not response.ok:
# Every error is {"error": {"code", "message", "details", "request_id"}}.
error = payload["error"]
raise RuntimeError(f"{error['code']}: {error['message']} (request {error['request_id']})")
print(payload["data"])<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client(['http_errors' => false, 'timeout' => 30]);
$response = $client->post('https://communication-api.artofluminaire.com/api/v1/contacts', [
'headers' => [
'Authorization' => 'Bearer omni_live_YOUR_API_KEY',
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Idempotency-Key' => 'CRM-CUSTOMER-88213',
],
'json' => [
'first_name' => 'Rahul',
'last_name' => 'Verma',
'phone' => '919876543210',
'email' => 'rahul@example.com',
'company' => 'Verma Traders',
'lifecycle_stage' => 'customer',
'tags' => [
'wholesale',
'delhi',
],
],
]);
$payload = json_decode((string) $response->getBody(), true);
if ($response->getStatusCode() >= 400) {
// Every error is ['error' => ['code', 'message', 'details', 'request_id']].
throw new RuntimeException(sprintf('%s: %s (request %s)', $payload['error']['code'], $payload['error']['message'], $payload['error']['request_id']));
}
echo json_encode($payload['data'], JSON_PRETTY_PRINT), PHP_EOL;// Java 17+, no dependencies (java.net.http). Parse the JSON with your usual library.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Example {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://communication-api.artofluminaire.com/api/v1/contacts"))
.header("Authorization", "Bearer omni_live_YOUR_API_KEY")
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Idempotency-Key", "CRM-CUSTOMER-88213")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"first_name": "Rahul",
"last_name": "Verma",
"phone": "919876543210",
"email": "rahul@example.com",
"company": "Verma Traders",
"lifecycle_stage": "customer",
"tags": [
"wholesale",
"delhi"
]
}
"""))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
// Every error is {"error": {"code", "message", "details", "request_id"}}.
throw new RuntimeException("Request failed: " + response.body());
}
System.out.println(response.body());
}
}// .NET 7+ (C# 11 raw string literals). Parse the JSON with System.Text.Json.
using System;
using System.Net.Http;
using System.Text;
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "https://communication-api.artofluminaire.com/api/v1/contacts");
request.Headers.TryAddWithoutValidation("Authorization", "Bearer omni_live_YOUR_API_KEY");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Idempotency-Key", "CRM-CUSTOMER-88213");
request.Content = new StringContent("""
{
"first_name": "Rahul",
"last_name": "Verma",
"phone": "919876543210",
"email": "rahul@example.com",
"company": "Verma Traders",
"lifecycle_stage": "customer",
"tags": [
"wholesale",
"delhi"
]
}
""", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var payload = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
// Every error is {"error": {"code", "message", "details", "request_id"}}.
throw new Exception($"Request failed: {payload}");
}
Console.WriteLine(payload);// Go 1.20+, standard library only. Decode the JSON with encoding/json.
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{
"first_name": "Rahul",
"last_name": "Verma",
"phone": "919876543210",
"email": "rahul@example.com",
"company": "Verma Traders",
"lifecycle_stage": "customer",
"tags": [
"wholesale",
"delhi"
]
}`)
req, err := http.NewRequest("POST", "https://communication-api.artofluminaire.com/api/v1/contacts", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer omni_live_YOUR_API_KEY")
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "CRM-CUSTOMER-88213")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
payload, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
// Every error is {"error": {"code", "message", "details", "request_id"}}.
panic(fmt.Sprintf("request failed: %s", payload))
}
fmt.Println(string(payload))
}Try it - send it and see the response
{
"data": {
"id": "01997f2a-6f90-7f41-8e7d-5c3f20b1ad44",
"first_name": "Rahul",
"last_name": "Verma",
"name": "Rahul Verma",
"company": "Verma Traders",
"job_title": null,
"email": "rahul@example.com",
"phone": "919876543210",
"country": null,
"city": null,
"language": null,
"lifecycle_stage": "customer",
"opted_out": false,
"marketing_opted_out": false,
"identities": [],
"tags": [
"wholesale",
"delhi"
],
"last_activity_at": null,
"created_at": "2026-09-15T10:00:00+05:30"
}
}Keep the returned id against your customer record. A contact needs at least one of email, phone or an identity; a handle that already belongs to a contact is refused with 422 validation_failed and the owner's id in details.contact_id - use that id rather than creating a duplicate. custom_fields keys must match fields the business has defined under Settings → CRM; store_customer_id is the kind of thing to put there.
Step 2 - Send the order confirmation
On WhatsApp a business may only start a conversation with an approved template; free text is allowed once the customer has written to you in the last 24 hours. Telegram, email and web chat accept text at any time. So the store confirms an order with a template:
curl -X POST "https://communication-api.artofluminaire.com/api/v1/messages" \
-H "Authorization: Bearer omni_live_YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: INV-10025-WHATSAPP" \
-d '{
"channel": "whatsapp",
"connection_id": "01997f2a-5e8f-7e30-9d6c-4b2e1fa09c33",
"to": "919876543210",
"type": "template",
"external_reference": "INV-10025",
"message": {
"template": "invoice_ready",
"language": "en",
"variables": [
"Rahul",
"INV-10025",
"₹4,850"
]
}
}'// Node.js 18+ (built-in fetch), saved as request.mjs for top-level await. Replace the key with your own.
const response = await fetch("https://communication-api.artofluminaire.com/api/v1/messages", {
method: "POST",
headers: {
"Authorization": "Bearer omni_live_YOUR_API_KEY",
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "INV-10025-WHATSAPP",
},
body: JSON.stringify({
"channel": "whatsapp",
"connection_id": "01997f2a-5e8f-7e30-9d6c-4b2e1fa09c33",
"to": "919876543210",
"type": "template",
"external_reference": "INV-10025",
"message": {
"template": "invoice_ready",
"language": "en",
"variables": [
"Rahul",
"INV-10025",
"₹4,850"
]
}
}),
});
const payload = await response.json();
if (!response.ok) {
// Every error is { error: { code, message, details, request_id } }.
throw new Error(`${payload.error.code}: ${payload.error.message} (request ${payload.error.request_id})`);
}
console.log(payload.data);# pip install requests
import requests
response = requests.post(
"https://communication-api.artofluminaire.com/api/v1/messages",
headers={
"Authorization": "Bearer omni_live_YOUR_API_KEY",
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "INV-10025-WHATSAPP",
},
json={
"channel": "whatsapp",
"connection_id": "01997f2a-5e8f-7e30-9d6c-4b2e1fa09c33",
"to": "919876543210",
"type": "template",
"external_reference": "INV-10025",
"message": {
"template": "invoice_ready",
"language": "en",
"variables": [
"Rahul",
"INV-10025",
"₹4,850",
],
},
},
timeout=30,
)
payload = response.json()
if not response.ok:
# Every error is {"error": {"code", "message", "details", "request_id"}}.
error = payload["error"]
raise RuntimeError(f"{error['code']}: {error['message']} (request {error['request_id']})")
print(payload["data"])<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client(['http_errors' => false, 'timeout' => 30]);
$response = $client->post('https://communication-api.artofluminaire.com/api/v1/messages', [
'headers' => [
'Authorization' => 'Bearer omni_live_YOUR_API_KEY',
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Idempotency-Key' => 'INV-10025-WHATSAPP',
],
'json' => [
'channel' => 'whatsapp',
'connection_id' => '01997f2a-5e8f-7e30-9d6c-4b2e1fa09c33',
'to' => '919876543210',
'type' => 'template',
'external_reference' => 'INV-10025',
'message' => [
'template' => 'invoice_ready',
'language' => 'en',
'variables' => [
'Rahul',
'INV-10025',
'₹4,850',
],
],
],
]);
$payload = json_decode((string) $response->getBody(), true);
if ($response->getStatusCode() >= 400) {
// Every error is ['error' => ['code', 'message', 'details', 'request_id']].
throw new RuntimeException(sprintf('%s: %s (request %s)', $payload['error']['code'], $payload['error']['message'], $payload['error']['request_id']));
}
echo json_encode($payload['data'], JSON_PRETTY_PRINT), PHP_EOL;// Java 17+, no dependencies (java.net.http). Parse the JSON with your usual library.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Example {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://communication-api.artofluminaire.com/api/v1/messages"))
.header("Authorization", "Bearer omni_live_YOUR_API_KEY")
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Idempotency-Key", "INV-10025-WHATSAPP")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"channel": "whatsapp",
"connection_id": "01997f2a-5e8f-7e30-9d6c-4b2e1fa09c33",
"to": "919876543210",
"type": "template",
"external_reference": "INV-10025",
"message": {
"template": "invoice_ready",
"language": "en",
"variables": [
"Rahul",
"INV-10025",
"₹4,850"
]
}
}
"""))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
// Every error is {"error": {"code", "message", "details", "request_id"}}.
throw new RuntimeException("Request failed: " + response.body());
}
System.out.println(response.body());
}
}// .NET 7+ (C# 11 raw string literals). Parse the JSON with System.Text.Json.
using System;
using System.Net.Http;
using System.Text;
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "https://communication-api.artofluminaire.com/api/v1/messages");
request.Headers.TryAddWithoutValidation("Authorization", "Bearer omni_live_YOUR_API_KEY");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Idempotency-Key", "INV-10025-WHATSAPP");
request.Content = new StringContent("""
{
"channel": "whatsapp",
"connection_id": "01997f2a-5e8f-7e30-9d6c-4b2e1fa09c33",
"to": "919876543210",
"type": "template",
"external_reference": "INV-10025",
"message": {
"template": "invoice_ready",
"language": "en",
"variables": [
"Rahul",
"INV-10025",
"₹4,850"
]
}
}
""", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var payload = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
// Every error is {"error": {"code", "message", "details", "request_id"}}.
throw new Exception($"Request failed: {payload}");
}
Console.WriteLine(payload);// Go 1.20+, standard library only. Decode the JSON with encoding/json.
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{
"channel": "whatsapp",
"connection_id": "01997f2a-5e8f-7e30-9d6c-4b2e1fa09c33",
"to": "919876543210",
"type": "template",
"external_reference": "INV-10025",
"message": {
"template": "invoice_ready",
"language": "en",
"variables": [
"Rahul",
"INV-10025",
"₹4,850"
]
}
}`)
req, err := http.NewRequest("POST", "https://communication-api.artofluminaire.com/api/v1/messages", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer omni_live_YOUR_API_KEY")
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "INV-10025-WHATSAPP")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
payload, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
// Every error is {"error": {"code", "message", "details", "request_id"}}.
panic(fmt.Sprintf("request failed: %s", payload))
}
fmt.Println(string(payload))
}Try it - send it and see the response
{
"data": {
"message_id": "01997f2a-4d7e-7d2f-8c5b-3a1d0e9f8b22",
"conversation_id": "01997f2a-3c6d-7c1e-9b4a-2f0c9d8e7a11",
"external_reference": "INV-10025",
"status": "queued"
}
}202 means queued - the customer does not have it yet. Store message_id, and put your order number in external_reference: every webhook about this message carries it back, so you can match a delivery receipt to an order without a lookup. Which templates exist, with their variable slots, comes from GET /templates (Sending messages).
Step 3 - Tell the platform what happened
Post a named event whenever something worth reacting to happens in your store - paid, shipped, delivered, refunded, cart abandoned. Events send nothing by themselves; they are what the business's automations listen for (Automations):
curl -X POST "https://communication-api.artofluminaire.com/api/v1/events" \
-H "Authorization: Bearer omni_live_YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ORDER-10025-PAID" \
-d '{
"event": "order.paid",
"identity": {
"channel": "whatsapp",
"external_id": "919876543210"
},
"data": {
"order_id": "10025",
"amount": 4850,
"currency": "INR"
},
"dedupe_key": "ORDER-10025-PAID"
}'// Node.js 18+ (built-in fetch), saved as request.mjs for top-level await. Replace the key with your own.
const response = await fetch("https://communication-api.artofluminaire.com/api/v1/events", {
method: "POST",
headers: {
"Authorization": "Bearer omni_live_YOUR_API_KEY",
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "ORDER-10025-PAID",
},
body: JSON.stringify({
"event": "order.paid",
"identity": {
"channel": "whatsapp",
"external_id": "919876543210"
},
"data": {
"order_id": "10025",
"amount": 4850,
"currency": "INR"
},
"dedupe_key": "ORDER-10025-PAID"
}),
});
const payload = await response.json();
if (!response.ok) {
// Every error is { error: { code, message, details, request_id } }.
throw new Error(`${payload.error.code}: ${payload.error.message} (request ${payload.error.request_id})`);
}
console.log(payload.data);# pip install requests
import requests
response = requests.post(
"https://communication-api.artofluminaire.com/api/v1/events",
headers={
"Authorization": "Bearer omni_live_YOUR_API_KEY",
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "ORDER-10025-PAID",
},
json={
"event": "order.paid",
"identity": {
"channel": "whatsapp",
"external_id": "919876543210",
},
"data": {
"order_id": "10025",
"amount": 4850,
"currency": "INR",
},
"dedupe_key": "ORDER-10025-PAID",
},
timeout=30,
)
payload = response.json()
if not response.ok:
# Every error is {"error": {"code", "message", "details", "request_id"}}.
error = payload["error"]
raise RuntimeError(f"{error['code']}: {error['message']} (request {error['request_id']})")
print(payload["data"])<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client(['http_errors' => false, 'timeout' => 30]);
$response = $client->post('https://communication-api.artofluminaire.com/api/v1/events', [
'headers' => [
'Authorization' => 'Bearer omni_live_YOUR_API_KEY',
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Idempotency-Key' => 'ORDER-10025-PAID',
],
'json' => [
'event' => 'order.paid',
'identity' => [
'channel' => 'whatsapp',
'external_id' => '919876543210',
],
'data' => [
'order_id' => '10025',
'amount' => 4850,
'currency' => 'INR',
],
'dedupe_key' => 'ORDER-10025-PAID',
],
]);
$payload = json_decode((string) $response->getBody(), true);
if ($response->getStatusCode() >= 400) {
// Every error is ['error' => ['code', 'message', 'details', 'request_id']].
throw new RuntimeException(sprintf('%s: %s (request %s)', $payload['error']['code'], $payload['error']['message'], $payload['error']['request_id']));
}
echo json_encode($payload['data'], JSON_PRETTY_PRINT), PHP_EOL;// Java 17+, no dependencies (java.net.http). Parse the JSON with your usual library.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Example {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://communication-api.artofluminaire.com/api/v1/events"))
.header("Authorization", "Bearer omni_live_YOUR_API_KEY")
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Idempotency-Key", "ORDER-10025-PAID")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"event": "order.paid",
"identity": {
"channel": "whatsapp",
"external_id": "919876543210"
},
"data": {
"order_id": "10025",
"amount": 4850,
"currency": "INR"
},
"dedupe_key": "ORDER-10025-PAID"
}
"""))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
// Every error is {"error": {"code", "message", "details", "request_id"}}.
throw new RuntimeException("Request failed: " + response.body());
}
System.out.println(response.body());
}
}// .NET 7+ (C# 11 raw string literals). Parse the JSON with System.Text.Json.
using System;
using System.Net.Http;
using System.Text;
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "https://communication-api.artofluminaire.com/api/v1/events");
request.Headers.TryAddWithoutValidation("Authorization", "Bearer omni_live_YOUR_API_KEY");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Idempotency-Key", "ORDER-10025-PAID");
request.Content = new StringContent("""
{
"event": "order.paid",
"identity": {
"channel": "whatsapp",
"external_id": "919876543210"
},
"data": {
"order_id": "10025",
"amount": 4850,
"currency": "INR"
},
"dedupe_key": "ORDER-10025-PAID"
}
""", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var payload = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
// Every error is {"error": {"code", "message", "details", "request_id"}}.
throw new Exception($"Request failed: {payload}");
}
Console.WriteLine(payload);// Go 1.20+, standard library only. Decode the JSON with encoding/json.
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{
"event": "order.paid",
"identity": {
"channel": "whatsapp",
"external_id": "919876543210"
},
"data": {
"order_id": "10025",
"amount": 4850,
"currency": "INR"
},
"dedupe_key": "ORDER-10025-PAID"
}`)
req, err := http.NewRequest("POST", "https://communication-api.artofluminaire.com/api/v1/events", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer omni_live_YOUR_API_KEY")
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "ORDER-10025-PAID")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
payload, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
// Every error is {"error": {"code", "message", "details", "request_id"}}.
panic(fmt.Sprintf("request failed: %s", payload))
}
fmt.Println(string(payload))
}Try it - send it and see the response
{
"data": {
"event": "order.paid",
"contact_id": "01997f2a-6f90-7f41-8e7d-5c3f20b1ad44",
"workflows_matched": 1
}
}Name the customer by contact_id or by an identity you know. Everything under data reaches the workflow as {{data.order_id}}-style placeholders, so a "Send message" action can say "Order {{data.order_id}} has shipped with {{data.courier}} - track it at {{data.tracking_url}}" without you sending the message yourself. workflows_matched tells you whether anything is listening.
Step 4 - Hear the customer back
Register a webhook endpoint under your application (Webhooks) subscribed to message.received, message.delivered and message.failed. When the customer replies "Can I change the delivery address?", your endpoint receives within a second or two:
{
"id": "6d2c0a84-1f2e-4b3c-9d4e-5f6a7b8c9d0e",
"type": "message.received",
"created_at": "2026-09-17T09:12:44+00:00",
"data": {
"message": {
"id": "d3a0…", "direction": "inbound", "type": "text", "status": "delivered",
"body": "Can I change the delivery address?",
"conversation_id": "9c0e…", "contact_id": "b7f2…", "connection_id": "e51a…",
"attachments": [], "received_at": "2026-09-17T09:12:43+00:00"
}
}
}
Verify the signature, answer 200, then act: reply from your own system with POST /messages (type: "text" is fine now - the customer just wrote to you), or leave it to the agents in the inbox. Either way every message and every status lands on the same webhook, so your order page can show "Sent · Delivered · Read" from the receipts alone.
Subscribe to conversation.status_changed and conversation.assigned as well if your system tracks whether a customer's question is open or who is handling it, and to lead.stage_changed if the store feeds a sales pipeline.
Step 5 - Keep the CRM in step
Optional, and usually a nightly job:
GET /contacts?created_after=…- contacts the business created since your last sync.GET /conversations?contact_id=…thenGET /conversations/{id}/messages- a customer's full history, for your own support view.POST /leads- open a lead in the business's pipeline when a quote is requested;lead.stage_changedtells you when it moves.
Going live
- The live key (
omni_live_…) is in your secret manager, never in code or a browser. - Every
POSTsends anIdempotency-Key; retries are safe. - The webhook endpoint is
https://, verifies the signature and the timestamp, dedupes onX-Webhook-ID, and answers within 10 seconds before doing the work. - Send test on the endpoint page delivered a
webhook.testyou verified. - The templates you send are
is_sendable: trueinGET /templates. - Your events have published workflows listening (
workflows_matched > 0). Retry-Afteris honoured on a429, andX-Request-IDis in your logs.
https://communication-api.artofluminaire.com
Every response carries X-Request-ID; quote it when you write to support.