Documentation

Webhooks

Hiras can forward inbound texts to your own backend. When the phone receives an SMS, Hiras POSTs it to a URL you configure, signed so you can verify it came from your gateway. It’s forward-and-forget: Hiras keeps no inbox and stores no inbound message — once your endpoint has it, Hiras is done.

Setting it up

In the Webhooks screen, set your webhook URL and turn webhooks on. Only http and https URLs are accepted. On a multi-SIM phone you can filter which SIM’s inbound messages get forwarded. There is no separate secret to configure — Hiras signs each delivery with your existing API key (covered under Verifying the signature).

The inbound receiver is only active while the gateway is running and a webhook is enabled — with no webhook set, Hiras never reads your inbox at all.

The payload

Each inbound message is delivered as a JSON POST with this exact shape:

{
  "event": "sms.received",
  "from": "09171234567",
  "message": "Reply text here",
  "receivedAt": "2026-07-06T09:15:00Z",
  "simSlot": 1,
  "receivedOn": "09991234567"
}
  • from — the sender’s number.
  • receivedAt — UTC, ISO-8601.
  • simSlot — the 1-based SIM that received it, or null.
  • receivedOn — the receiving number (MSISDN), or null if the carrier hides it.

simSlot and receivedOn are always present as keys; they’re null rather than fabricated when unknown.

Verifying the signature

Every delivery carries two headers:

X-Hiras-Timestamp: 1751792100
X-Hiras-Signature: sha256=<hmac>

The signature is an HMAC-SHA256 computed over the timestamp string concatenated directly with the raw JSON body, keyed by your API key. The key itself is never transmitted — a network observer sees only the one-way signature, never the secret that produced it.

To verify, recompute the HMAC on your side and compare:

import crypto from "node:crypto";

function verify(rawBody, timestamp, signature, apiKey) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", apiKey)
    .update(timestamp + rawBody)   // timestamp string + raw body, no separator
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

Because the timestamp is inside the signed material, you can also reject deliveries whose timestamp is too old to blunt replay attempts. Compute the HMAC over the raw body bytes you received, not a re-serialized copy.

Delivery and retries

Hiras tries each delivery up to four times — immediately, then after 1s, 2s, and 4s, with a 10-second timeout per attempt. It retries only on a network failure, a timeout, or an HTTP 5xx. Any 4xx (or other non-2xx) is treated as final and is not retried — so return a 2xx quickly once you’ve accepted the message, and a 4xx if you want Hiras to stop.

There is no persistent queue. If all four attempts fail, the message is dropped and the failure is recorded in the in-app delivery log. You can send a test event from the app to check your endpoint and signature verification end to end.

Next