# Register

Create a user with username, email and password, and receive an access token, a refresh token and the user profile in the same response. A wallet can be created or imported in the same call.

## Overview

A successful registration returns `201` with `accessToken`, `refreshToken`, `expiresIn` and `user`, plus `wallet` when one was created or imported. The user is already signed in, so no separate [Login](https://docs.aureahub.com/docs/auth-login.md) call is needed. Renew the access token with [Refresh Token](https://docs.aureahub.com/docs/auth-refresh.md).

- **Tenant.** The user is created in the tenant identified by the `x-tenant-api-key` signing header. A `tenantApiKey` body field is accepted but does not change the tenant.
- **Uniqueness.** Neither the username nor the email may already belong to an account in the same tenant, unless that account's status is `inactive`. Otherwise the call returns `409`.
- **Password rules.** At least 8 characters, including a lowercase letter, an uppercase letter and a digit. A password shorter than 8 characters is rejected with `400`; a longer one missing a character type is rejected with `422`.

> ⚠️ This endpoint requires **tenant request signing**: send `x-tenant-api-key`, `x-timestamp` and `x-signature` as described in [Authentication](https://docs.aureahub.com/docs/authentication.md). It does not take a bearer token, and it is rate-limited to **20 requests per minute per client IP**.

## Endpoint

### `POST /v1/auth/register`

Creates a user in the signing tenant and returns an access token, a refresh token and the user (plus the wallet, if one was created or imported). Requires tenant HMAC signing headers.

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `username` | string | yes | 3–100 characters: letters, digits, underscore or hyphen |
| `email` | string | yes | A valid email address |
| `password` | string | yes | At least 8 characters, including a lowercase letter, an uppercase letter and a digit |
| `wallet` | object | no | Wallet to set up at registration. See Wallet Options. |
| `wallet.option` | string | no | create, import or skip. Required when wallet is sent. |
| `wallet.chain` | string | no | Chain identifier. Required for create and import. |
| `wallet.privateKey` | string | no | 64 hexadecimal characters, with or without 0x. Required for import. |
| `referralCode` | string | no | Referral code, up to 100 characters |
| `termsAcceptedAt` | string | no | When the user accepted your terms: ISO 8601 date-time in UTC with a Z suffix, e.g. 2026-09-01T10:00:00Z |
| `tenantApiKey` | string | no | Accepted (at least 32 characters) but not used: the tenant comes from the x-tenant-api-key header |

**Responses**

`201` Created

```json
{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
  "expiresIn": 900,
  "user": {
    "id": "<user-id>",
    "username": "alice",
    "email": "alice@example.com",
    "qrCode": "<qr-code>",
    "role": "user",
    "createdAt": "2026-09-01T10:00:00.000Z"
  }
}
```

`400` Bad Request

```json
{ "error": "Bad Request", "message": "Request validation failed" }
```

`401` Unauthorized

```json
{ "statusCode": 401, "error": "UnauthorizedError", "message": "Invalid signature" }
```

`404` Unknown API Key

```json
{ "statusCode": 404, "error": "NotFoundError", "message": "Invalid API key" }
```

`409` Conflict

```json
{ "error": "ConflictError", "message": "Username already exists" }
```

`422` Validation Error

```json
{
  "statusCode": 422,
  "error": "Validation Error",
  "message": "Request validation failed",
  "details": [
    { "path": "password", "message": "Password must contain at least one uppercase letter" },
    { "path": "password", "message": "Password must contain at least one digit" }
  ]
}
```

`429` Too Many Requests

```json
{ "statusCode": 429, "error": "Error", "message": "Rate limit exceeded, retry in 1 minute" }
```

**Example request**

```bash
curl -X POST https://api.aureahub.com/v1/auth/register \
  -H "Content-Type: application/json" \
  -H "x-tenant-api-key: <your-tenant-api-key>" \
  -H "x-timestamp: <unix-seconds>" \
  -H "x-signature: <hex-hmac-sha256>" \
  -d '{"username":"alice","email":"alice@example.com","password":"SecurePass123"}'
```

## Wallet Options

The optional `wallet` object controls whether a wallet is set up during registration:

| wallet | Result |
| --- | --- |
| `(omitted)` | If the tenant's configuration sets a default registration chain, a wallet is created on that chain automatically, except on tenants that use MPC wallets. Otherwise no wallet is created. |
| `{ "option": "create", "chain": "…" }` | Creates a wallet on that chain using the tenant's wallet mode (custodial unless the tenant is configured otherwise). On tenants that use MPC wallets, no wallet is created here: MPC wallets are created client-side through the MPC key-generation ceremony. |
| `{ "option": "import", "chain": "…", "privateKey": "…" }` | Imports an existing private key as the user's wallet on that chain. The raw private key travels to the API in the request body. |
| `{ "option": "skip" }` | No wallet is created, even if the tenant has a default registration chain. |

When a wallet is created or imported, the response includes `wallet` with `id`, `chain`, `address`, `isPrimary` and `createdAt`.

## Errors

On this endpoint, `400` and `409` bodies contain only `error` and `message`; `401`, `404`, `422` and `429` use the full envelope described in [Error Handling](https://docs.aureahub.com/docs/errors.md).

| Status | message | When |
| --- | --- | --- |
| 400 | `Request validation failed` | Any of: a required field is missing; username is not 3–100 letters, digits, underscores or hyphens; email is not a valid address; password is shorter than 8 characters; wallet.option is not create, import or skip; wallet.privateKey is not 64 hex characters; referralCode is longer than 100 characters; termsAcceptedAt is not a date-time; tenantApiKey is shorter than 32 characters. No details are returned. |
| 422 | `Request validation failed` | Any of: password is missing a lowercase letter, an uppercase letter or a digit; wallet is sent without option; option create without chain; option import without chain or privateKey; termsAcceptedAt has a UTC offset instead of Z. details lists each problem. |
| 401 | `Missing required HMAC headers: x-tenant-api-key, x-timestamp, x-signature` | Signature problem. Other messages: Invalid x-timestamp header, Request expired, Invalid signature, Replay detected. See Authentication. |
| 404 | `Invalid API key` | The x-tenant-api-key header does not match an active tenant. |
| 409 | `Username already exists` | The username is taken in this tenant. |
| 409 | `Email already exists` | The email is taken in this tenant. |
| 429 | `Rate limit exceeded, retry in 1 minute` | More than 20 requests in one minute from the same client IP. See the retry-after header. |

## Implementation

The tenant signature must cover the exact bytes you send, so build the body string once and use it for both the signature and the request:

```javascript
import { createHash, createHmac } from 'node:crypto';

const API = 'https://api.aureahub.com';

// Tenant request signing: see Authentication → HMAC Request Signing
function signedHeaders(method, pathWithQuery, body = '') {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const bodyHash = createHash('sha256').update(body).digest('hex');
  const signature = createHmac('sha256', process.env.AUREA_TENANT_API_SECRET)
    .update(`${method}\n${pathWithQuery}\n${timestamp}\n${bodyHash}`)
    .digest('hex');
  return {
    'x-tenant-api-key': process.env.AUREA_TENANT_API_KEY,
    'x-timestamp': timestamp,
    'x-signature': signature,
  };
}

export async function register({ username, email, password }) {
  const path = '/v1/auth/register';
  const body = JSON.stringify({ username, email, password });

  const res = await fetch(API + path, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', ...signedHeaders('POST', path, body) },
    body, // exactly the string that was signed
  });
  const data = await res.json();

  if (res.status === 422 && Array.isArray(data.details)) {
    // e.g. "password: Password must contain at least one digit"
    throw new Error(data.details.map((d) => `${d.path}: ${d.message}`).join('; '));
  }
  if (!res.ok) {
    // 409: "Username already exists" / "Email already exists"
    throw new Error(`${res.status}: ${data.message ?? data.error}`);
  }

  const { accessToken, refreshToken, expiresIn, user, wallet } = data;
  return { accessToken, refreshToken, expiresIn, user, wallet };
}
```

---

Web version: https://docs.aureahub.com/#auth-register
