Signup
An agent creates its own account in two calls, with no human and no card:
GET /v1/signup/challengereturns a challenge and a difficulty.- Solve the proof of work and send it to
POST /v1/signup. The response holds the API key (shown once), the first inbox and the account’s limits.
New accounts start in sandbox, with a 7-day period.
Get a proof-of-work challenge
Section titled “Get a proof-of-work challenge”GET /v1/signup/challenge
Auth: none
Step 1 of signup. Returns a signed challenge that is valid for 10 minutes, and its
difficulty. Find a nonce of 1 to 32 ASCII letters or digits such that the SHA-256 digest
of the UTF-8 string <challenge>:<nonce> starts with at least difficulty zero bits
(counted from the most significant bit of the first byte), then send both to
POST /v1/signup. At difficulty 20 that takes about a million attempts on average. Treat the
challenge as an opaque string. Nothing is stored on the server until signup.
Example solver (Node.js 22 or later):
import { createHash } from "node:crypto";const zeroBits = (buf) => { let bits = 0; for (const byte of buf) { if (byte === 0) { bits += 8; continue; } return bits + Math.clz32(byte) - 24; } return bits;};let i = 0;while (zeroBits(createHash("sha256").update(`${challenge}:${i.toString(36)}`).digest()) < difficulty) i++;const nonce = i.toString(36);Each challenge is good for one signup. If Replyz raises the difficulty, challenges issued
before at a lower difficulty are refused with pow_expired.
Problem codes: signups_disabled (503), rate_limited (429).
Responses
Section titled “Responses”A new challenge.
Content type application/json.
| Field | Type | Required | Description |
|---|---|---|---|
challenge |
string | yes | Opaque signed string. Send it back unchanged with your nonce. |
difficulty |
integer (8–28) | yes | Number of leading zero bits the digest must have. 20 by default; Replyz can set it anywhere from 8 to 28. |
expires_at |
string (date-time) | yes | End of the challenge’s 10-minute validity. After it, signup answers pow_expired. |
{ "challenge": "v1.20.1793959798000.9SbAcYNJrG6jK6XF.3b7a3f744a722f0867867dae9719a28191d0f50f89bb00b70164c5e5405d2cd4", "difficulty": 20, "expires_at": "2026-11-06T10:09:58.000Z"}rate_limited: too many signup requests from this network in the last minute.
| Header | Description |
|---|---|
Retry-After |
Always 60. |
Problem object (application/problem+json). Codes: rate_limited.
Example: rate_limited
{ "type": "https://docs.replyz.io/errors/rate_limited", "code": "rate_limited", "status": 429, "message": "Too many requests from this network.", "fix": "Wait a minute before trying again.", "docs": "https://docs.replyz.io/errors/rate_limited.md"}internal_error: something failed on Replyz’s side. Retry after a few seconds.
Problem object (application/problem+json). Codes: internal_error.
Example: internal_error
{ "type": "https://docs.replyz.io/errors/internal_error", "code": "internal_error", "status": 500, "message": "Something went wrong on our side.", "fix": "Retry in a few seconds.", "docs": "https://docs.replyz.io/errors/internal_error.md"}signups_disabled: Replyz has paused new signups.
| Header | Description |
|---|---|
Retry-After |
Always 3600. |
Problem object (application/problem+json). Codes: signups_disabled.
Example: signups_disabled
{ "type": "https://docs.replyz.io/errors/signups_disabled", "code": "signups_disabled", "status": 503, "message": "New signups are paused.", "fix": "Try again later.", "docs": "https://docs.replyz.io/errors/signups_disabled.md"}Create an account (agent self-signup)
Section titled “Create an account (agent self-signup)”POST /v1/signup
Auth: none
Step 2 of signup. Send the challenge from GET /v1/signup/challenge with a solved nonce.
Creates a sandbox account with one inbox and returns its API key.
- Save
api_keynow: it is shown only once. Replyz stores only a hash of it and cannot show it again. agent_name(optional, defaultagent) is stored on the account and becomes the start of the inbox address (see the Inboxes tag). A reserved name is refused before the proof of work is checked, so the same challenge can be sent again with another name.- The challenge is used up as soon as its proof of work is accepted, even if a later check fails. Get a new challenge before retrying.
- Signup is not idempotent, and
Idempotency-Keyis ignored here: every successful call creates a new account. If the response is lost, sign up again; the unused account still counts toward the network’s daily signup limit. - Limits: 20 signups per network per day (UTC), counted after a valid proof of work, and a burst guard of about 20 requests per minute per network. A network is one IPv4 address or one IPv6 /64.
- The sandbox period is 7 days (
expires_at). After it, the account is treated asexpired. - The inbox created here counts toward the account’s daily inbox-creation limit (3 per UTC day in sandbox), so a new account can create 2 more inboxes on its first day (a sandbox account still has at most 1 live inbox at a time).
claim_urlis meant for the human owner, but the claim page is not available yet.
Checks run in this order: signups enabled, burst guard, body, reserved name, proof of work, challenge already used, daily limit per network, free address.
Problem codes: signups_disabled (503), rate_limited (429), payload_too_large (413),
invalid_json (400), invalid_request (400), name_reserved (400), pow_invalid (400),
pow_expired (400), pow_reused (400), signup_limit (429), address_unavailable (503).
Request body
Section titled “Request body”Content type application/json (required).
| Field | Type | Required | Description |
|---|---|---|---|
challenge |
string | yes | The challenge from GET /v1/signup/challenge, unchanged. |
nonce |
string | yes | Your solution: 1 to 32 ASCII letters or digits such that the SHA-256 digest of <challenge>:<nonce> starts with at least difficulty zero bits. A nonce of any other shape is refused with pow_invalid. |
agent_name |
string or null (length 0–64, default "agent") |
no | A name for the agent, at most 64 characters after control characters are removed and the value is trimmed. Stored on the account and used for the inbox address (see the Inboxes tag). null or an empty string means agent. A reserved name is refused with name_reserved. |
{ "challenge": "v1.20.1793959798000.9SbAcYNJrG6jK6XF.3b7a3f744a722f0867867dae9719a28191d0f50f89bb00b70164c5e5405d2cd4", "nonce": "11luo", "agent_name": "Atlas"}Responses
Section titled “Responses”The account was created. Store api_key now; it is not shown again.
Content type application/json.
| Field | Type | Required | Description |
|---|---|---|---|
org_id |
string | yes | The new account’s id. |
status |
string "sandbox" |
yes | New accounts are always sandbox. |
api_key |
string | yes | The account’s API key. Shown once: store it now. Replyz keeps only a hash and cannot show it again. |
inbox |
InboxRef | yes | The first inbox, created with the account. |
inbox.id |
string | yes | |
inbox.address |
string (email) | yes | |
claim_url |
string (uri) | yes | Link meant for the human owner to claim the account (https://replyz.io/claim/<token>). The claim page is not available yet. |
expires_at |
string (date-time) | yes | End of the sandbox period, 7 days after signup. |
limits |
Limits | yes | The account’s limits, which depend on its status. Expired and suspended accounts show the sandbox numbers, or the claimed numbers when they had been claimed before. |
limits.inboxes |
integer | yes | Maximum number of live inboxes: 1 in sandbox (3 for a claimed account; claiming is not available yet). |
limits.receive_per_month |
integer | yes | Messages accepted per calendar month (UTC): 1,000 in sandbox (5,000 for a claimed account; claiming is not available yet). Mail above the cap is refused during delivery. |
limits.send_scope |
string: none |
yes | What the account may send. Always none, because sending is not available in this version. |
terms_url |
string (uri) | yes | Link to the terms of service. The page is published before the public launch. |
privacy_url |
string (uri) | yes | Link to the privacy notice. The page is published before the public launch. |
next |
string | yes | A short instruction for the agent. Until the claim page opens it says that claim_url will let the human own the account once that page opens. |
{ "org_id": "org_01M6R4T180QW261WNQRBSATDJ0", "status": "sandbox", "api_key": "rz_live_v0O1W6r3uuJyQUgG7EQ6Ei1JZ2rHdmbS", "inbox": { "id": "ibx_01M6R4T180ZADNV1160NS1SRNP", "address": "atlas-wo7tw5@youreplyz.com" }, "claim_url": "https://replyz.io/claim/RphFRvF9niaE5R393dB1cOZTardtvAsD", "expires_at": "2026-11-13T10:00:00.000Z", "limits": { "inboxes": 1, "receive_per_month": 1000, "send_scope": "none" }, "terms_url": "https://replyz.io/terms", "privacy_url": "https://replyz.io/privacy", "next": "Save api_key now (shown once). claim_url will let your human own this account once the claim page opens."}The request was refused.
invalid_json: the body is not a JSON object.invalid_request:challengeornonceis missing or not a string, oragent_nameis not a string or is longer than 64 characters.name_reserved:agent_nameis reserved. The challenge is still valid.pow_invalid: the challenge is malformed or was not issued by Replyz, the nonce does not match^[0-9A-Za-z]{1,32}$, or the digest has too few zero bits. The message names the reason (malformed,bad_signatureorinsufficient).pow_expired: the challenge expired, or was issued at a lower difficulty than the current one.pow_reused: the challenge was already used for a signup.
Problem object (application/problem+json). Codes: invalid_json, invalid_request, name_reserved, pow_invalid, pow_expired, pow_reused.
Example: invalid_json
{ "type": "https://docs.replyz.io/errors/invalid_json", "code": "invalid_json", "status": 400, "message": "Body must be a JSON object.", "fix": "Send Content-Type: application/json with a JSON object.", "docs": "https://docs.replyz.io/errors/invalid_json.md"}Example: invalid_request: agent_name too long
{ "type": "https://docs.replyz.io/errors/invalid_request", "code": "invalid_request", "status": 400, "message": "\"agent_name\" is longer than 64 characters.", "fix": "Shorten \"agent_name\".", "docs": "https://docs.replyz.io/errors/invalid_request.md"}Example: name_reserved: reserved agent_name
{ "type": "https://docs.replyz.io/errors/name_reserved", "code": "name_reserved", "status": 400, "message": "That agent_name is reserved (role words, brand names and look-alikes).", "fix": "Choose a different agent_name (your challenge is still valid), e.g. your own name for the agent.", "docs": "https://docs.replyz.io/errors/name_reserved.md"}Example: pow_invalid: not enough zero bits
{ "type": "https://docs.replyz.io/errors/pow_invalid", "code": "pow_invalid", "status": 400, "message": "Proof of work rejected (insufficient).", "fix": "Get a fresh challenge from GET /v1/signup/challenge and solve it.", "docs": "https://docs.replyz.io/errors/pow_invalid.md"}Example: pow_expired
{ "type": "https://docs.replyz.io/errors/pow_expired", "code": "pow_expired", "status": 400, "message": "The challenge expired or is out of date.", "fix": "Get a fresh challenge from GET /v1/signup/challenge and solve it.", "docs": "https://docs.replyz.io/errors/pow_expired.md"}Example: pow_reused
{ "type": "https://docs.replyz.io/errors/pow_reused", "code": "pow_reused", "status": 400, "message": "This challenge was already used.", "fix": "Get a fresh challenge from GET /v1/signup/challenge.", "docs": "https://docs.replyz.io/errors/pow_reused.md"}payload_too_large: the body is larger than 16,384 bytes.
Problem object (application/problem+json). Codes: payload_too_large.
Example: payload_too_large
{ "type": "https://docs.replyz.io/errors/payload_too_large", "code": "payload_too_large", "status": 413, "message": "Body is larger than 16384 bytes.", "fix": "Send a smaller JSON body.", "docs": "https://docs.replyz.io/errors/payload_too_large.md"}rate_limited: too many signup requests from this network in the last minute.signup_limit: this network already made 20 signups today (UTC).
| Header | Description |
|---|---|
Retry-After |
60 with rate_limited; the seconds until the next UTC day with signup_limit. |
Problem object (application/problem+json). Codes: rate_limited, signup_limit.
Example: rate_limited
{ "type": "https://docs.replyz.io/errors/rate_limited", "code": "rate_limited", "status": 429, "message": "Too many requests from this network.", "fix": "Wait a minute before trying again.", "docs": "https://docs.replyz.io/errors/rate_limited.md"}Example: signup_limit
{ "type": "https://docs.replyz.io/errors/signup_limit", "code": "signup_limit", "status": 429, "message": "Too many signups from this network today.", "fix": "Try again tomorrow (UTC).", "docs": "https://docs.replyz.io/errors/signup_limit.md"}internal_error: something failed on Replyz’s side. Retry after a few seconds.
Problem object (application/problem+json). Codes: internal_error.
Example: internal_error
{ "type": "https://docs.replyz.io/errors/internal_error", "code": "internal_error", "status": 500, "message": "Something went wrong on our side.", "fix": "Retry in a few seconds.", "docs": "https://docs.replyz.io/errors/internal_error.md"}signups_disabled: Replyz has paused new signups.address_unavailable: no free address was found (rare). Retry with a new challenge.
| Header | Description |
|---|---|
Retry-After |
3600 with signups_disabled; not sent with address_unavailable. |
Problem object (application/problem+json). Codes: signups_disabled, address_unavailable.
Example: signups_disabled
{ "type": "https://docs.replyz.io/errors/signups_disabled", "code": "signups_disabled", "status": 503, "message": "New signups are paused.", "fix": "Try again later.", "docs": "https://docs.replyz.io/errors/signups_disabled.md"}Example: address_unavailable (signup)
{ "type": "https://docs.replyz.io/errors/address_unavailable", "code": "address_unavailable", "status": 503, "message": "Could not find a free address.", "fix": "Retry the signup with a new challenge.", "docs": "https://docs.replyz.io/errors/address_unavailable.md"}