# Authentication

> Set up Bearer token authentication for the DayZero API.

- **Category:** Getting Started
- HTML version: https://www.ondayzero.com/docs/guides/authentication
- All guides: https://www.ondayzero.com/docs/guides/index.md
- Endpoint catalog: https://www.ondayzero.com/docs/reference/index.md

---

# Authentication

All DayZero API requests require a **Bearer token** in the `Authorization` header. Almost every endpoint also needs a **business context** header, because one token can usually reach more than one business.

## Obtaining a Token

Generate an API token from your DayZero dashboard under **Settings → Developers**. A person creates every token in the dashboard: minting one requires **two-factor authentication** on the account, and the dashboard asks you to verify both factors when you generate it. There is no way to mint a token via the API — `POST /api/v1/tokens` refuses an API token with `403` `AUTH_057`, so a leaked key can never be used to create more keys.

Tokens look like `dz_...`. When you create one you choose:

| Option | Values | Notes |
|--------|--------|-------|
| Name | text | Shown in the dashboard; use it to identify the integration |
| Expiration | 1–365 days (default 365) | Expired tokens return `401` |
| Scope | *(omit)*, `read_only`, `credit`, `credit:read`, `credit:payments` | Omit for full access. `read_only` blocks every mutating endpoint. `credit` / `credit:read` / `credit:payments` limit the token to the partner-credit API (all methods, GET only, or payment writes plus a line's payoff quote and payment list) |
| Allowed IPs | IPv4, IPv6, or CIDR | Optional. When set, requests from any other address return `403` `AUTH_028`. Set at minting; mint a new token to change the list |

The plaintext token is shown **once**, in the dashboard. Store it; it cannot be retrieved again. Create a separate token per integration so one can be revoked without breaking the others.

## Making Authenticated Requests

Start by listing the businesses the token can access — this is the one common endpoint that does **not** need `x-business-id`:

```bash
curl "https://api.ondayzero.com/api/v1/businesses" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "Accept: application/json"
```

Then send both headers on every other request:

```bash
curl "https://api.ondayzero.com/api/v1/invoices?limit=20" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: 01912345-abcd-7000-8000-000000000001" \
  -H "Accept: application/json"
```

| Header | Required | Description |
|--------|----------|-------------|
| `Authorization` | Yes | `Bearer dz_...` token |
| `x-business-id` | Yes, on business-scoped endpoints | UUID of the business. Operations that need it list it as a required header parameter in the API Reference |
| `Accept` | Recommended | `application/json` guarantees JSON error bodies (HTML error pages are only rendered for browser-style `text/html` requests) |
| `Content-Type` | On requests with a body | `application/json` |

### Authentication errors

| Status | `code` | Meaning |
|--------|--------|---------|
| `401` | `AUTH_008` | `x-business-id` header missing on an endpoint that requires it |
| `401` | `AUTH_010` | The token cannot access that business |
| `401` | other `AUTH_*` | Token missing, malformed, expired or revoked |
| `403` | — | Token is valid but its scope (e.g. `read_only`) does not permit the operation |
| `403` | `AUTH_028` | Token is valid but the request did not come from an allowed IP |
| `403` | `AUTH_057` | The operation needs a recent identity check an API token cannot provide (for example `POST /api/v1/tokens`). Do it in the dashboard while signed in |

None of these are worth retrying unchanged — fix the header or the token. See [Error Handling](/docs/guides/error-handling) for the response envelope.

## Token Limits and Revocation

- Maximum **10 active tokens** per user (`VALIDATION_166` when exceeded)
- Optional expiration between 1 and 365 days. Mint a replacement before expiry, swap the integration, then revoke the old token so nothing breaks mid-swap
- Optional IP allowlist per token (`allowed_ips` on create)
- Revoke at any time from **Settings → Developers**, or with `POST /api/v1/tokens/{token_id}/revoke` (keeps the record, marks it revoked) or `DELETE /api/v1/tokens/{token_id}`
- `GET /api/v1/tokens` lists your tokens (add `include_revoked=true` to see revoked ones); plaintext values are never returned

## Partner OAuth 2.0

DayZero also runs an OAuth 2.0 token service for approved partner integrations: `POST /api/v1/oauth2/token` issues access + refresh tokens for a user, authenticated with an HMAC-SHA256 signature over a partner `client_id`, timestamp and nonce; `POST /api/v1/oauth2/refresh` renews them. It is provisioned per partner and is not self-serve — for your own integrations, use API tokens.

## AI Assistants (MCP)

The DayZero **MCP server** (`https://api.ondayzero.com/mcp`) lets AI assistants like Claude, ChatGPT, Cursor and Codex interact with your accounting data through **OAuth 2.1**:

1. The client discovers auth requirements via `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server`.
2. It registers via dynamic client registration and opens a browser to `/oauth2/authorize`.
3. You sign in and approve scopes on the consent page.
4. The client exchanges the code at `/oauth2/mcp/token` for MCP-scoped tokens.

**Important:** MCP tokens and REST API tokens are separate credential types:

| Credential | Works on MCP | Works on REST API |
|------------|--------------|-------------------|
| MCP OAuth token (`type=mcp`) | ✓ | ✗ |
| REST API key (`type=api`, `dz_…`) | ✗ | ✓ |
| SPA session token (`type=access`) | ✗ | ✓ |

Review and revoke MCP-connected clients with `GET /api/v1/oauth/grants` and `DELETE /api/v1/oauth/grants/{grant_id}`. REST API keys are revoked separately (see above).

See the [MCP Server](/docs/guides/mcp) guide for setup, scopes, and troubleshooting, or [Using DayZero from an AI Agent](/docs/guides/ai-agents) if you are building an agent against the REST API.
