Skip to main content

Webhook Signature Verification

Verification Workflow

  1. 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.
  2. Read headers – extract X-Webhook-Signature (lowercase hex digest, omitted when no secret is configured) and X-Webhook-Timestamp (epoch milliseconds). Keep the string value; do not coerce or trim.
  3. 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.
  4. Compare using a constant-time check – reject the request if the computed value differs from the header. Avoid ==/equals comparisons to prevent timing analysis.
  5. 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”.
  6. 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

Eventdata fieldsNotes
analysis.completedid (analysis UUID), type = "analysis", status = "COMPLETED"Call the Analysis API to retrieve the full result payload.
analysis.failedanalysisId (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.completedid, type = "batch", status = "COMPLETED", totalCount, completedCount, failedCount, completedAtEmitted when every item succeeds.
batch.partialSame fields as batch.completed; status is "FAILED".At least one item failed while others succeeded.
batch.failedSame fields as batch.completed; status is "FAILED".All items failed.
batch.cancelledSame fields as batch.completed; status is "CANCELLED".Caused by explicit cancellation.

Headers

HeaderDescription
X-Webhook-EventEvent type (e.g., analysis.completed)
X-Webhook-TimestampEpoch milliseconds for replay protection
X-Webhook-SignatureLowercase hex digest of HMAC_SHA256(secret, body); omitted when no secret is configured

Events

  • analysis.completed - Analysis finished successfully
  • analysis.failed - Analysis failed (error details in payload)
  • batch.completed - Batch processing completed with no failures
  • batch.partial - Batch finished with a mix of successes and failures
  • batch.failed - Batch processing failed for every item
  • batch.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 + event as key
  • Async processing - Return 200 immediately, process asynchronously
  • Secret hygiene - Choose an alphanumeric secret at least 32 characters long (64 recommended) and rotate periodically.