For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 issued

Step 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.

Field
Required
Notes

email

Yes

Max 254 characters

password

Yes

Must satisfy the live policy — see Password rules

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.

Do not render "that email is already taken." The response cannot tell you, because a distinguishable answer would let anyone test a list of addresses to learn who has a Duro account.

Show the same "check your email" state for every accepted signup and let the mailbox resolve it.

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 count

  • Common patterns are rejected — a capitalised word followed by digits and punctuation (Password1!, Summer2026!), keyboard runs like qwerty, 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.

The endpoint is unauthenticated and cheap, so call it before rendering a signup form and show the live rules. A form that says "at least 12 characters" against a server enforcing 15 accepts the password and then fails on submit with nothing explaining why.

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.

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:

Route in
How it happens

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.

If people are meant to arrive by invitation, send the invitation first. Invitations match the verified address case-insensitively, so an invitation to dana@acme.com is not consumed by an account that verified dana.reyes@acme.com — it stays open and Duro reports that it was issued to a different address.

Rate limits

Route
Budget, per IP

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

Code
Status
Means

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?