n8n has no Lumbox node, and it does not need one. The whole OTP flow is three calls to the Lumbox REST API, and n8n's built-in HTTP Request node can make all three: create an inbox, wait for the code, delete the inbox. What takes care is the configuration around those calls, because the middle one is a long-poll that holds the connection open for up to two minutes, and n8n's defaults are not set up for that.
This post walks through the workflow node by node, including the timeout setting that silently breaks it and what to do with the HTTP 408 you get when no code arrives.
The shape of the workflow
- Create inbox (HTTP Request, POST). Returns an
idand anaddressontrylumbox.com. - Trigger the signup. Whatever submits the address to the service: an HTTP call to your own app's signup endpoint, or a call to a browser automation service when the form needs a real browser.
- Wait for OTP (HTTP Request, GET). Blocks until a code lands or the timeout passes.
- Use the code. Map
{{ $json.code }}into the next step. - Delete inbox (HTTP Request, DELETE). On the success path and on the error path.
Every execution gets its own address, so two executions running at once can never read each other's codes. That is the property a shared Gmail inbox cannot give you: with one mailbox, two runs that both ask "what is the latest code?" can both get the same answer.
One credential for all three nodes
Lumbox authenticates with an X-API-Key header. In n8n, create a credential of the generic Header Auth type with Name X-API-Key and Value set to your ak_... key, then select it in each HTTP Request node (Authentication: Generic Credential Type, then Header Auth). Keeping the key in a credential rather than a plain header field means it lives in one place instead of being typed into every node.
Node 1: create the inbox
Method: POST
URL: https://api.lumbox.co/v1/inboxes
Send Body: JSON
Body:
{
"metadata": { "n8n_execution": "{{ $execution.id }}" }
}
The response is 201 with id, address, name, status and created_at. Leave name out and Lumbox generates a local part for you. If you do set a name, remember that trylumbox.com is a shared domain: the name becomes the address, a name someone else already holds returns 409, and reserved words like support or admin are refused. Putting the execution ID in metadata gives you the link back to the n8n run without fighting over addresses.
Node 3: wait for the code
Method: GET
URL: https://api.lumbox.co/v1/inboxes/{{ $('Create inbox').item.json.id }}/otp
Query: timeout = 90
from = example.com
Options: Timeout = 100000
The /otp endpoint checks the inbox once a second and returns as soon as a verification email with a code is present:
{
"code": "482915",
"all_codes": ["482915"],
"from": "no-reply@example.com",
"subject": "Your verification code",
"expires": "2026-09-29T10:14:00.000Z",
"email_id": "eml_..."
}
expires is only filled when the email says something like "expires in 10 minutes"; otherwise it is null. from in the query is a substring match on the sender address, so example.com matches no-reply@example.com. Use it whenever you know the sender, since it stops an unrelated email from answering the wait.
The timeout setting that breaks it
Two timeouts are in play and they have to agree. The timeout query parameter tells Lumbox how long to hold the request, in seconds, clamped between 1 and 120. The n8n node has its own client timeout, in milliseconds, covering the wait for response headers. Lumbox sends no headers until it has an answer, so the client timeout has to be longer than the long-poll.
In the n8n source (HttpRequestV3.node.ts), a node with no Timeout option falls back to 300,000 ms, which is fine. The trap is adding the Timeout option: its field pre-fills with 10,000 ms. Add the option without changing the value and every wait aborts after 10 seconds with a timeout error, even though the code was about to arrive. Set it a few seconds above your Lumbox timeout, as in the 100,000 above.
Handling the 408
When the timeout passes with no matching email, Lumbox answers 408 with {"error":"No OTP found","timeout":90}. By default the HTTP Request node treats any non-2xx status as a failure and stops the workflow, which skips your cleanup node and leaves the inbox behind.
The simplest fix is in the node's Settings tab: set On Error to Continue (using error output). The node grows a second output; wire it to the delete node and then to whatever alerts you. The alternative is the Never Error response option plus Include Response Headers and Status, followed by an IF node on statusCode. Both work. The error output keeps the happy path readable.
Retry On Fail is safe to use here, with one thing to know. Unless you pass since, /otp looks back five minutes, so a code that arrived during the gap between attempts is still found on the retry. Each retry is another full wait, though: three tries at 90 seconds is four and a half minutes before the workflow gives up.
When there is no numeric code
/otp is narrow on purpose. It only considers emails the parser categorised as verification, and codes are pulled from the subject and plain-text body as 4 to 8 digit numbers. Three cases fall outside it: services that send a link instead of a code, alphanumeric codes like K7Q-2MX, and login codes whose wording ("new sign-in from a new device") gets the email categorised as security.
For those, use /wait, which returns the whole parsed email:
Method: GET
URL: https://api.lumbox.co/v1/inboxes/{{ $('Create inbox').item.json.id }}/wait
Query: timeout = 90
from = example.com
since = {{ $('Create inbox').item.json.created_at }}
Options: Timeout = 100000
Then read {{ $json.parsed.verification_links[0] }} for a link, {{ $json.parsed.otp_codes[0] }} for a numeric code in any category, or {{ $json.text_body }} if you need to pull something the parser does not know about. Adding has_otp=true makes /wait hold out for an email that contains a code.
The since parameter matters more here than it looks. /wait defaults to "emails received after this request started". In n8n the signup step runs before the wait node, so a fast sender can deliver the email in the gap, and the wait then sits there for 90 seconds looking for mail that already arrived. Passing the inbox's created_at closes that gap and uses the server's clock rather than the n8n host's.
Parallel executions and limits
Per-execution inboxes remove collisions, but two limits still apply. Your plan caps how many inboxes exist at once (3 on Free, 10 on Starter, 50 on Pro, 250 on Scale), and creating one past the cap returns 402 with code: "PLAN_LIMIT_EXCEEDED". Deleting the inbox at the end of each execution frees the slot, which is why the delete node sits on both branches. The API also allows 120 requests per minute per key, and a long-poll counts as one request however long it blocks, so each execution costs three.
If you would rather not hold a connection open, Lumbox can POST an email.received webhook to an n8n Webhook trigger instead. That splits the flow across two executions and makes you correlate them yourself, and in production those deliveries are not retried if your endpoint is down. Webhooks vs polling covers the tradeoff; for a single signup inside one workflow, the long-poll is simpler.
What this does not cover
The workflow receives the email. It does not solve CAPTCHAs, SMS verification or bot detection on the signup form, and it gives you no permission to automate signups a service's terms forbid. Use it for your own product's signup flow, for staging environments, and for services that allow programmatic accounts. The same pattern in Make is in Make.com email verification automation, and the full list of /otp and /wait parameters is in the inboxes docs.