Quick links
How to setup webhooks for SMSPool
- By SMSPool Admin
- SMSPool
A webhook sends each SMS to your server the moment it arrives, so you don't have to keep polling the API to check for a code. This guide covers setting one up, what we send, how to check that a request really came from SMSPool, and what happens when your server doesn't answer.
Where webhooks can go
- Your own server: a signed JSON
POSTfor every event, described below. - Discord (
https://discord.com/api/webhooks/...): a chat message for each SMS, showing the code and the full message. - Slack (
https://hooks.slack.com/...): a chat message for each SMS.
Discord and Slack need no code on your side. The rest of this guide is about your own server.
Set up your webhook
- Go to Settings and open the Developer tab.
- Under Webhook, enter your URL and your account password, then press Update webhook.
- Copy the Signing secret (it starts with
whsec_) into your server's configuration. You need it to check requests. - Press Send test event. The result appears under Webhook deliveries on the same page.
Your URL must:
- start with
https://and be no longer than 255 characters; - be reachable from the internet. Private, local and other reserved addresses are refused when you save the URL and again before each request;
- answer by itself. Redirects are not followed, so a
301or302counts as a failure.
Changing the URL later keeps the same signing secret.
What we send
Every request is a POST with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | SMSPool-Webhook/2.0 |
X-SMSPool-Event | The event type, for example order.code |
X-SMSPool-Event-Id | Unique per event. A retry of the same event uses the same ID |
X-SMSPool-Delivery-Attempt | 1 for the first try, up to 4 |
X-SMSPool-Signature | t=<unix time>,v1=<signature>, see "Check that a request came from SMSPool" below |
X-SMSPool-Secret | Your signing secret, only if you turn on the simple check (see below) |
order.code: an SMS arrived for an order
{
"event": "order.code",
"event_id": "evt_6f1c0e9a2b4d8f7e1a3c5b90",
"orderid": "AB12CD34",
"sms": "123456",
"full_sms": "Your verification code is 123456",
"type": "short_term",
"order_code": "AB12CD34",
"status": "completed",
"code": "123456",
"full_code": "Your verification code is 123456",
"phonenumber": "14155550123",
"timestamp": "2026-10-18 02:15:09pm",
"unix_timestamp": 1792332909
}New integrations should read order_code, code, full_code and unix_timestamp. The fields orderid, sms, full_sms, type and timestamp are still sent unchanged for older integrations. timestamp is in server time with no time zone, so prefer unix_timestamp.
An order that receives more than one SMS, for example after a resend, sends one order.code event per SMS.
order.status: an order ended without a code (optional)
Turn on Also send order.status events in the Developer tab to receive these.
{
"event": "order.status",
"event_id": "evt_0b7d2c4e6a8f1e3d5c7b9a01",
"order_code": "AB12CD34",
"orderid": "AB12CD34",
"status": "refunded",
"phonenumber": "14155550123",
"timestamp": "2026-10-18 02:35:12pm",
"unix_timestamp": 1792334112
}status is expired, refunded or cancelled. An order that expires and is then refunded sends two events. A completed order doesn't send an order.status event, because the order.code event already tells you.
rental.sms: an SMS arrived on a rental
{
"event": "rental.sms",
"event_id": "evt_3a5c7e9b1d2f4a6c8e0b2d4f",
"phonenumber": "14155550123",
"rental_code": "R1A2B3C4",
"full_sms": "Your code is 998877",
"timestamp": "2026-10-18 02:15:09pm",
"unix_timestamp": 1792332909
}rental.auto_extend: a rental was auto-extended, or couldn't be
{
"event": "rental.auto_extend",
"event_id": "evt_9e8d7c6b5a4f3e2d1c0b9a8f",
"rental_code": "R1A2B3C4",
"phonenumber": "14155550123",
"success": 1,
"timestamp": "2026-10-18 02:15:09pm",
"unix_timestamp": 1792332909
}success is 1 when the rental was extended and 0 when it couldn't be.
webhook.test: sent by the Send test event button
{
"event": "webhook.test",
"event_id": "evt_1f2e3d4c5b6a7f8e9d0c1b2a",
"message": "This is a test event from SMSPool.",
"timestamp": "2026-10-18 02:15:09pm",
"unix_timestamp": 1792332909
}Answer quickly
Answer with any 2xx status within 5 seconds (3 seconds to connect). If your processing takes longer, answer first and process afterwards.
Anything else, such as another status, a timeout or a refused connection, is retried after 15, 45 and 90 seconds. Each event is tried 4 times within about 3 minutes and then dropped, because orders expire a few minutes after purchase and a later code would be no use. If your server can be down for longer than that, keep checking orders through the API as a fallback.
Because of retries, the same event can reach you more than once, and events can arrive out of order. Use X-SMSPool-Event-Id (or event_id in the body) to skip events you've already handled.
Webhook deliveries in the Developer tab lists your last 25 attempts and their results. Entries are kept for 30 days.
Check that a request came from SMSPool
Anyone who learns your URL can send requests to it. Check the signature before you trust a request.
The X-SMSPool-Signature header looks like this:
X-SMSPool-Signature: t=1792843200,v1=268b68d207c255da63fcf785e80376ebc3699018a56f3a10b62a9d9fee6be899tis the Unix time (in seconds) when the request was signed.v1is an HMAC-SHA256, written as lowercase hexadecimal with nosha256=prefix.- The signed text is the timestamp, a full stop, then the raw request body:
<t>.<raw body>. - The key is your signing secret exactly as shown in Settings, including the
whsec_prefix. Don't decode it.
To check a request:
- Split the header on
,intotandv1. - Compute HMAC-SHA256 of
<t>.<raw body>with your secret as the key, and write it as hexadecimal. - Compare the result with
v1using a constant-time comparison. - Reject the request if
tis more than 5 minutes from your server's clock, so an old request can't be replayed.
Use the body exactly as you received it. If you parse the JSON and encode it again, the bytes change and the signature won't match.
Worked example
Use this example to test your code. The secret is made up.
- Secret:
whsec_0123456789abcdef0123456789abcdef0123456789abcdef t:1792843200- Raw body:
{"event":"webhook.test","event_id":"evt_0000000000000000000000aa"} - Signed text:
1792843200.{"event":"webhook.test","event_id":"evt_0000000000000000000000aa"} - Expected
v1:268b68d207c255da63fcf785e80376ebc3699018a56f3a10b62a9d9fee6be899
PHP
<?php
$secret = getenv('SMSPOOL_WEBHOOK_SECRET'); // whsec_..., exactly as shown in Settings
$body = file_get_contents('php://input'); // the raw body, before any JSON parsing
$header = $_SERVER['HTTP_X_SMSPOOL_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts);
$timestamp = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (!hash_equals($expected, $parts['v1'] ?? '') || abs(time() - $timestamp) > 300) {
http_response_code(401);
exit;
}
http_response_code(200);
$event = json_decode($body, true);
// Skip $event['event_id'] values you have already handled, then process the event.Node.js (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
// express.raw keeps the body as bytes, so the signature can be checked.
app.post('/smspool-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-SMSPool-Signature') || '';
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto
.createHmac('sha256', process.env.SMSPOOL_WEBHOOK_SECRET)
.update(`${parts.t}.${req.body}`)
.digest('hex');
const valid =
parts.v1 &&
parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) &&
Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
if (!valid) return res.sendStatus(401);
res.sendStatus(200);
const event = JSON.parse(req.body);
// Skip event.event_id values you have already handled, then process the event.
});Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["SMSPOOL_WEBHOOK_SECRET"].encode()
@app.post("/smspool-webhook")
def smspool_webhook():
header = request.headers.get("X-SMSPool-Signature", "")
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
body = request.get_data() # raw bytes
expected = hmac.new(SECRET, parts.get("t", "").encode() b"." body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")) or abs(time.time() - int(parts.get("t", 0))) > 300:
abort(401)
event = request.get_json()
# Skip event["event_id"] values you have already handled, then process the event.
return "", 200Simple check
If you'd rather not compute the HMAC, turn on Simple check under the signing secret in the Developer tab. Every request to your https:// URL then also carries your secret, exactly as shown in Settings, in an X-SMSPool-Secret header. Your server only has to compare it with the secret:
if (!hash_equals(getenv('SMSPOOL_WEBHOOK_SECRET'), $_SERVER['HTTP_X_SMSPOOL_SECRET'] ?? '')) {
http_response_code(401);
exit;
}This is weaker than the signature. Anything that logs request headers, such as a proxy or an error tracker, ends up holding your secret, and a captured request can be sent again. X-SMSPool-Signature is still sent, so you can switch to the full check at any time.
When you press Generate new secret, the old secret stops working immediately. Update your server straight away.
Troubleshooting
- The signature never matches. Make sure you sign the raw body, not parsed and re-encoded JSON. Make sure you include the
t.prefix and use the full secret,whsec_included. Then compare your code against the worked example above. - Nothing arrives. Check Webhook deliveries in the Developer tab for the error. Common causes are a firewall blocking our requests, a redirect, or an answer slower than 5 seconds.
- The webhook was switched off. Each dropped event sends you an on-site notification. After 100 dropped events the webhook is switched off. Fix the problem, then save your URL again to turn it back on.
Still stuck? Contact support and include the time of a failed delivery.
Return to knowledgebaseAbout the author
SMSPool Admin
The owner of SMSPool.net, a site that originally started as a hobby but saw rapid expansion due to the high demand, loves anything technology related and loves writing about technology related articles.
www.smspool.netOther interesting articles
SMSPool Business Account Explained
Learn how to unlock your free SMSPool Business Account. Get step-by-step videos on creating sub-accounts, funding your team, and managing access
- By Admin
- April 16, 2026
How to create a Greggs account
This a step by step guide on how to create a Greggs account, bypass SMS verification and get free treats!
- By Admin
- June 26, 2025
How to Get an Australian Phone Number
Need an Australian phone number but not in Australia? Get a +61 number online with SMSPool, no SIM or ID required. Verify Gumtree and more in seconds.
- By Admin
- September 18, 2026
How to get a Signal SMS verification
This guide shows you how to get an SMS verification for Signal without a phone number.
- By Admin
- May 25, 2023
How to get a Facebook SMS verification
In this guide we will show you a step to step guide on registering on Facebook without a phone number, using a Facebook SMS verification service.
- By Admin
- May 24, 2023
Create an anonymous account on SMSPool
This guide showcases how to create an anonymous account on SMSPool.
- By Admin
- November 17, 2025
How to get a Zalo SMS verification
This guide shows you how to create a Zalo account with our Zalo SMS verification service.
- By Admin
- April 18, 2024