Website sign-in API

The /_axis/auth and /_axis/api endpoints on your own site: sign-up, sign-in, one-time codes, the session cookie, API routes and axis.user.

Sign people in on your own website and call your functions as them. Everything here is served on your site's own address (for example https://yourshop.axispages.app) under /_axis/ — never on axisiq.co. A site answers once it has a user pool chosen in Settings → Sign-in & API; see Sign-in and API on your site.

How requests work

  • Same site only. Call these addresses from your own pages with relative URLs (fetch("/_axis/auth/me")). There is no cross-origin access.
  • JSON for every change. Any request other than GET must send Content-Type: application/json (otherwise 415). When the browser sends an Origin header it must be your site's own address (otherwise 403).
  • The session is a cookie. A successful sign-in sets __Host-axis_session — HttpOnly, Secure, SameSite=Lax, for your site's address only. Your JavaScript can't read it and doesn't need to: the browser sends it with every same-site request. Only https:// addresses can sign in.
  • Answers look like the rest of the API: {"data": …} on success, {"error": {"code", "message", "request_id"}} on failure, never cached.
  • Limits. 30 sign-in requests a minute per visitor address, 10 password tries per account per 15 minutes, 5 codes per address per 15 minutes. Past a limit: 429 RATE_LIMITED with Retry-After.

Sign-in endpoints

Method Path Body → answer
GET /_axis/auth/config → {pool, methods, signup, signup_fields, editable_fields, password_min, password_reset}. methods lists password, email_otp, sms_otp; signup is open or invite; signup_fields are {key, label, type, required, options} for drawing your sign-up form
POST /_axis/auth/signup {email?, phone?, password, profile?} → the account, signed in. 409 EMAIL_TAKEN / PHONE_TAKEN when the pool already has it; 403 on an invite-only pool; 400 names a missing or invalid profile field
POST /_axis/auth/login {email | phone, password} → the account, signed in. Wrong email or password: 401 INVALID_CREDENTIALS (the same answer either way)
POST /_axis/auth/otp/start {channel: "email" | "sms", to, purpose?: "login" | "verify" | "reset"} → 204. Always 204, whether or not an account exists
POST /_axis/auth/otp/verify {channel, to, code, purpose?, profile?, password?} → login: the account, signed in (an open pool creates it, using profile for the sign-up fields). verify: 204, the signed-in person's own email or phone is now confirmed. reset: 204, password is set and every session ends
GET /_axis/auth/me → the account; 401 UNAUTHORIZED when signed out
PATCH /_axis/auth/me {profile: {…}} — only the pool's editable fields → the account
POST /_axis/auth/password {current_password, password} → 204; other sessions end
POST /_axis/auth/logout {all?: true} → 204, cookie cleared; all signs out every browser

phone numbers include the country code, like +919876543210. Emails are matched case-insensitively.

The account looks like this:

{
  "id": "01a1…",
  "pool": "customers",
  "email": "ana@example.com",
  "phone": "",
  "email_verified": true,
  "phone_verified": false,
  "has_password": true,
  "roles": [{ "id": "01a1…", "name": "Buyer" }],
  "profile_id": "01a1…",
  "profile": { "name": "Ana" }
}

profile holds the fields the pool shows to people. A suspended account gets 403 FORBIDDEN after a correct password.

Example: sign in with a code

<form id="start"><input name="email" type="email" required><button>Email me a code</button></form>
<form id="verify" hidden><input name="code" inputmode="numeric" required><button>Sign in</button></form>
<script>
  const post = (path, body) => fetch(path, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  }).then(async (r) => (r.status === 204 ? {} : r.json()));

  let email;
  start.onsubmit = async (e) => {
    e.preventDefault();
    email = start.email.value;
    await post("/_axis/auth/otp/start", { channel: "email", to: email });
    start.hidden = true; verify.hidden = false;
  };
  verify.onsubmit = async (e) => {
    e.preventDefault();
    const res = await post("/_axis/auth/otp/verify", { channel: "email", to: email, code: verify.code.value });
    if (res.error) return alert(res.error.message);
    const next = new URLSearchParams(location.search).get("next");
    location.href = next && next.startsWith("/") ? next : "/account";
  };
</script>

A sign-in only page sends signed-out visitors to its sign-in page with ?next= — only follow it when it starts with /, as above.

API routes

An API route you add in Settings → Sign-in & API answers at /_axis/api<path> and runs its function as the visitor. The function's handler(event) receives:

{
  "method": "POST",
  "path": "/orders/42",
  "route": "/orders/*",
  "splat": "42",
  "query": { "include": "lines" },
  "body": { "qty": 2 }
}

body is the parsed JSON, or the raw text when it isn't JSON (up to 1 MB). The function's return value is sent back as {"data": <value>}. A function that throws or runs out of time answers 500 FUNCTION_ERROR — the details are in the function's run history, never shown to the visitor. A route for signed-in people answers 401 to a signed-out visitor and 403 to one without the route's roles, before the function runs. An unknown route is 404.

axis.user

Inside a function behind a route, axis.user is the signed-in person, or null for anyone else. It can't be changed.

function handler(event) {
  if (!axis.user) throw new Error("sign in first");
  const mine = axis.records.search("orders", {
    filters: [{ field: "customer_email", op: "eq", value: axis.user.email }],
  });
  return { hello: axis.user.profile.name, orders: mine.records };
}

axis.user holds id, pool, email, phone, email_verified, phone_verified, roles ([{id, name}]), profile_id and profile (the fields the pool shows to people).

Every axis.records call runs with the signed-in person's roles: a role limited to only ones they created only ever sees and changes their own records, and records the function creates count as created by them. For a visitor who isn't signed in, the function runs with the access your workspace gives the public.