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 |
A successful live API send has been queued for sending. This happens before the gateway accepts it or a delivery report arrives. |
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());GET /user/webhooks returns both the current settings and
available_fields. The latter lists selectable fields separately for
impression, click, conversion and delivery events.
An impression only confirms that a live API send was queued. It does not mean
the SMS was sent or delivered. Campaign sends do not fire an impression; use
the delivery event for the eventual outcome. The impression cost is the
price estimated at send time, not the final billed cost.
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
