Webhooks overview
Copy page
Webhooks push events to your server the moment they happen — no polling. Each event type goes to its own URL, configured per workspace.
Event types
Section titled “Event types”| Type | Fires when |
|---|---|
impression | SMS accepted by the gateway (API single-send path) |
click | Recipient clicks a short URL (optional per-recipient dedup) |
conversion | Postback received via /track or createAction |
delivery | Carrier returns a delivery report (DLR) |
Configure
Section titled “Configure”In the portal under Webhooks, or via API:
PUT
/api/v1/user/webhooks
· Permission: webhooks.write
curl -X PUT https://restlink23telecom.com/api/v1/user/webhooks \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "delivery_url": "https://your-server.com/webhook/delivery", "click_url": "https://your-server.com/webhook/click", "unique_clicks_only": true, "delivery_fields": [ "message_id", "recipient", "sender_id", "status", "status_code", "num_parts", "cost", "timestamp" ]}'const res = await fetch('https://restlink23telecom.com/api/v1/user/webhooks', { method: 'PUT', headers: { 'X-API-Key': process.env.API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "delivery_url": "https://your-server.com/webhook/delivery", "click_url": "https://your-server.com/webhook/click", "unique_clicks_only": true, "delivery_fields": [ "message_id", "recipient", "sender_id", "status", "status_code", "num_parts", "cost", "timestamp" ] }),});
if (!res.ok) { const err = await res.json(); throw new Error(`${err.error_code}: ${err.description}`);}const data = await res.json();console.log(data);import osimport requests
res = requests.put( "https://restlink23telecom.com/api/v1/user/webhooks", headers={"X-API-Key": os.environ["API_KEY"]}, json={ "delivery_url": "https://your-server.com/webhook/delivery", "click_url": "https://your-server.com/webhook/click", "unique_clicks_only": True, "delivery_fields": ["message_id", "recipient", "sender_id", "status", "status_code", "num_parts", "cost", "timestamp"] },)res.raise_for_status()print(res.json())<?php$ch = curl_init('https://restlink23telecom.com/api/v1/user/webhooks');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'PUT', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['X-API-Key: ' . getenv('API_KEY'), 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'delivery_url' => 'https://your-server.com/webhook/delivery', 'click_url' => 'https://your-server.com/webhook/click', 'unique_clicks_only' => true, 'delivery_fields' => ['message_id', 'recipient', 'sender_id', 'status', 'status_code', 'num_parts', 'cost', 'timestamp'] ]),]);
$response = curl_exec($ch);$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);curl_close($ch);
if ($status !== 200) { throw new Exception("HTTP $status: $response");}$data = json_decode($response, true);print_r($data);require "net/http"require "json"
uri = URI("https://restlink23telecom.com/api/v1/user/webhooks")req = Net::HTTP::Put.new(uri)req["X-API-Key"] = ENV.fetch("API_KEY")req["Content-Type"] = "application/json"req.body = { delivery_url: "https://your-server.com/webhook/delivery", click_url: "https://your-server.com/webhook/click", unique_clicks_only: true, delivery_fields: ["message_id", "recipient", "sender_id", "status", "status_code", "num_parts", "cost", "timestamp"]}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }raise "HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
puts JSON.parse(res.body)import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://restlink23telecom.com/api/v1/user/webhooks")) .header("X-API-Key", System.getenv("API_KEY")) .header("Content-Type", "application/json") .PUT(HttpRequest.BodyPublishers.ofString(""" { "delivery_url": "https://your-server.com/webhook/delivery", "click_url": "https://your-server.com/webhook/click", "unique_clicks_only": true, "delivery_fields": [ "message_id", "recipient", "sender_id", "status", "status_code", "num_parts", "cost", "timestamp" ] }""")) .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());System.out.println(response.body());package main
import ( "fmt" "io" "net/http" "os" "strings")
func main() { payload := strings.NewReader(`{ "delivery_url": "https://your-server.com/webhook/delivery", "click_url": "https://your-server.com/webhook/click", "unique_clicks_only": true, "delivery_fields": [ "message_id", "recipient", "sender_id", "status", "status_code", "num_parts", "cost", "timestamp" ]}`)
req, err := http.NewRequest("PUT", "https://restlink23telecom.com/api/v1/user/webhooks", payload) if err != nil { panic(err) } req.Header.Set("X-API-Key", os.Getenv("API_KEY")) req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, _ := io.ReadAll(res.Body) fmt.Println(string(data))}using System.Text;
using var client = new HttpClient();client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("API_KEY"));
var json = """{ "delivery_url": "https://your-server.com/webhook/delivery", "click_url": "https://your-server.com/webhook/click", "unique_clicks_only": true, "delivery_fields": [ "message_id", "recipient", "sender_id", "status", "status_code", "num_parts", "cost", "timestamp" ]}""";var content = new StringContent(json, Encoding.UTF8, "application/json");var response = await client.PutAsync("https://restlink23telecom.com/api/v1/user/webhooks", content);
response.EnsureSuccessStatusCode();Console.WriteLine(await response.Content.ReadAsStringAsync());Validation errors
Section titled “Validation errors”PUT /user/webhooks returns 400 with stable typed codes:
| Code | When |
|---|---|
INVALID_URL | URL is not a string or fails SSRF validation (""/null are valid — they disable the channel) |
INVALID_SECRET | signing_secret empty, longer than 64 chars, or wrong type |
INVALID_FIELDS | *_fields is null, empty, or contains an unknown field name |
INVALID_TOGGLE | unique_clicks_only is not a boolean |
CONFLICTING_FIELDS | signing_secret and clear_signing_secret: true sent together |
INVALID_BODY | JSON parse error |
Choose your payload fields
Section titled “Choose your payload fields”You pick which fields each webhook carries (defaults shown in the example
above). The full field list per event is in
delivery webhook. workspace_id is
opt-in for every event type — add it to the relevant *_fields list if
you route multiple workspaces to one receiver.
Click dedup (unique_clicks_only)
Section titled “Click dedup (unique_clicks_only)”When true, the click webhook fires only on the first human click per
(broadcast, recipient) pair; repeat clicks are skipped. Bot clicks never fire
webhooks. Applies to broadcast links only — API auto-shortened links always
deliver every human click.
Test & inspect
Section titled “Test & inspect” POST
/api/v1/user/webhooks/test
· Permission: webhooks.write
curl -X POST https://restlink23telecom.com/api/v1/user/webhooks/test \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "delivery"}'const res = await fetch('https://restlink23telecom.com/api/v1/user/webhooks/test', { method: 'POST', headers: { 'X-API-Key': process.env.API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "type": "delivery" }),});
if (!res.ok) { const err = await res.json(); throw new Error(`${err.error_code}: ${err.description}`);}const data = await res.json();console.log(data);import osimport requests
res = requests.post( "https://restlink23telecom.com/api/v1/user/webhooks/test", headers={"X-API-Key": os.environ["API_KEY"]}, json={ "type": "delivery" },)res.raise_for_status()print(res.json())<?php$ch = curl_init('https://restlink23telecom.com/api/v1/user/webhooks/test');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['X-API-Key: ' . getenv('API_KEY'), 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'type' => 'delivery' ]),]);
$response = curl_exec($ch);$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);curl_close($ch);
if ($status !== 200) { throw new Exception("HTTP $status: $response");}$data = json_decode($response, true);print_r($data);require "net/http"require "json"
uri = URI("https://restlink23telecom.com/api/v1/user/webhooks/test")req = Net::HTTP::Post.new(uri)req["X-API-Key"] = ENV.fetch("API_KEY")req["Content-Type"] = "application/json"req.body = { type: "delivery"}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }raise "HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
puts JSON.parse(res.body)import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://restlink23telecom.com/api/v1/user/webhooks/test")) .header("X-API-Key", System.getenv("API_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(""" { "type": "delivery" }""")) .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());System.out.println(response.body());package main
import ( "fmt" "io" "net/http" "os" "strings")
func main() { payload := strings.NewReader(`{ "type": "delivery"}`)
req, err := http.NewRequest("POST", "https://restlink23telecom.com/api/v1/user/webhooks/test", payload) if err != nil { panic(err) } req.Header.Set("X-API-Key", os.Getenv("API_KEY")) req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, _ := io.ReadAll(res.Body) fmt.Println(string(data))}using System.Text;
using var client = new HttpClient();client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("API_KEY"));
var json = """{ "type": "delivery"}""";var content = new StringContent(json, Encoding.UTF8, "application/json");var response = await client.PostAsync("https://restlink23telecom.com/api/v1/user/webhooks/test", content);
response.EnsureSuccessStatusCode();Console.WriteLine(await response.Content.ReadAsStringAsync()); GET
/api/v1/user/webhooks/logs
· Permission: webhooks.read
curl "https://restlink23telecom.com/api/v1/user/webhooks/logs?limit=10" \ -H "X-API-Key: $API_KEY"const res = await fetch('https://restlink23telecom.com/api/v1/user/webhooks/logs?limit=10', { method: 'GET', headers: { 'X-API-Key': process.env.API_KEY },});
if (!res.ok) { const err = await res.json(); throw new Error(`${err.error_code}: ${err.description}`);}const data = await res.json();console.log(data);import osimport requests
res = requests.get( "https://restlink23telecom.com/api/v1/user/webhooks/logs", headers={"X-API-Key": os.environ["API_KEY"]}, params={ "limit": 10 },)res.raise_for_status()print(res.json())<?php$ch = curl_init('https://restlink23telecom.com/api/v1/user/webhooks/logs?limit=10');curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['X-API-Key: ' . getenv('API_KEY')],]);
$response = curl_exec($ch);$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);curl_close($ch);
if ($status !== 200) { throw new Exception("HTTP $status: $response");}$data = json_decode($response, true);print_r($data);require "net/http"require "json"
uri = URI("https://restlink23telecom.com/api/v1/user/webhooks/logs?limit=10")req = Net::HTTP::Get.new(uri)req["X-API-Key"] = ENV.fetch("API_KEY")
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }raise "HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
puts JSON.parse(res.body)import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://restlink23telecom.com/api/v1/user/webhooks/logs?limit=10")) .header("X-API-Key", System.getenv("API_KEY")) .GET() .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());System.out.println(response.body());package main
import ( "fmt" "io" "net/http" "os")
func main() { req, err := http.NewRequest("GET", "https://restlink23telecom.com/api/v1/user/webhooks/logs?limit=10", nil) if err != nil { panic(err) } req.Header.Set("X-API-Key", os.Getenv("API_KEY"))
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, _ := io.ReadAll(res.Body) fmt.Println(string(data))}using System.Text;
using var client = new HttpClient();client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("API_KEY"));
var response = await client.GetAsync("https://restlink23telecom.com/api/v1/user/webhooks/logs?limit=10");
response.EnsureSuccessStatusCode();Console.WriteLine(await response.Content.ReadAsStringAsync());Logs include the payload sent, response code, response time and error details for every delivery attempt.
- Delivery webhook — payload, retries, receiver examples
- Securing webhooks — HMAC signatures, secret rotation
- Tracker postbacks — conversion ingestion and URL placeholders