eHub SMS REST API
Send SMS, manage sender IDs, and check your wallet programmatically. All endpoints require HMAC-signed requests.
Overview
Everything you need to integrate eHub SMS into your application.
The eHub SMS API is a REST API that accepts JSON request bodies and returns JSON responses. Every request must be authenticated with a Bearer token and signed with an HMAC-SHA256 signature to prevent replay attacks.
Authentication
Every request requires your API key in the Authorization header.
Authorization: Bearer YOUR_API_KEY
Your API key identifies your account. Keep it secret — never expose it in client-side code or public repositories. All requests must also include a valid HMAC signature (see below).
Request Signing
Every request must be signed with HMAC-SHA256 using your API secret.
401 Unauthorized.Signing algorithm
# Concatenate these four values with newlines {unix_timestamp} {HTTP_METHOD} {/full/path} {raw_body} # Signature signature = HMAC-SHA256(secret, payload) # hex-encoded
Required headers
| Header | Value | Notes |
|---|---|---|
X-Timestamp | Unix timestamp (integer) | Must be within ±5 min of server time |
X-Signature | HMAC-SHA256 hex string | Signed with your API secret |
"" as the body. Timestamps use Unix time (seconds since epoch) — timezone doesn't matter.Code examples
// PHP signing example $apiKey = 'sk_your_api_key'; $secret = 'your_api_secret'; $method = 'POST'; $path = '/api/v1/sms/send'; $body = json_encode(['to' => '255755000000', 'message' => 'Hello!', 'sender_id' => 'uuid-here']); $timestamp = time(); $payload = $timestamp . "\n" . $method . "\n" . $path . "\n" . $body; $signature = hash_hmac('sha256', $payload, $secret); $response = Http::withHeaders([ 'Authorization' => 'Bearer ' . $apiKey, 'X-Timestamp' => $timestamp, 'X-Signature' => $signature, 'Content-Type' => 'application/json', ])->post('https://sms.ehub.co.tz/api/v1/sms/send', json_decode($body, true));
# Python signing example import hmac, hashlib, time, json, requests api_key = 'sk_your_api_key' secret = 'your_api_secret' method = 'POST' path = '/api/v1/sms/send' body = json.dumps({'to': '255755000000', 'message': 'Hello!', 'sender_id': 'uuid-here'}) timestamp = str(int(time.time())) payload = f"{timestamp}\n{method}\n{path}\n{body}" signature = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest() response = requests.post( 'https://sms.ehub.co.tz/api/v1/sms/send', headers={ 'Authorization': f'Bearer {api_key}', 'X-Timestamp': timestamp, 'X-Signature': signature, 'Content-Type': 'application/json', }, data=body )
// Go signing example package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "net/http" "strings" "strconv" "time" ) func sign(secret, method, path, body string) (string, string) { ts := strconv.FormatInt(time.Now().Unix(), 10) payload := ts + "\n" + method + "\n" + path + "\n" + body mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(payload)) return ts, hex.EncodeToString(mac.Sum(nil)) } func main() { apiKey := "sk_your_api_key" secret := "your_api_secret" body := `{"to":"255755000000","message":"Hello!","sender_id":"uuid-here"}` ts, sig := sign(secret, "POST", "/api/v1/sms/send", body) req, _ := http.NewRequest("POST", "https://sms.ehub.co.tz/api/v1/sms/send", strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+apiKey) req.Header.Set("X-Timestamp", ts) req.Header.Set("X-Signature", sig) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) fmt.Println(resp.Status, err) }
// Java signing example import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.net.URI; import java.net.http.*; import java.time.Instant; public class EhubSmsClient { static String sign(String secret, String method, String path, String body) throws Exception { String ts = String.valueOf(Instant.now().getEpochSecond()); String payload = ts + "\n" + method + "\n" + path + "\n" + body; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256")); byte[] raw = mac.doFinal(payload.getBytes()); StringBuilder hex = new StringBuilder(); for (byte b : raw) hex.append(String.format("%02x", b)); return ts + ":" + hex; // returns "timestamp:signature" } public static void main(String[] args) throws Exception { String apiKey = "sk_your_api_key"; String secret = "your_api_secret"; String body = "{\"to\":\"255755000000\",\"message\":\"Hello!\",\"sender_id\":\"uuid-here\"}"; String[] parts = sign(secret, "POST", "/api/v1/sms/send", body).split(":", 2); String ts = parts[0]; String sig = parts[1]; HttpRequest req = HttpRequest.newBuilder() .uri(URI.create("https://sms.ehub.co.tz/api/v1/sms/send")) .header("Authorization", "Bearer " + apiKey) .header("X-Timestamp", ts) .header("X-Signature", sig) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse<String> resp = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()); System.out.println(resp.body()); } }
// Node.js signing example const crypto = require('crypto'); const API_KEY = 'sk_your_api_key'; const SECRET = 'your_api_secret'; const BASE_URL = 'https://sms.ehub.co.tz'; async function sendSms(to, message, senderId) { const path = '/api/v1/sms/send'; const body = JSON.stringify({ to, message, sender_id: senderId }); const timestamp = Math.floor(Date.now() / 1000); const payload = [timestamp, 'POST', path, body].join('\n'); const signature = crypto .createHmac('sha256', SECRET) .update(payload) .digest('hex'); const res = await fetch(BASE_URL + path, { method: 'POST', headers: { 'Authorization': `Bearer ${API_KEY}`, 'X-Timestamp': String(timestamp), 'X-Signature': signature, 'Content-Type': 'application/json', }, body, }); const data = await res.json(); console.log(data); } sendSms('255755000000', 'Hello!', 'uuid-here');
# cURL signing example (bash) API_KEY="sk_your_api_key" SECRET="your_api_secret" BODY='{"to":"255755000000","message":"Hello!","sender_id":"uuid-here"}' TS=$(date +%s) PAYLOAD="${TS} POST /api/v1/sms/send ${BODY}" SIG=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') curl -s -X POST https://sms.ehub.co.tz/api/v1/sms/send \ -H "Authorization: Bearer $API_KEY" \ -H "X-Timestamp: $TS" \ -H "X-Signature: $SIG" \ -H "Content-Type: application/json" \ -d "$BODY"
Rate Limiting
120 requests per minute per API key.
Every response includes rate limit headers so you can track your usage in real time. When the limit is exceeded, the API returns 429 Too Many Requests with a retry_after field.
| Header | Description |
|---|---|
X-RateLimit-Limit | Max requests per minute for your key |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Response Format
All responses are JSON with a consistent envelope.
{
"success": true,
"message": "SMS sent successfully",
"data": { /* endpoint-specific payload */ },
"timestamp": "2026-06-05T10:00:00+03:00"
}{
"success": false,
"message": "Invalid sender_id.",
"timestamp": "2026-06-05T10:00:00+03:00"
}Error Codes
Standard HTTP status codes with descriptive messages.
| Status | Meaning |
|---|---|
| 401 | Missing/invalid API key · Expired timestamp · Invalid HMAC signature · Inactive account |
| 403 | Sender ID not found or not accessible · Sender ID not yet approved |
| 409 | Duplicate sender ID — you have already registered this name |
| 422 | Validation failed — check the errors field for details |
| 429 | Rate limit exceeded — wait until the window resets (retry_after seconds) |
| 500 | Internal server error — contact support if this persists |
Sender IDs
Manage the alphanumeric names that appear as the SMS sender.
id (UUID) field when sending SMS. GET /api/v1/sender-ids Authorization: Bearer sk_... X-Timestamp: 1780658993 X-Signature: abc123...
{
"success": true,
"data": {
"own": [
{
"id": "00420892-38bd-47b0-9a5f-ea55bef5d2d1",
"sender_name": "MICHANGO",
"status": "approved",
"purpose": "TRANSACTIONAL",
"is_default": false,
"type": "own",
"created_at": "2026-01-27T14:40:26+03:00"
}
],
"shared": [],
"public": []
}
}| Field | Type | Description | |
|---|---|---|---|
sender_id | string | required | 4–11 alphanumeric characters, will be uppercased |
purpose | string | required | One of: Notification, Transactional, OTP / Verification, Promotional, Marketing, Alerts & Reminders, Other |
sample | string | required | A realistic sample of the messages you'll send (50–250 characters) |
POST /api/v1/sender-ids Authorization: Bearer sk_... X-Timestamp: 1780658993 X-Signature: abc123... Content-Type: application/json { "sender_id": "MYBRAND", "purpose": "Promotional", "sample": "Dear customer, enjoy 20% off all plans this week. Reply STOP to opt out." }
{
"success": true,
"message": "Sender ID submitted for approval.",
"data": {
"id": "e47c45f3-43ce-4bcc-b868-9db92840f11f",
"sender_name": "MYBRAND",
"status": "pending",
"purpose": "Promotional",
"created_at": "2026-06-05T14:33:52+03:00"
}
}SMS
Send single or bulk SMS messages to Tanzanian phone numbers.
| Field | Type | Description | |
|---|---|---|---|
to | string | required | Recipient phone number (e.g. 255755000000 or 0755000000) |
message | string | required | SMS body text, max 640 characters (4 SMS parts) |
sender_id | uuid | required | UUID of an approved sender ID from GET /sender-ids |
POST /api/v1/sms/send Authorization: Bearer sk_... X-Timestamp: 1780658993 X-Signature: abc123... Content-Type: application/json { "to": "255755957514", "message": "Your verification code is 123456", "sender_id": "00420892-38bd-47b0-9a5f-ea55bef5d2d1" }
{
"success": true,
"message": "SMS sent successfully",
"data": {
"message_id": "2d853818-5529-4838-bee0-1cb012b1a136",
"to": "255755957514",
"status": "sent",
"cost": "25.00",
"parts": 1,
"created_at": "2026-06-05T13:45:33+03:00"
}
}| Field | Type | Description | |
|---|---|---|---|
recipients | array | required | Array of phone numbers, max 1000. Duplicates are removed. |
message | string | required | SMS body text, max 640 characters |
sender_id | uuid | required | UUID of an approved sender ID |
campaign_name | string | optional | Label for this campaign, max 255 characters |
scheduled_at | datetime | optional | ISO 8601 future datetime to schedule the send |
POST /api/v1/sms/send-bulk Content-Type: application/json { "recipients": ["255755000001", "255755000002", "255755000003"], "message": "Flash sale! 30% off today only.", "sender_id": "00420892-38bd-47b0-9a5f-ea55bef5d2d1", "campaign_name": "June Flash Sale", "scheduled_at": null }
{
"success": true,
"message": "Bulk SMS campaign created successfully",
"data": {
"campaign_id": "f84e8878-27be-4bce-aac8-98e1b3ec19a0",
"name": "June Flash Sale",
"total_recipients": 3,
"total_cost": "75.00",
"status": "processing",
"scheduled_at": null,
"created_at": "2026-06-05T13:45:57+03:00",
"messages": [ // one entry per recipient — store these to match delivery webhooks
{ "message_id": "9757991f-...-uuid", "to": "0712345678" },
{ "message_id": "a1b2c3d4-...-uuid", "to": "0713111222" },
{ "message_id": "c3d4e5f6-...-uuid", "to": "0714333444" }
]
}
}| Parameter | Type | Description | |
|---|---|---|---|
status | string | optional | pending · queued · sent · delivered · failed |
campaign_id | string | optional | Return only messages from this campaign (the id from /sms/send-bulk) — pull every message_id and recipient after a bulk send |
from_date | date | optional | Filter from date (YYYY-MM-DD) |
to_date | date | optional | Filter to date (YYYY-MM-DD) |
limit | integer | optional | Results per page, max 100. Default: 20 |
page | integer | optional | Page number. Default: 1 |
GET /api/v1/sms/history?status=delivered&limit=20&page=1 Authorization: Bearer sk_... X-Timestamp: 1780658993 X-Signature: abc123...
{
"success": true,
"data": {
"messages": [
{
"message_id": "2d853818-5529-4838-bee0-1cb012b1a136",
"to": "255755957514",
"message": "Your code is 123456",
"sender_id": "MICHANGO",
"status": "delivered",
"cost": "25.00",
"parts": 1,
"sent_at": "2026-06-05T13:45:35+03:00",
"delivered_at": "2026-06-05T13:45:37+03:00",
"created_at": "2026-06-05T13:45:33+03:00"
}
],
"pagination": {
"current_page": 1,
"per_page": 20,
"total": 142,
"last_page": 8
}
}
}GET /api/v1/sms/2d853818-5529-4838-bee0-1cb012b1a136
{
"success": true,
"data": {
"message_id": "2d853818-5529-4838-bee0-1cb012b1a136",
"to": "255755957514",
"status": "delivered",
"cost": "25.00",
"sent_at": "2026-06-05T13:45:35+03:00",
"delivered_at": "2026-06-05T13:45:37+03:00",
"error_message": null,
"created_at": "2026-06-05T13:45:33+03:00"
}
}Wallet
Check your SMS balance and transaction history.
GET /api/v1/wallet/balance
{
"success": true,
"data": {
"balance": "0.00",
"sms_balance": 78,
"currency": "TZS",
"updated_at": "2026-06-05T13:45:16+03:00"
}
}| Parameter | Type | Description | |
|---|---|---|---|
limit | integer | optional | Results per page, max 100. Default: 20 |
page | integer | optional | Page number. Default: 1 |
GET /api/v1/wallet/transactions?limit=20&page=1 Authorization: Bearer sk_... X-Timestamp: 1780658993 X-Signature: abc123...
{
"success": true,
"data": {
"transactions": [
{
"id": "txn_9c3a1d88-f401-4b2a-91c3-d7ec1e028f3b",
"type": "debit",
"amount": "25.00",
"description": "SMS to 255755957514",
"balance": "1950.00",
"created_at": "2026-06-05T13:45:33+03:00"
},
{
"id": "txn_2aa7e1b3-0c43-4d19-b98c-0fa3c9b23ef0",
"type": "credit",
"amount": "5000.00",
"description": "Wallet top-up via Snippe",
"balance": "1975.00",
"created_at": "2026-06-04T09:12:05+03:00"
}
],
"pagination": {
"current_page": 1,
"per_page": 20,
"total": 47,
"last_page": 3
}
}
}Plans & Payments
View pricing tiers, initiate top-ups, and check payment status.
GET /api/v1/plans
{
"success": true,
"data": {
"plans": [
{
"name": "Starter",
"min_sms": 1,
"max_sms": 999,
"price_per_sms": 16,
"currency": "TZS"
},
{
"name": "Growth",
"min_sms": 1000,
"max_sms": 4999,
"price_per_sms": 14,
"currency": "TZS"
}
]
}
}422.| Parameter | Type | Description | |
|---|---|---|---|
sms_quantity | integer | required | Number of SMS credits to purchase |
phone_number | string | required | Mobile money number (e.g. 0755957514) |
idempotency_key | string | optional | Unique key (max 64 chars) to prevent duplicate charges |
POST /api/v1/payments/initiate { "sms_quantity": 100, "phone_number": "0755957514", "idempotency_key": "pay_abc123" }
{
"success": true,
"message": "Payment initiated. Check your phone.",
"data": {
"order_reference": "ORD260721092015ABCDE",
"amount": 1600,
"sms_quantity": 100,
"price_per_sms": 16,
"currency": "TZS",
"gateway": "snippe"
}
}{
"success": false,
"message": "Daily API top-up limit of TZS 150,000 exceeded. You have TZS 20,000 remaining today.",
"errors": {
"daily_limit_tzs": 150000,
"spent_today_tzs": 130000,
"remaining_today_tzs": 20000
}
}| Status | Description | ||
|---|---|---|---|
pending | Payment created, waiting for gateway | ||
processing | Push sent to phone, waiting for confirmation | ||
completed | Payment successful, SMS credits added | ||
failed | Payment failed or expired | ||
cancelled | Payment cancelled by user | ||
GET /api/v1/payments/ORD260721092015ABCDE/status
{
"success": true,
"data": {
"order_reference": "ORD260721092015ABCDE",
"amount": 1600,
"sms_quantity": 100,
"status": "completed",
"currency": "TZS",
"completed_at": "2026-07-21T09:20:45+03:00",
"created_at": "2026-07-21T09:20:15+03:00"
}
}status, from_date, to_date, limit (max 100), page.{
"data": {
"payments": [ {
"order_reference": "ORD...",
"amount": 3000,
"sms_quantity": 100,
"price_per_sms": 30,
"status": "completed",
"gateway": "clickpesa",
"completed_at": "2026-08-30T09:15:00+03:00"
} ],
"pagination": { "total": 1, "per_page": 20, "current_page": 1, "last_page": 1 }
}
}Reseller & Sub-accounts
Give each of your own customers an independent SMS balance under one eHub account.
Create an API key per customer, then allocate SMS credits to it from your main wallet. A metered key spends only its own balance, so your customers are billed and rate-limited independently — and can never spend each other's or your credits.
Master vs customer keys
Only a master key may manage keys or move wallet credits (the endpoints below). Designate exactly one master key in the dashboard (API Keys → Set as master). Every other key — including ones created via the API for your customers — is non-master and cannot create keys or allocate credit. Calling these endpoints with a non-master key returns 403.
How allocation works
Allocating moves credits wallet → key. If your wallet is short, we fund what's available now and record the rest as pending (visible to the customer but not spendable). Pending settles automatically, oldest-first, the next time you top up your wallet.
key and secret are returned once — store them now; the key is stored only as a hash and cannot be retrieved again.| Field | Type | Description | |
|---|---|---|---|
name | string | required | Label for the key (e.g. the customer's name) |
metered | boolean | optional | Bill this key's own balance. Auto-enabled on first allocation. |
expires_in_days | integer | optional | Auto-expire after N days (1–3650) |
POST /api/v1/api-keys Authorization: Bearer sk_master... { "name": "George", "metered": true }
{
"success": true,
"message": "API key created. Store the key and secret now — they will not be shown again.",
"data": {
"id": 42,
"name": "George",
"key": "sk_9f8c1e2a...",
"secret": "a1b2c3...",
"metered": true
}
}{
"data": { "keys": [ {
"id": 42, "name": "George",
"is_active": true, "metered": true,
"sms_balance": 3000, "sms_pending": 2000
} ] }
}id (from the list endpoint). Immediately blocks that customer.{
"success": true,
"message": "API key deactivated.",
"data": { "id": 42, "is_active": false }
}| Field | Type | Description | |
|---|---|---|---|
key | string | required | The customer key's plaintext value (sk_…) |
sms | integer | required | Credits to allocate |
sender_id | uuid | optional | Sender UUID for the confirmation SMS (must be one you can use) |
customer_name | string | optional | Personalizes the confirmation |
customer_msisdn | string | optional | Where to send the confirmation SMS |
message | string | optional | Custom template: {customer_name} {sender} {sms} {available} {pending} |
idempotency_key | string | optional | Safe retries — the same key returns the original result |
POST /api/v1/allocations Authorization: Bearer sk_master... { "key": "sk_9f8c1e2a...", "sms": 5000, "sender_id": "e47c45f3-...-uuid", "customer_name": "George", "customer_msisdn": "0712345678" }
{
"success": true,
"data": {
"funded": 3000,
"pending": 2000,
"key_usable": 3000,
"key_pending": 2000,
"notice": { "message_id": "...", "status": "sent" }
}
}| Field | Type | Description | |
|---|---|---|---|
key | string | required | The customer key's plaintext value (sk_…) |
sms | integer | required | Credits to return to your wallet (capped at the key's usable balance) |
{
"success": true,
"data": {
"returned": 1500,
"key_usable": 2500,
"reseller_sms_balance": 2500
}
}GET /wallet/balance?key=sk_….{
"data": {
"keys": [ {
"name": "George",
"sms_balance": 3000, // usable now
"sms_reserved": 0,
"sms_pending": 2000, // allocated, not yet funded
"is_active": true,
"last_used_at": "2026-08-30T09:15:00+03:00"
} ]
}
}limit (max 100) and page.{
"data": {
"allocations": [ {
"key": "George",
"customer_name": "George",
"funded": 3000,
"pending": 2000,
"created_at": "2026-08-30T09:15:00+03:00"
} ],
"pagination": { "total": 1, "per_page": 20, "current_page": 1, "last_page": 1 }
}
}Customers
Onboard your own customers as headless eHub accounts — they use the API through your system and never log in.
| Field | Type | Description | |
|---|---|---|---|
name | string | required | Customer's name |
email | string | required | Unique email (identifier; no login email is sent) |
phone_number | string | required | Tanzania format (0…/255…/+255…) |
key_name | string | optional | Label for the issued key |
POST /api/v1/customers Authorization: Bearer sk_master... { "name": "Acme Ltd", "email": "ops@acme.co.tz", "phone_number": "0712345678" }
{
"success": true,
"data": {
"customer_id": 128,
"name": "Acme Ltd",
"email": "ops@acme.co.tz",
"api_key": {
"id": 42,
"key": "sk_9f8c1e2a...",
"secret": "a1b2c3..."
}
}
}limit (max 100) and page.{
"data": {
"customers": [ {
"customer_id": 128,
"name": "Acme Ltd",
"email": "ops@acme.co.tz",
"phone_number": "0712345678",
"is_active": true,
"api_keys": 1,
"created_at": "2026-08-30T09:15:00+03:00"
} ],
"pagination": { "total": 1, "per_page": 20, "current_page": 1, "last_page": 1 }
}
}Delivery Report Webhook
Get a real-time callback when your messages are delivered or fail.
Set an HTTPS endpoint in the dashboard (Developer → Webhook). Whenever one of your messages reaches a terminal state, we POST a JSON payload to it. Each POST carries an X-eHub-Signature header — the HMAC-SHA256 of the raw request body, keyed with your webhook secret. Recompute it and compare (constant-time) to verify the call really came from eHub.
Events
| Event | When |
|---|---|
sms.delivered | The handset confirmed receipt. |
sms.failed | The network gave up; error says why. |
webhook.test | Sent from the dashboard's "Send a test event" only. |
Headers on every request
| Header | Value |
|---|---|
X-eHub-Event | The event name, for routing before you parse the body. |
X-eHub-Signature | HMAC-SHA256 of the raw body, hex, keyed with your webhook secret. |
Content-Type | application/json |
Payload
{
"event": "sms.delivered", // or "sms.failed"
"message_id": "9757991f-...-uuid",
"to": "+255712345678",
"status": "delivered",
"sender_id": "MYBRAND",
"error": null,
"sent_at": "2026-08-30T09:14:50+03:00",
"delivered_at": "2026-08-30T09:15:00+03:00",
"failed_at": null,
"timestamp": 1788069700
}Verify the signature
$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $body, $yourWebhookSecret);
if (! hash_equals($expected, $_SERVER['HTTP_X_EHUB_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}import hmac, hashlib
expected = hmac.new(
your_webhook_secret.encode(), raw_body, hashlib.sha256
).hexdigest()
sig = request.headers.get('X-eHub-Signature', '')
if not hmac.compare_digest(expected, sig):
abort(401)const crypto = require('crypto');
const expected = crypto
.createHmac('sha256', yourWebhookSecret)
.update(rawBody) // the raw request body
.digest('hex');
const sig = req.get('X-eHub-Signature') || '';
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.sendStatus(401);
}body, _ := io.ReadAll(r.Body)
mac := hmac.New(sha256.New, []byte(yourWebhookSecret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(r.Header.Get("X-eHub-Signature"))) {
w.WriteHeader(http.StatusUnauthorized)
return
}Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String expected = HexFormat.of().formatHex(
mac.doFinal(rawBody.getBytes(StandardCharsets.UTF_8)));
String sig = request.getHeader("X-eHub-Signature");
if (!MessageDigest.isEqual(expected.getBytes(), sig.getBytes())) {
response.setStatus(401);
return;
}require 'openssl'
expected = OpenSSL::HMAC.hexdigest('SHA256', your_webhook_secret, raw_body)
sig = request.env['HTTP_X_EHUB_SIGNATURE'].to_s
unless Rack::Utils.secure_compare(expected, sig)
halt 401
endRetries
Respond with any 2xx within 10 seconds to acknowledge. Anything else is retried after 30 s, 2 min and 10 min — four attempts in total — then dropped. Retries mean your handler should be idempotent on message_id.