Skip to Content
APIWebhooks

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

EventDescription
job.completedFired when a job finishes successfully (status = COMPLETED)
job.failedFired 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

FieldRequiredDescription
webhookUrl / webhook.urlYesHTTP or HTTPS endpoint that receives POST callbacks. HTTPS is required when a secret is configured.
webhookSecret / webhook.secretNoShared secret for HMAC signature verification (max 512 characters). HTTPS is required when a secret is configured.
webhookEvents / webhook.eventsNoArray 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 } }
FieldTypeDescription
idstring (UUID)Unique delivery ID for deduplication
eventstringEvent name (job.completed or job.failed)
createdAtstring (ISO 8601)Timestamp when the event was created
job.idstring (UUID)The job identifier
job.typestringJob type (e.g., sima_validation, ship_upload)
job.statusstringTerminal status: COMPLETED or FAILED
job.progressnumberProgress percentage (100 for completed jobs)
job.resultobject or nullJob-specific result payload (varies by job type)
job.errorstring or nullError message (populated for job.failed events)

HTTP headers

Every webhook delivery includes these headers:

HeaderDescription
Content-Typeapplication/json
X-RGL8R-EventEvent name (e.g., job.completed)
X-RGL8R-TimestampUnix timestamp of the delivery
X-RGL8R-Delivery-IDUnique delivery ID (matches payload id)
X-RGL8R-AttemptAttempt number (1-based: 1 for initial delivery, 2+ for retries)
X-RGL8R-SignatureHMAC-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_digest

The 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

  1. Read the X-RGL8R-Timestamp and X-RGL8R-Signature headers.
  2. Parse the signature header: verify it starts with v1= and extract the hex digest after the prefix.
  3. Concatenate the timestamp, a literal ., and the raw request body.
  4. Compute HMAC-SHA256 using your webhook secret.
  5. Compare the computed hex digest to the extracted signature using a constant-time comparison.
  6. 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:

AttemptDelay 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 webhookSecret field and validate the X-RGL8R-Signature header.
  • Use the delivery id for deduplication. Retries send the same delivery ID, so your handler can detect and skip duplicates.
  • Respond quickly. Return a 200 within 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