Programmatic signup

For most users, the right path is to sign up in the browser and copy your API key from the dashboard — see Authentication. This page is for code that needs to sign up its own user, typically:

  • Agents and Claude skills that onboard a new caller automatically.
  • CI / test harnesses that mint throwaway accounts.
  • Wrapper tools that abstract the dashboard away from end users.

The same three endpoints power the Code Dashboard's sign-up form, so anything you do here matches what a normal user would do in the browser.

The three-step flow


POST /api/user/create   →   creates the account (no token yet)
        │
        ▼
POST /api/user/login    →   returns a Bearer JWT
        │
        ▼
GET  /api/user/key/create   →   returns the long-lived API key

All requests go to https://lar.axiom.ai.

Step 1 — Create the account

curl -X POST https://lar.axiom.ai/api/user/create \
  -H "Content-Type: application/json" \
  -d '{
    "name":     "Ada Lovelace",
    "email":    "ada@example.com",
    "password": "<strong-password>",
    "company":  "",
    "country":  "",
    "role":     "",
    "campaign": "programmatic-signup",
    "language": "en-GB"
  }'
FieldRequiredNotes
nameyesFree text. Rejected if it looks like a URL or email address — the server matches [a-z0-9@:%._+~#=]{1,256}\.[a-z0-9()]{1,6} and bails. Keep names plain.
emailyesMust not be from a disposable-email provider (the server runs an isDisposable() check and returns {"status": "To prevent abuse of our free plan, please do not register with an anonymous email provider."} on failure).
passwordyesNo length minimum enforced server-side at the moment; use something sensible.
company, country, rolenoFree text. Used for analytics; pass empty strings if unknown.
campaignnoFree text. Use this to mark where the signup came from (code-dashboard-signup, claude-skill, your tool name) so the team can attribute traffic.
languagenoBCP 47 tag (e.g. en-GB, de-DE). Defaults to whatever the browser sends; in headless code, pass it explicitly.

Response (success): the new user object.

{
  "id": 12345,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "company": "",
  "role": "",
  "country": "",
  "language": "en-GB"
}

Response (failure): a 200 OK with a status field describing what went wrong. The endpoint doesn't use HTTP status codes for these cases — watch the status field.

{ "status": "email_exists" }
{ "status": "Could not create an account, name field contains a URL or email address" }
{ "status": "To prevent abuse of our free plan, please do not register with an anonymous email provider." }

Welcome email is sent automatically on success.

Step 2 — Log in

curl -X POST https://lar.axiom.ai/api/user/login \
  -H "Content-Type: application/json" \
  -d '{"email": "ada@example.com", "password": "<strong-password>"}'

Response (success):

{
  "id": 12345,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
  "refresh_token": "def502009b..."
}

token is an OAuth2 access token (Bearer JWT) you'll use to authenticate the next call. It's not the long-lived API key yet — that's step 3.

Response (failure):

BodyMeaning
nullEmail doesn't exist on the platform, or password is wrong. The endpoint deliberately collapses these into one signal to avoid leaking which case applies.
"blocked"Account is locked out after 29 failed login attempts. Will stay locked until contact support to reset. Don't loop on this.

Step 3 — Mint the long-lived API key

curl https://lar.axiom.ai/api/user/key/create \
  -H "Authorization: Bearer <JWT_FROM_STEP_2>"

Response:

{ "token": "axm_..." }

This token is the long-lived API key you use everywhere else in the API — see Authentication for how it's passed.

One key per account. Calling GET /api/user/key/create on an account that already has a key invalidates the old one and returns a new one. Any existing integration using the old key stops working immediately. To check if an account already has a key without invalidating it, call GET /api/user/key/has-existing (same Bearer JWT auth) and inspect {"result": true|false}.

End-to-end example (Node.js)


const BASE = 'https://lar.axiom.ai';

async function signUpAndMintKey({ name, email, password, campaign = 'programmatic' }) {
  // Step 1 — create
  const createRes = await fetch(`${BASE}/api/user/create`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      name, email, password,
      company: '', country: '', role: '',
      campaign,
      language: process.env.LANG?.split('.')[0]?.replace('_', '-') ?? 'en-GB',
    }),
  });
  const createJson = await createRes.json();
  if (createJson.status) {
    // status field present = an error case (email_exists, disposable, name-looks-like-url, ...)
    throw new Error(`signup failed: ${createJson.status}`);
  }

  // Step 2 — log in
  const loginRes = await fetch(`${BASE}/api/user/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });
  const loginJson = await loginRes.json();
  if (loginJson === null) throw new Error('login failed: bad credentials');
  if (loginJson === 'blocked') throw new Error('login failed: account locked (too many failed attempts)');

  // Step 3 — mint API key (uses the JWT from step 2)
  const keyRes = await fetch(`${BASE}/api/user/key/create`, {
    headers: { 'Authorization': `Bearer ${loginJson.token}` },
  });
  const keyJson = await keyRes.json();

  return { user: createJson, apiKey: keyJson.token };
}

// Usage
const { user, apiKey } = await signUpAndMintKey({
  name: 'Ada Lovelace',
  email: `ada+${Date.now()}@example.com`,  // unique email avoids `email_exists`
  password: process.env.NEW_ACCOUNT_PASSWORD,
  campaign: 'my-tool-headless-signup',
});
console.log(`Provisioned ${user.email} with API key (last 6: ${apiKey.slice(-6)})`);

If the account already exists


Skip step 1 and just do steps 2-3:

// existing email + password → JWT → API key
const loginRes = await fetch(`${BASE}/api/user/login`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, password }),
});
const { token: jwt } = await loginRes.json();

const keyRes = await fetch(`${BASE}/api/user/key/create`, {
  headers: { 'Authorization': `Bearer ${jwt}` },
});
const { token: apiKey } = await keyRes.json();

Remember: step 3 invalidates the previous API key. If the user already had integrations wired up, you'll break them. Use GET /api/user/key/has-existing first to avoid surprising them.

Abuse & rate-limit considerations


  • Disposable-email check. Step 1 blocks signups from anonymous email providers (mailinator.com, 10minutemail.com, etc.). If your tool legitimately needs to onboard high volumes, work with the team to whitelist your domain rather than trying to bypass the check.
  • Login lockout. 29 failed login attempts puts the account into "blocked" state. The lockout is not auto-cleared — the user has to email support. So loops that retry login on a bad password are dangerous.
  • No bulk-create endpoint. If you find yourself signing up thousands of accounts programmatically, talk to the team first — at that point a different account model (team API key, multi-tenant scoping) is the right answer.
  • Welcome email is sent on success. Every successful POST /api/user/create sends a registered-with-Axiom email. Don't use this flow for ephemeral test accounts at scale without coordinating with the team — it's a deliverability liability.