Developers
Webhooks
When an OTP arrives, kiwiSMS POSTs JSON to the webhook URL on your account or API key. Keep polling as a backup — failed deliveries are logged and not retried automatically.
Payload
Event name is always sms.received. Temporary rentals use a numeric rentalId; long-term ids are strings.
JSON
{
"event": "sms.received",
"rentalId": "1042",
"kind": "s1",
"serviceNo": 5,
"code": "s1-5",
"serviceName": "Google",
"phone": "+12025551234",
"otpCode": "123456",
"smsText": "Your Google verification code is 123456",
"receivedAt": "2026-05-29T12:00:00.000Z"
}Signature
Header X-Webhook-Signature is the HMAC-SHA256 hex digest of the raw request body using your webhook secret. Respond with HTTP 2xx to acknowledge.
Verify the raw bytes
Do not re-serialize JSON before checking the signature. Parse only after the HMAC matches.
Verify (Node.js)
express.raw
JavaScript
import crypto from "crypto";
function verifyWebhook(rawBody, secret, signatureHeader) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signatureHeader.trim(), "hex"),
Buffer.from(expected, "hex")
);
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
const sig = req.headers["x-webhook-signature"];
if (!verifyWebhook(raw, process.env.WEBHOOK_SECRET, sig)) {
return res.status(401).send("invalid signature");
}
const payload = JSON.parse(raw);
console.log(payload.otpCode, payload.code);
res.sendStatus(200);
});Verify (Python)
Python
import hmac, hashlib
def verify_webhook(raw_body: bytes, secret: str, signature: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature.strip())
@app.post("/webhook")
def webhook():
raw = request.get_data()
sig = request.headers.get("X-Webhook-Signature", "")
if not verify_webhook(raw, WEBHOOK_SECRET, sig):
return "invalid signature", 401
payload = request.json
return "", 200