✉️Email hooks
Configure an HTTPS receiver on your Cronly email hook. Cronly parses incoming mail, stores the event, and POSTs it to your receiver. JSON is the default; multipart form-data is an optional encoding of the same data.
This guide describes version 1 of the email webhook contract. For cron job and certificate alerts, see Webhooks.
Contract downloads
JSON Schema — canonical event fields and validation
OpenAPI 3.1 — request headers, encodings and responses
Example event — synthetic data
Node.js receiver helper — signature verification and parsing for both formats
Configure your receiver
Create an HTTPS POST endpoint on port 443, reachable over public IPv4, with a trusted TLS certificate. Redirects are not followed.
Save that URL in Cronly. Select JSON unless your receiver requires multipart.
Copy the hook’s signing secret into your receiver’s private configuration. Keep it out of browser code and logs.
Verify the signature against the raw request bytes, parse the selected format, then validate using the linked schema.
Deduplicate by
event_idand durably store or queue the event before returning a 2xx response. Run application work asynchronously.Use Send test event on the hook and inspect the delivery result. Tests consume one monthly event and are limited to five requests per minute.
Encoding and fields
JSON (default)
application/json
Parse the body as a JSON object.
Multipart
multipart/form-data; boundary=…
One text part per top-level field. JSON-decode payload and metadata; convert account_id and monitor_id to integers.
The envelope contains event_id, account_id, monitor_id, monitor_type, event_type, received_at, source, status, payload and metadata. Email fields are inside payload.
event_type is email.received for real mail or email.test for synthetic tests. Tests also have metadata.test=true; receivers should avoid triggering real business actions for these events. Outbound status remains received, including retries. Empty metadata is [] for compatibility; otherwise it is an object. This v1 envelope is Cronly-specific, not CloudEvents.
payload.recipient is the SMTP envelope recipient used for routing. from and reply_to are decoded header strings that may include display names. message_id is the email’s Message-ID, separate from Cronly’s event UUID. Optional body/header fields may be missing or null. received_at is Cronly’s acceptance time, not the sender’s Date header.
Attachments are metadata only: name, content_type and size in bytes. Neither format includes file bytes or download URLs. Original attachment files are not stored and cannot be retrieved by retrying. HTML and all sender-provided content are untrusted; sanitize HTML before rendering.
Example JSON event
Verify signatures before parsing
Cronly uses the symmetric signing convention from Standard Webhooks. This authenticates delivery from Cronly; it does not authenticate the email’s claimed sender.
webhook-id: stable event UUIDwebhook-timestamp: Unix seconds for this attemptwebhook-signature:v1,<base64 signature>X-Cronly-Schema-Version:1Idempotency-KeyandX-Cronly-Event-Id: same event UUIDLink: canonical JSON Schema URL withrel="describedby"
Compare signatures in constant time and reject timestamps more than 300 seconds from your clock. Preserve exact raw bytes, including multipart boundaries and line endings. Do not re-serialize parsed JSON before verification. Match the parsed event_id to webhook-id. Support space-separated signatures for future key rotation. The per-hook secret is encrypted at rest and available only to users or tokens with write access.
Delivery, retries and versions
Delivery is at least once; duplicates and out-of-order arrivals are possible. A receiver that already accepted an event should return 2xx again. Cronly allows 3 seconds to connect and 10 seconds total, does not follow redirects, and stops reading responses after 64 KiB. Keep acknowledgements small, ideally 204 with no body.
A non-2xx response or network failure is recorded as failed. Ordinary failures currently require a manual retry, with five total attempts by default. Interrupted worker leases recover automatically. Inspect the event’s delivery history in Cronly. Raw response bodies are not retained; keep useful validation details in your receiver logs without recording secrets or email bodies.
Each hook and queued delivery is pinned to schema version 1. Format changes apply to new events; pending deliveries keep their original format. Manual retries use the current destination and format, retain the original contract version and event UUID, and get a fresh signature timestamp. Treat unknown optional fields as forward-compatible; breaking schema changes require a new version.
Management API
Existing company bearer tokens may manage hooks. Scoped tokens require email-hooks:read or email-hooks:write as appropriate.
POST /api/email-hooks: name, optional destination_url, webhook_format (jsonormultipart; defaultjson), webhook_schema_version (currently only"1").PATCH /api/email-hooks/{id}: name is required; omitted destination_url preserves the destination, explicit null clears it. Format is optional. Only version"1"is supported.GET /api/email-hooks/{id}: format, version, documentation/schema links and paginated events. API event status is the current processing state; outbound status staysreceived.GET /api/email-hooks/{id}/signing-secret: write scope; returns signing_secret withCache-Control: private, no-store.POST /api/email-hooks/{id}/test: write scope; queues a syntheticemail.testevent and returns 202 with event_id. Requires an active hook and destination.POST /api/email-hooks/{id}/events/{event_id}/retry: write scope; retries an eligible failed delivery using the current destination and format.
Last updated