An enumeration attack is simple to describe and tedious to defend against: an attacker doesn't try to break into any one account, they try to discover which accounts exist at all, usually by guessing identifiers and watching how the system responds. Against an email provider, a successful enumeration pass hands you a list of live addresses worth targeting with phishing, spam, or credential-stuffing attempts. DimeBox's inbox model closes off most of the obvious ways to run that scan.
The free provisioning route, POST /api/inboxes, hands back a random adjective-noun-number slug — something like bold-ember-564 — not an incrementing ID. That matters more than it looks: a sequential identifier (inbox #10482, #10483, #10484...) turns "does this inbox exist" into a one-line loop. A random slug drawn from a large combination of words and numbers doesn't. Guessing a valid, in-use slug by brute force means searching a space large enough that it isn't a practical attack, rather than counting upward.
If you want a specific, memorable address instead of a random one, that's what claiming a custom inbox name is for — a different endpoint, a different price, and a different set of rules, covered in its own post.
Neither an inbox nor a message response ever includes a raw database row number. The only identifiers a caller ever sees are the slug (for inboxes) and a message_id — a UUID, not a row number — for individual messages. This closes the other classic enumeration vector: if an API ever returns an incrementing primary key anywhere in a response, that number itself becomes a map of how many records exist and roughly which ones are recent. DimeBox's responses simply never contain that number, for inboxes or messages.
There's no endpoint that lists inboxes, and no endpoint that searches messages across inboxes. Reading a mailbox means calling GET /api/inboxes/{slug}/messages with the X-Inbox-Token issued for that specific inbox — see read tokens explained for what that credential actually is and isn't. A valid token for one inbox doesn't help you probe another: a message_id that genuinely exists, but belongs to a different inbox than the one in the URL, returns a plain 404, even though the ID itself is real and the token presented is valid for some inbox. The lookup is scoped to the inbox named in the path, not to "any message this token's holder might know about."
That scoping matters because it means knowing one valid identifier never helps you learn about a second one. An attacker who somehow obtains a real message_id (from a log, a bug, a leak) can't use it to fish for other inboxes' data — it only resolves inside the one inbox it was ever issued for.
The paid claim route, POST /api/inboxes/claim, checks whether a requested slug is available before settling payment, so an unavailable name fails with a 422 for free rather than charging you to find out. That check necessarily tells you whether a specific name you guessed is taken — there's no way to let you claim your own choice of name without confirming availability somehow. What it doesn't do is help you enumerate names you haven't already guessed; the response to a single guess tells you about that one slug, not a list of what else exists. Reserved, system-looking names are rejected the same way regardless of whether they happen to be in use — see reserved inbox names for the full list of patterns DimeBox blocks outright.
None of these are exotic protections — random high-entropy identifiers, no internal row numbers in responses, and lookups scoped strictly to the identifier in the request are standard practice for any API that doesn't want to be trivially crawlable. What's specific to DimeBox is that there's no account layer sitting on top to add a second line of defense: no login to lock out after failed attempts, no email verification step to slow a script down. The identifiers themselves have to carry the whole weight, which is why the slug format, the UUID message IDs, and the strict per-inbox scoping aren't incidental implementation details — they're the actual security boundary.
A UUID used for message_id draws from a keyspace large enough that guessing a valid one by brute force isn't a realistic strategy on its own — the practical attack would have to start from a UUID an attacker already obtained some other way, which is exactly why the per-inbox scoping on lookups matters as much as the format itself. Format and scoping are doing two different jobs: the format makes a blind guess computationally pointless; the scoping makes even a correctly guessed or leaked ID useless outside the one inbox it belongs to. Either one alone would leave a gap the other closes.
The same logic applies to the free provisioning route's slug generator. A word-plus-number pattern is memorable and readable precisely because it's meant to be typed into an email client by a human corresponding with the agent behind it — but readability and brute-force resistance aren't actually in tension here, because the search space across adjectives, nouns, and a trailing number is still large enough that sequential guessing isn't viable, even though a sequential row number would make the identical goal trivial.
If you lose the one credential that does exist, see what happens when you lose a read token for what's recoverable and what isn't.