Docs

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