Skip to main content

Security & Webhooks

Receiving Webhooks

When CallMeLater delivers an HTTP action, it sends a signed request to your endpoint.

Request headers

HeaderDescription
Content-Typeapplication/json
User-AgentCallMeLater/1.0
X-CallMeLater-Action-IdAction UUID
X-CallMeLater-TimestampUnix timestamp
X-CallMeLater-SignatureHMAC signature (if secret configured)

The body is the exact JSON you provided when creating the action, plus any custom headers you specified.

Response expectations

Your ResponseWhat Happens
2xxSuccess — action marked as executed
4xx (except 429)Permanent failure — no retry
429Rate limited — retry with backoff
5xxTemporary failure — retry
Timeout (30s)Retry
tip

Return 200 immediately and process asynchronously. Requests timeout after 30 seconds.

Webhook Signatures

Actions are signed with your webhook secret (Settings → Webhook Secret) or the per-action webhook_secret.

X-CallMeLater-Signature: sha256=5d41402abc4b2a76b9719d911017c592

Verifying signatures

PHP

$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_CALLMELATER_SIGNATURE'];
$expected = 'sha256=' . hash_hmac('sha256', $payload, $webhookSecret);

if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}

Node.js

const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}

Python

import hmac, hashlib

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
info

The Node.js and Laravel SDKs handle signature verification automatically. See Node.js SDK or Laravel SDK.

Timestamp validation

Reject old requests to prevent replay attacks:

const age = Date.now() / 1000 - parseInt(req.headers['x-callmelater-timestamp']);
if (age > 300) return res.status(401).send('Request too old'); // 5 minutes

SSRF Protection

Requests are blocked to:

  • Private IPs (10.x.x.x, 192.168.x.x, 172.16-31.x.x)
  • Loopback (127.x.x.x, ::1, localhost)
  • Link-local (169.254.x.x)
  • Cloud metadata (169.254.169.254)
  • Internal hostnames (*.local, *.internal)

DNS rebinding is also prevented — hostnames are resolved and IPs validated before requests.

IP Allowlisting

All outbound HTTP calls originate from:

203.0.113.50

You can also fetch this from https://callmelater.io/api/public/server-info.

Firewall examples

AWS Security Group:

Type: Custom TCP | Port: 443 | Source: 203.0.113.50/32

nginx:

location /webhook {
allow 203.0.113.50;
deny all;
}

Data Security

  • Encryption at rest: AES-256
  • Encryption in transit: TLS 1.2+
  • HTTPS only: HTTP requests are rejected

Avoid including passwords, API keys, or PII in action payloads. Use references/IDs instead.

Reporting Issues

Email security@callmelater.io. We respond within 24 hours.