Email events

Hosted applications receive signed HTTP webhooks. Local projects open an outbound WebSocket through Rails Action Cable; no public domain or tunnel is required. Both deliver the same event after the complete email and attachments are saved. Read contents through the existing email API.

EndpointPermissionResult
GET /v1/buckets/:bucket_id/mailbox/webhookMailbox adminEndpoint, or webhook: null; excludes the signing secret.
PUT /v1/buckets/:bucket_id/mailbox/webhookMailbox adminSet one URL; returns the endpoint and signing secret.
DELETE /v1/buckets/:bucket_id/mailbox/webhookMailbox adminDisable delivery; 200 with data.deleted and data.id (the bucket ID).
GET /v1/buckets/:bucket_id/mailbox/subscriptionMailbox readSigned WebSocket ticket valid for 60 seconds.

Requests go to https://api.revdoku.com. Every path starts with /v1, for example https://api.revdoku.com/v1/buckets. Use your normal bearer API key. Select another granted account with account_id in the query for GET/DELETE, or in the JSON body for PUT. Endpoints must use public HTTPS, including explicit ports such as 8443, with no URL user/password or fragment. Local/private addresses and redirects are rejected. Invalid configuration returns INVALID_ARGUMENT (422).

PUT fieldMeaning
webhook_urlRequired public HTTPS receiver URL, up to 2048 bytes.
rotate_secretOptional boolean, default false. Replace the secret even when the URL is unchanged; pending deliveries are canceled.
account_idOptional explicitly granted account; defaults to the credential’s account.

Hosted application workflow

  1. Implement an HTTPS POST receiver.
  2. PUT {“webhook_url”:“https://example.org/email-events”} to configure it. Securely save data.webhook.signing_secret. Repeating PUT with the same URL retains the secret and does not return it again; replacing the URL rotates it and cancels pending old-endpoint deliveries.
  3. Verify the exact request bytes before parsing JSON.
  4. Deduplicate the event ID, durably accept the event, and return any 2xx.
  5. Fetch /v1/buckets/:bucket_id/mailbox/emails/:email_id with your own API key.
{
  "id": "email.received:eml_example",
  "type": "email.received",
  "created_at": "2026-10-01T12:00:00.000000Z",
  "data": {
    "account_id": "acct_example",
    "bucket_id": "bkt_example",
    "email_id": "eml_example",
    "received_at": "2026-10-01T11:59:58.000000Z",
    "attachment_count": 1
  }
}
FieldMeaning
idStable event ID for deduplication; unchanged on retries.
typeemail.received.
created_atUTC time when intake queued the event.
data.account_id, data.bucket_idAccount and mailbox that saved the message.
data.email_idStable email ID for the existing read endpoint.
data.received_atUTC email receipt time.
data.attachment_countNumber of saved attachments.

Events contain no subjects, senders, bodies, download links or API credentials. Only new accepted deliveries emit them. Edits, copies, backfills and read changes do not. Configuring a webhook does not replay history. Copies and account moves clear the webhook; configure the destination explicitly.

HeaderMeaning
X-Revdoku-Event-IdEvent id.
X-Revdoku-TimestampUnix seconds for this delivery attempt.
X-Revdoku-Signaturev1= followed by lowercase HMAC-SHA256 hex.

Sign timestamp + ”.” + raw_body. Reject timestamps outside a five-minute window and compare signatures in constant time. A Rails receiver can use:

timestamp = request.headers["X-Revdoku-Timestamp"].to_s
body = request.raw_post
provided = request.headers["X-Revdoku-Signature"].to_s.delete_prefix("v1=")
fresh = timestamp.match?(/\A\d+\z/) && (Time.current.to_i - timestamp.to_i).abs <= 300
expected = OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("REVDOKU_WEBHOOK_SECRET"), "#{timestamp}.#{body}")
return head :unauthorized unless fresh && ActiveSupport::SecurityUtils.secure_compare(expected, provided)
event = JSON.parse(body)
# Persist/deduplicate event["id"] and queue application work before returning 2xx.
head :no_content

Use your application’s normal webhook CSRF exemption. Verify signatures before accepting requests.

Delivery ruleBehavior
Automatic attemptsUp to 8 total for network failures, HTTP 408, 429 and 5xx
Retry delayPolynomial backoff; valid Retry-After on 429/503 is bounded to 1–3,600 seconds
Other failuresOther non-2xx responses, redirects, private destinations, or responses over 64 KiB stop delivery
Timeout15 seconds total per request; acknowledge promptly after durable acceptance
Disable / replace / rotateCancels pending attempts; an in-flight request may finish
Retry identitySame event ID, fresh timestamp and signature; no additional incoming-email charge

Delivery can repeat or arrive out of order; deduplicate by event ID.

Plans and delivery history

AllowanceEffective value
Buckets with a webhookExisting active-mailbox capacity; read limits.max_buckets.
Endpoints per bucketOne.
New email eventsFollow accepted messages within incoming count, byte and storage limits.
Delivery historyRead limits.audit_retention_days.

Webhook capacity follows the account’s existing mailbox and incoming-email limits, including overrides and shared billing. There is no separate webhook event allowance.

Delivery history is available in Analytics → Webhooks.

Local project workflow

  1. GET a subscription ticket with your read-authorized API key.
  2. Open the returned websocket_url, adding email_subscription_token=TICKET within 60 seconds, using the actioncable-v1-json subprotocol.
  3. Subscribe to EmailReceivedChannel with the returned account_id and bucket_id. Wait for confirm_subscription.
  4. List messages with your saved ascending arrival cursor and process every page. Deduplicate email IDs against live events received during catch-up.
  5. Handle each live event’s message and fetch its email through REST.
  6. On a temporary disconnect, get a fresh ticket, reconnect, and repeat cursor catch-up. Stop if the server rejects the subscription or sends reconnect: false.

Runnable JavaScript/TypeScript and Python examples cover signed receivers and reconnects. The Node watch-mail.js example waits for subscription confirmation, catches up with a saved cursor, detects stale connections, and requests a fresh ticket on reconnect.

Client guardLimit
Duplicate mailbox subscriptionsOne per connection
Mailbox subscriptions32 per connection; tickets authorize one bucket
Connections32 per credential per web process
Handshakes120 per minute per source IP
Client commands120 per minute per connection; 4 KiB maximum per command

Keep account, mailbox and filters unchanged when reusing a cursor. Preserve pagination.next_cursor even after an empty page. WebSocket events are live hints; REST catch-up supplies messages received while disconnected.

Ticket expiry limits connection establishment; an established subscription lasts until disconnect or access revocation. Credentials and membership are checked before each transmitted event. Treat the ticket as a temporary credential: it authorizes only its selected bucket channel, never account notification streams.

API guide · API reference

Markdown version
↧
Loading PDF…