Webhooks
RGL8R delivers webhook callbacks when asynchronous jobs complete or fail. Instead of polling GET /api/jobs/:id, configure a webhook URL on the enqueue request and receive an HTTP POST when the job reaches a terminal state.
Events
| Event | Description |
|---|---|
job.completed | Fired when a job finishes successfully (status = COMPLETED) |
job.failed | Fired when a job fails permanently (status = FAILED) |
Configuration
Add webhook fields to any enqueue request body (e.g., POST /api/ship/upload, POST /api/sima/batch).
Top-level format
{
"webhookUrl": "https://your-app.com/webhooks/rgl8r",
"webhookSecret": "whsec_your_shared_secret",
"webhookEvents": ["job.completed", "job.failed"]
}Nested object format
{
"webhook": {
"url": "https://your-app.com/webhooks/rgl8r",
"secret": "whsec_your_shared_secret",
"events": ["job.completed"]
}
}Field reference
| Field | Required | Description |
|---|---|---|
webhookUrl / webhook.url | Yes | HTTP or HTTPS endpoint that receives POST callbacks. HTTPS is required when a secret is configured. |
webhookSecret / webhook.secret | No | Shared secret for HMAC signature verification (max 512 characters). HTTPS is required when a secret is configured. |
webhookEvents / webhook.events | No | Array of event names to subscribe to. Defaults to all events (["job.completed", "job.failed"]). |
Payload
Every webhook delivery sends a JSON POST with this shape:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"event": "job.completed",
"createdAt": "2026-03-10T14:30:00.000Z",
"job": {
"id": "f0e1d2c3-b4a5-6789-0abc-def123456789",
"type": "ship_upload",
"status": "COMPLETED",
"progress": 100,
"createdAt": "2026-03-10T14:28:00.000Z",
"updatedAt": "2026-03-10T14:30:00.000Z",
"result": { "shipmentCount": 42, "findingCount": 7 },
"error": null
}
}| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique delivery ID for deduplication |
event | string | Event name (job.completed or job.failed) |
createdAt | string (ISO 8601) | Timestamp when the event was created |
job.id | string (UUID) | The job identifier |
job.type | string | Job type (e.g., sima_validation, ship_upload) |
job.status | string | Terminal status: COMPLETED or FAILED |
job.progress | number | Progress percentage (100 for completed jobs) |
job.result | object or null | Job-specific result payload (varies by job type) |
job.error | string or null | Error message (populated for job.failed events) |
HTTP headers
Every webhook delivery includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-RGL8R-Event | Event name (e.g., job.completed) |
X-RGL8R-Timestamp | Unix timestamp of the delivery |
X-RGL8R-Delivery-ID | Unique delivery ID (matches payload id) |
X-RGL8R-Attempt | Attempt number (1-based: 1 for initial delivery, 2+ for retries) |
X-RGL8R-Signature | HMAC-SHA256 signature with version prefix, format: v1=<hex_digest> (only when a secret is configured) |
HMAC signature verification
When you configure a webhookSecret, every delivery includes X-RGL8R-Signature and X-RGL8R-Timestamp headers. Verify the signature to confirm the request came from RGL8R and was not tampered with.
How the signature is computed:
message = "{timestamp}.{raw_json_body}"
hex_digest = HMAC-SHA256(secret, message)
signature = "v1=" + hex_digestThe X-RGL8R-Signature header value has the format v1=<hex_digest>, where <hex_digest> is the hex-encoded HMAC-SHA256 of the timestamp, a dot separator, and the raw JSON request body. The v1 prefix identifies the signature scheme version.
Verification steps
- Read the
X-RGL8R-TimestampandX-RGL8R-Signatureheaders. - Parse the signature header: verify it starts with
v1=and extract the hex digest after the prefix. - Concatenate the timestamp, a literal
., and the raw request body. - Compute HMAC-SHA256 using your webhook secret.
- Compare the computed hex digest to the extracted signature using a constant-time comparison.
- Optionally reject requests where the timestamp is more than 5 minutes old to prevent replay attacks.
Node.js example
import { createHmac, timingSafeEqual } from 'node:crypto'
function verifyWebhook(req, secret) {
const signatureHeader = req.headers['x-rgl8r-signature']
const timestamp = req.headers['x-rgl8r-timestamp']
const body = req.rawBody // raw request body as string
if (!signatureHeader || !timestamp) {
return false
}
// Parse version prefix
if (!signatureHeader.startsWith('v1=')) {
return false
}
const signature = signatureHeader.slice(3)
// Reject old timestamps (5-minute tolerance)
const age = Math.abs(Date.now() / 1000 - Number(timestamp))
if (age > 300) {
return false
}
const message = `${timestamp}.${body}`
const expected = createHmac('sha256', secret).update(message).digest('hex')
// Constant-time comparison
const a = Buffer.from(signature, 'hex')
const b = Buffer.from(expected, 'hex')
if (a.length !== b.length) {
return false
}
return timingSafeEqual(a, b)
}Python example
import hashlib
import hmac
import time
def verify_webhook(headers: dict, body: str, secret: str) -> bool:
signature_header = headers.get("x-rgl8r-signature", "")
timestamp = headers.get("x-rgl8r-timestamp", "")
if not signature_header or not timestamp:
return False
# Parse version prefix
if not signature_header.startswith("v1="):
return False
signature = signature_header[3:]
# Reject old timestamps (5-minute tolerance)
age = abs(time.time() - float(timestamp))
if age > 300:
return False
message = f"{timestamp}.{body}"
expected = hmac.new(
secret.encode(), message.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Retry behavior
If your endpoint does not return a 2xx status code, RGL8R retries the delivery with exponential backoff:
| Attempt | Delay after previous attempt |
|---|---|
| 1 (initial) | Immediate |
| 2 (retry 1) | 30 seconds |
| 3 (retry 2) | 2 minutes |
| 4 (retry 3) | 10 minutes |
After 4 total attempts (1 initial + 3 retries), the delivery is marked as failed. There is no manual retry mechanism; re-enqueue the job with webhook configuration to receive a new delivery.
What counts as success: Any HTTP 2xx response. The response body is ignored.
Request timeout: Each delivery attempt has a 10-second timeout. If your endpoint does not respond within 10 seconds, the attempt is treated as a failure and retried.
Security
SSRF protection
RGL8R validates webhook URLs to prevent Server-Side Request Forgery (SSRF). The following targets are blocked:
- Private IP ranges:
127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 - Link-local addresses:
169.254.0.0/16 - Localhost hostnames:
localhost,::1
DNS pinning
To prevent DNS rebinding attacks, RGL8R resolves the webhook hostname before making the request and pins the resolved IP address for the duration of the delivery. The resolved IP is also checked against the SSRF blocklist.
HTTPS requirement
When a webhookSecret is configured, the webhookUrl must use HTTPS. Webhook URLs without a secret may use HTTP (useful for development), but HTTPS is strongly recommended for production.
Encryption at rest
Webhook secrets are encrypted at rest using AES-256-GCM. They are decrypted only at delivery time.
Best practices
- Always verify signatures in production. Use the
webhookSecretfield and validate theX-RGL8R-Signatureheader. - Use the delivery
idfor deduplication. Retries send the same delivery ID, so your handler can detect and skip duplicates. - Respond quickly. Return a
200within a few seconds. Offload any heavy processing to a background queue. - Check the timestamp. Reject deliveries older than 5 minutes to prevent replay attacks.
- Use HTTPS. Always use HTTPS endpoints in production to protect the payload in transit.
Last updated: 2026-03-10