Customer buckets for your SaaS
Give each customer of your application a private bucket: provision it, process arrivals, estimate capacity and recover.
Give each customer a private mailbox. Your backend creates buckets, reads messages
and downloads attachments. Customers need no separate Revdoku signup.
Requests go to https://api.revdoku.com. Every path starts with /v1, for
example https://api.revdoku.com/v1/buckets.
Start with the examples
- Select your development account. In Account → Access, create an API key
with All current and future buckets and Manage buckets and files
(bucket_admin) for provisioning. The setup steps after signup create your first bucket. - Clone the repository. In
examples, copy
.env.exampleto.env. With Node.js 22+, set
REVDOKU_API_KEYandREVDOKU_ACCOUNT_IDlocally. Keep keys off browsers and Git. - Run these commands with customer IDs from a trusted application job. They
consume creation capacity.
The examples use native fetch and a private persistent journal in
.revdoku-examples (REVDOKU_STATE_DIR overrides this). In your application,
you can use the SDK instead (npm install revdoku or pip install revdoku); see
the quickstart. Run one process at a time;
verify its process stopped before removing a leftover lock.
Send test mail to a confirmed address. The processor saves summaries and arrival
cursors for mapped buckets. Rerun it periodically and after restarts. External
side effects need their own deduplication.
Map customers and choose access
Store customer_id, account_id, bucket_id, confirmed address, provisioning
state and cursor. Authenticate every customer request and resolve its mapping
server-side; reject another customer’s bucket ID.
Use a separate Revdoku account when ownership, membership, administration or the
account encryption boundary must differ. Eligible managed accounts share
billing-group capacity while retaining separate files and memberships.
Browser signup creates a new owner account.
| Task | Credential needed |
|---|---|
| List/read messages, download attachments | bucket_read with the mapped mailbox selected |
| Discover an address or receiving state | Mailbox write access; store the address during provisioning for readers |
| Create a bucket | Account-wide bucket_admin access and an eligible account membership |
| Configure a webhook or delete a message | Mailbox admin access |
| Manage aliases and custom email domains | An account owner or administrator, in the dashboard. The API has no endpoints for them. |
Use separate provisioning and reading credentials. Selected-mailbox access does
not permit creating buckets. A normal API key consumes max_api_keys; an
agent-labelled OAuth/device connection consumes
max_agent_connections, even when its bearer token calls REST. Human memberships
have their own allowance.
Check GET /v1/accounts and GET /v1/accounts/:account_id. Include the granted account_id
on every request. Browser account switching does not change a key’s default.
Namespace mappings, cursors and work records by account and mailbox.
Provision once and recover
The minimal creation request sends {"bucket":{"email":{}}}.
For a connected custom domain, set REVDOKU_EMAIL_DOMAIN before provisioning.
| Outcome | Next action |
|---|---|
201 |
Persist the bucket ID and confirmed receiving address. |
503 EMAIL_NOT_READY |
Persist error.details.bucket_id; inspect its receiving state (bucket.email) with GET /v1/buckets/:id?account_id=…. |
| Timeout, broken connection or unreadable response | Retain an unknown outcome; reconcile existing buckets. Do not repeat the POST. |
| Capacity/permission error | Resolve the returned condition before an explicitly chosen new attempt. |
The example saves intent and a unique email username before sending. After an
unknown result, rerunning searches that username and requires one bucket whose
title or address name equals it; it never repeats the POST. On revdokumail.com
the address name is the username, with dots and underscores as hyphens, and the default title (the username as sent)
identifies the bucket too. The username is not a retry key. Missing
or multiple matches need human review; an empty list does not prove creation
failed. A known bucket ID is checked with GET. Structured 4xx rejection permits a
later explicit new attempt. Use only confirmed addresses.
Aliases share a bucket’s messages, restrictions and quotas; they provide no
isolation. Add an alias in the dashboard (Email Settings of the bucket). The API has no
endpoints for aliases or address changes: see custom email domains.
Estimate capacity
Read GET /v1/account/limits?account_id=…; use effective values rather than copied
plan numbers. Full-account admin connections and browser sessions also receive
data.usage.bucket_creations, including remaining creations and the UTC reset.
| Dimension | Scope and timing |
|---|---|
max_buckets |
Active buckets across the billing group |
| Creation allowance | Billing group, UTC calendar month; deleting/archiving refunds nothing |
| Incoming count and raw bytes | Billing period; MIME encoding and matched rejected attempts can consume traffic |
| Stored bytes/current files | Retained content, including EML, JSON, Markdown, attachments and versions |
| Per-file/message limits | Each upload or incoming message; PDFs may have a separate cap |
| Domains/aliases | Domain slots per account; aliases per mailbox |
| Request/arrival throttles | API request limits and separate fixed-minute receiving limits |
Divide the incoming byte allowance by your average raw message size, and compare the
result with the count allowance: the lower value is reached first.
Measure MIME and saved artifact sizes; there is no fixed attachment multiplier.
Allow headroom for spikes and versions. The server decides capacity under concurrency.
Stop on BUCKET_CREATION_LIMIT_REACHED and report resets_at.
For larger workloads, send support your bucket count, creation rate,
monthly messages/raw bytes, stored bytes, peak arrivals, domains, webhook load
and support needs. Confirm allowances and pricing before promising capacity.
Process arrivals reliably
| State | Meaning |
|---|---|
| Shared read/unread | Presentation state; humans can change it |
| Arrival cursor | Which arrivals your reader has discovered |
| Application work record | Which business work completed |
Do not use unread filters as a job queue. Deduplicate business work by
account_id/bucket_id/email_id, and webhook delivery by event ID. Separate mailbox
deliveries must not collapse solely because they share a sender Message-ID.
For webhooks, verify the signature/timestamp and mapped account/mailbox, durably
accept work, then acknowledge. Workers fetch content afterward. Multiple workers
need unique work keys and claim leases. External side effects need idempotency
keys to survive a crash before completion is recorded.
Catch up with ascending cursors on startup and periodically, even with webhooks.
Save cursors after durably handling each page, including empty pages. Read-state
changes do not replay arrivals. See the
notification examples for signed
receivers and local WebSocket reconnects.
Display messages and attachments
Escape body_text as ordinary text and preserve whitespace. HTML-only mail is
converted to text; it is not a faithful HTML renderer. Handle body_status values
complete, empty, truncated and unavailable, offering the authoritative
original EML when needed. Saved attachment metadata and optional
omitted_attachment_count describe availability; older messages may lack the latter.
Authorize the customer mapping, store attachment IDs, and request links on demand.
Standard links last 15 minutes and can survive key revocation until expiry.
Never send the API key to a download URL. Use
download-attachments separately;
individual message/file deletion is an explicit action, never processing acknowledgement.
When receiving stops
A rolling hour reaching 30% of either effective monthly traffic allowance
automatically pauses that bucket. The owner receives email and an in-app alert.
An owner/administrator must resume in the browser after cooldown and other holds
clear. Pause/Resume and detailed diagnostic logs are dashboard-only. Mail rejected
while paused must be resent; there is no replay queue.
Mailbox-wide locks and moves can also stop intake, including queued saves. Prefer
file locks for narrow edits or a separate working mailbox for broad reorganizations.
Check: granted account → receiving state → limits → saved messages → dashboard
webhook history → application work. Webhook failure does not imply storage failure.
Give an authorized human account/mailbox
IDs, UTC interval, known email/event/request IDs, blocked reason and completed
checks. Use Analytics and the source mailbox;
history is retention-limited. Report “not found in accessible saved messages”
without guessing why an external sender’s mail is missing. Exclude bodies,
credentials and signed URLs from the handoff.
Development and CI
Reuse development buckets. Run npm ci, npm run build, and npm run check in
examples for offline fixtures. Cover duplicates, bad signatures, empty pages,
restarts, uncertain creation and cross-customer denial. For live smoke tests,
externally send/read mail, download an attachment, then restart/catch up. Delete
only identified test messages/files individually.
HIPAA and high-security accounts
Content-search filters may be unavailable; authorized reads still work. Signed API
download URLs check access and decrypt content. Fetch them without an API key.
See account setup and download differences.