Creating an Account
Self-serve signup is open to anyone. Two calls take someone from nothing to a live session.
POST /auth/signup ──► 202, no session
│
▼
verification email ──► https://durohub.com/verify-email?token=…
│
▼
POST /auth/verify-email ──► 200, first session issuedStep 1 — Register
curl -sX POST https://api.durohub.com/auth/signup \
-H 'Content-Type: application/json' \
-H 'Origin: https://durohub.com' \
-d '{
"email": "dana@acme.com",
"name": "Dana Reyes",
"password": "correct-horse-battery-9!"
}'{ "status": "pending_verification" }202 Accepted, and no session — signup creates an identity and stops there.
email
Yes
Max 254 characters
name
No
Derived from the address when omitted
Every signup returns the same 202
Register an address that already has an account and you get the same status and the same body. Nothing is created and no existing account is touched. Duro emails the address's owner instead, letting them know someone tried to sign up and offering a password reset.
A rejected password is the exception — that comes back as a 400 naming the rule that failed, since it describes what the caller just typed rather than who exists.
Password rules
The rules are served at runtime rather than fixed, so fetch them instead of hardcoding:
duro
nist
Minimum length
12
15
Maximum length
256
256
Lowercase, uppercase, number, and symbol each required
Yes
No
Common-pattern check
Yes
Yes
Which mode is active is a deployment setting — read passwordPolicy.mode rather than assuming. nist follows NIST SP 800-63B, which asks for more length and no character-class requirements.
Two things worth knowing if you validate before submitting:
Symbols are anything that is not a letter, a digit, or whitespace — not a fixed
!@#$%^&*list, so£,€, and non-Latin symbols all countCommon patterns are rejected — a capitalised word followed by digits and punctuation (
Password1!,Summer2026!), keyboard runs likeqwerty, and long character repeats
A rejected password returns 400 with a message naming the rule that failed. That is the one signup response that is not a fixed 202, since it describes what the caller just typed rather than who exists.
Step 2 — Verify the email address
The email links to the Duro app, not the API:
That page posts the token:
The response sets __Host-duro_session. Verifying signs the user in — the token already proved they control the mailbox.
If you build your own verification page, make it POST — never link straight to a GET that consumes the token.
Outlook Safe Links, Gmail's proxy, and corporate URL scanners all fetch links in email, often before the recipient opens the message. A GET endpoint would have its single-use token spent by the scanner, and the real user would always land on "this link is invalid". Scanners do not run JavaScript, so a page that renders first and posts second only fires for a human.
Tokens are single-use and expire after one hour. Unknown, expired, already-used, and wrong-purpose tokens all return:
Give that case somewhere to go — a "request a new link" action and a route back to sign-in. A link that sat in an inbox overnight is the common case, not the edge case.
Verification is required
An address must be verified before Duro will apply an invitation, an allowlist entry, or a domain match to it. Until then the account exists but cannot join anything.
Verification comes from one of three places:
Password accounts — consuming the emailed token
Google — Google asserting the address is verified
SAML — an organization asserting an address inside a domain it has verified ownership of
Completing a password reset also counts, since it proves the same thing.
What a new account can do
Very little until something admits it. Duro lets someone in when any of these is true:
Existing membership
They already belong to an organization
Invitation
An admin invited their address, and they verified it
Allowlist
An organization's allowlist covers their address or its domain
Org-creation grant
Duro issued a pending grant to create a new organization
With none of those, organization-scoped queries are refused. user { me { hasOrganizations } } tells you which state you are in — see Current User.
Rate limits
POST /auth/signup
10 per 10 minutes
POST /auth/verify-email
20 per 10 minutes
Past the budget, requests slow down rather than failing outright; a 429 AUTH_RATE_LIMITED only appears well beyond it. Limits are keyed on IP and never on the account, so nobody can lock an address out by hammering it.
Errors
AUTH_ORIGIN_REJECTED
403
Missing or non-allowlisted Origin header
AUTH_TOKEN_INVALID
400
Verification token unknown, expired, or already used
AUTH_RATE_LIMITED
429
Well past the route budget
AUTH_REQUEST_REJECTED
400
Malformed request, or a password the policy refused
Every /auth/* error returns this flat shape. Branch on code; the message is copy and may be reworded.
Next steps
Once accounts exist, Enterprise SSO lets your identity provider sign those people in, and SCIM Provisioning creates and deprovisions them from your directory automatically.
Last updated
Was this helpful?