ReceiveSMS.ink

Webhook Integration

Receive real-time HTTP POST notifications when an SMS arrives at your private number.

Overview

When you configure a webhook URL in your account settings, every inbound SMS to your private number triggers an HTTP POST to your endpoint. The request includes a JSON payload with the message details and an HMAC-SHA256 signature for verification.

Setup

  1. Go to your Inbox page and find the Webhook card
  2. Enter your endpoint URL (must be publicly accessible via HTTP or HTTPS)
  3. Click Save — a signing secret is automatically generated
  4. Copy the signing secret and store it securely in your application

Payload Format

Every webhook request is an HTTP POST with Content-Type: application/json.

{
  "event": "sms.received",
  "version": "1",
  "data": {
    "messageId": 42,
    "from": "+14155551234",
    "to": "+12025550100",
    "body": "Hello, this is a test message",
    "receivedAt": "2026-03-01T15:30:00.000Z"
  }
}

Field Reference

FieldTypeDescription
eventstringAlways sms.received
versionstringPayload version. Currently 1
data.messageIdnumberUnique message identifier
data.fromstringSender phone number in E.164 format
data.tostringYour private number in E.164 format
data.bodystringSMS message content
data.receivedAtstringISO 8601 timestamp (UTC)

Verifying Signatures

Every webhook request includes an X-Webhook-Signature header containing an HMAC-SHA256 signature of the request body, prefixed with sha256=.

To verify a webhook request:

  1. Read the raw request body (do not parse it first)
  2. Compute HMAC-SHA256 of the raw body using your signing secret
  3. Compare the computed signature with the header value using constant-time comparison
  4. Reject the request if signatures do not match

Code Examples

Node.js (Express)

const crypto = require('crypto');
const express = require('express');
const app = express();

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const expected = 'sha256=' + crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  console.log('SMS received:', event.data.from, event.data.body);
  res.status(200).send('OK');
});

app.listen(3000);

Python (Flask)

import hmac
import hashlib
import os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ['WEBHOOK_SECRET']

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Webhook-Signature', '')
    expected = 'sha256=' + hmac.new(
        WEBHOOK_SECRET.encode(),
        request.data,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        abort(401)

    event = request.get_json()
    print(f"SMS received: {event['data']['from']} - {event['data']['body']}")
    return 'OK', 200

Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

var webhookSecret = os.Getenv("WEBHOOK_SECRET")

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)

    mac := hmac.New(sha256.New, []byte(webhookSecret))
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))

    signature := r.Header.Get("X-Webhook-Signature")
    if !hmac.Equal([]byte(signature), []byte(expected)) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    var event struct {
        Event string `json:"event"`
        Data  struct {
            From string `json:"from"`
            Body string `json:"body"`
        } `json:"data"`
    }
    json.Unmarshal(body, &event)
    fmt.Printf("SMS received: %s - %s\n", event.Data.From, event.Data.Body)
    w.WriteHeader(http.StatusOK)
}

func main() {
    http.HandleFunc("/webhook", webhookHandler)
    http.ListenAndServe(":3000", nil)
}

Ruby (Sinatra)

require 'sinatra'
require 'openssl'
require 'json'

WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']

post '/webhook' do
  payload = request.body.read
  signature = request.env['HTTP_X_WEBHOOK_SIGNATURE']
  expected = 'sha256=' + OpenSSL::HMAC.hexdigest('SHA256', WEBHOOK_SECRET, payload)

  unless Rack::Utils.secure_compare(signature, expected)
    halt 401, 'Invalid signature'
  end

  event = JSON.parse(payload)
  puts "SMS received: #{event['data']['from']} - #{event['data']['body']}"
  status 200
  'OK'
end

Security Notes

  • Always verify signatures before processing webhook data to ensure requests originate from ReceiveSMS.ink
  • Use constant-time comparison to prevent timing attacks when comparing signatures
  • Use HTTPS for your webhook endpoint in production to encrypt data in transit
  • Respond quickly — webhook requests time out after 5 seconds. Process asynchronously if your handler needs more time
  • Regenerate your secret if you suspect it has been compromised. You can do this from your inbox settings

Delivery Behavior

  • Webhooks are delivered once per message — there are no automatic retries
  • If your endpoint returns a non-2xx status or times out, the delivery is marked as failed
  • You can see the last delivery status in your webhook settings card
  • Messages are always available in your web inbox regardless of webhook delivery status

Need help? Contact us at [email protected]