Webhook Signature Verification
Verification Workflow
- Capture the raw payload – disable automatic JSON parsing for the webhook route so you can hash the byte-for-byte payload that was signed by MIRI.
- Read headers – extract
X-Webhook-Signature(lowercase hex digest, omitted when no secret is configured) andX-Webhook-Timestamp(epoch milliseconds). Keep the string value; do not coerce or trim. - Derive the expected signature – compute
HMAC_SHA256(secret, rawBody)with the shared secret and hex-encode the digest (lowercase). Compare the digest directly to the header value—no prefix is added by the platform. - Compare using a constant-time check – reject the request if the computed value differs from the header. Avoid
==/equalscomparisons to prevent timing analysis. - Validate freshness – ensure the timestamp is within a five minute window (or stricter) to prevent replay attacks. The JSON envelope also contains a
timestamp(epoch seconds); both should represent “now”. - Process the event – once verified, acknowledge with HTTP 200 immediately and hand the payload to your async job queue. MIRI retries up to three times with a fixed two second gap, so idempotent handling is required.
Event Structure
{
"event": "analysis.completed",
"timestamp": 1704445800, // epoch seconds (JSON body); header X-Webhook-Timestamp uses epoch milliseconds
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "analysis",
"status": "COMPLETED"
}
}
Every delivery follows this envelope. The JSON timestamp is emitted in seconds, while the X-Webhook-Timestamp header uses milliseconds. Use both values when enforcing freshness.
Event payloads
| Event | data fields | Notes |
|---|---|---|
analysis.completed | id (analysis UUID), type = "analysis", status = "COMPLETED" | Call the Analysis API to retrieve the full result payload. |
analysis.failed | analysisId (analysis UUID) and error message. Some failure paths also include type, status, and id; always treat analysisId/id as the same identifier. | Triggered when processing errors occur or a timeout cancels the job. |
batch.completed | id, type = "batch", status = "COMPLETED", totalCount, completedCount, failedCount, completedAt | Emitted when every item succeeds. |
batch.partial | Same fields as batch.completed; status is "FAILED". | At least one item failed while others succeeded. |
batch.failed | Same fields as batch.completed; status is "FAILED". | All items failed. |
batch.cancelled | Same fields as batch.completed; status is "CANCELLED". | Caused by explicit cancellation. |
Headers
| Header | Description |
|---|---|
X-Webhook-Event | Event type (e.g., analysis.completed) |
X-Webhook-Timestamp | Epoch milliseconds for replay protection |
X-Webhook-Signature | Lowercase hex digest of HMAC_SHA256(secret, body); omitted when no secret is configured |
Events
analysis.completed- Analysis finished successfullyanalysis.failed- Analysis failed (error details in payload)batch.completed- Batch processing completed with no failuresbatch.partial- Batch finished with a mix of successes and failuresbatch.failed- Batch processing failed for every itembatch.cancelled- Batch processing was cancelled before completion
Delivery Policy
Each event is attempted up to three times with a fixed two-second gap (configured in application.yml). If all attempts fail (non-2xx response or network error), the platform marks the delivery as failed. Update the destination and replay the analysis or batch to trigger a new notification.
Production Requirements
- Use HTTPS - The platform does not enforce HTTPS, but using TLS protects payloads and secrets
- Response time - Must respond within 10 seconds
- Idempotency - Handle duplicate events using
analysisId + eventas key - Async processing - Return 200 immediately, process asynchronously
- Secret hygiene - Choose an alphanumeric secret at least 32 characters long (64 recommended) and rotate periodically.