Skip to content

Verify signed HTTP deliveries

Every attempt carries a project-scoped Ed25519 token in Stormedo-Signature and a stable ID in Stormedo-Request-Id. Use the SDK’s complete verification method before processing a body. For an endpoint you control, start with Receive deliveries and its runnable Express and FastAPI examples.

Create a verifier with STORMEDO_PROJECT_ID. Obtain that ID from data.project_id in a successful send response using your project key. The verifier retrieves the project’s public signing keys; it does not need an API key.

Set the expected destination URL in trusted application configuration. It must match the public URL submitted to Stormedo, including path, query, and trailing slash. A proxy’s internal hostname is not the audience. Never select the expected project, issuer, URL, or key location from unverified token claims.

These snippets show the verifier call inside a handler. signature and requestId / request_id are single, validated header values; rawBody / raw_body is the original byte buffer captured before a parser. The complete framework examples show how to obtain each value and return a response.

TypeScript
import { DeliveryVerifier } from "@stormedo/sdk";
const projectId = process.env.STORMEDO_PROJECT_ID;
if (!projectId) throw new Error("Set STORMEDO_PROJECT_ID");
const verifier = new DeliveryVerifier(projectId);
const delivery = await verifier.verifyDelivery({
signature,
requestId,
expectedUrl: "https://your-app.example/webhooks/stormedo",
method: "POST",
rawBody,
});
// delivery.payloadIntegrity is "verified".
Python
import os
from stormedo import DeliveryVerifier
verifier = DeliveryVerifier(os.environ["STORMEDO_PROJECT_ID"])
delivery = verifier.verify_delivery(
signature=signature,
request_id=request_id,
expected_url="https://your-app.example/webhooks/stormedo",
method="POST",
raw_body=raw_body,
)
# delivery.payload_integrity is "verified".

In async Python handlers, reuse AsyncDeliveryVerifier and await verifier.verify_delivery(...). JavaScript accepts Uint8Array, including Node Buffer. Python accepts bytes, bytearray, or memoryview.

Require exactly one of each signing header before calling the SDK. Some frameworks merge duplicate headers, so inspect the original header list. Limit the body to 256 KiB. Do not decode, re-serialize, or decompress it before verification.

A failed check throws DeliveryVerificationError. Do not catch it and continue processing. Return 401 for a rejected signature or binding check. A temporary key-fetch failure with code: "jwks_unavailable" should return 503 so configured retries can recover. Missing or duplicated headers can return 400.

Unexpected processing failures must also leave the delivery unacknowledged. Return success only after the business change, or your own durable handoff, commits. See verification troubleshooting for error codes and remedies.

HTTP
GET https://api.stormedo.com/projects/{project_id}/delivery-signing-keys/jwks.json

This endpoint needs no authentication and returns a standard JWKS object, outside the API response envelope. It publishes current verification keys and, during rotation, a previous key. It returns Cache-Control: public, max-age=60, must-revalidate and an ETag; a matching If-None-Match returns 304.

The SDK caches keys by trusted JWKS URL inside each process. Python async caches are per event loop. An unknown key ID can trigger a refresh with a 30-second cooldown. Reuse the verifier, permit outbound HTTPS to the key endpoint, and fail verification if fresh keys cannot be obtained. The cache is not permanent storage or a reason to skip rotation checks.

authenticateDelivery / authenticate_delivery checks the token and request context but deliberately skips the body digest. Its result reports payloadIntegrity: "unchecked" / payload_integrity == "unchecked". Use it only if your integration does not rely on the body, or protects that body independently. It is not a substitute for raw-body capture in a webhook that processes payload data.

Call the complete verifier once when the body is available; there is no need to authenticate separately first.

After verification, commit the validated request ID and the business effect in one transaction. Retries carry the same request ID; manual replays carry a new one. Use a payload event ID as well when business idempotency must survive replay. See Idempotency and the signing protocol reference.