> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dualentry.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication: API Keys, Roles, and Statuses

> Authenticate DualEntry Public API requests with an API key in the X-API-KEY header, covering key statuses, the roles a key carries, and both 403 responses.

All API requests must include an API key in the `X-API-KEY` header.

## How it works

1. An Admin generates an API key from the DualEntry dashboard and assigns it one or more roles
2. Include the API key in the `X-API-KEY` header with every request
3. The API validates the key, binds the request to that key's organization, and authorizes each endpoint against the key's roles

Requests are authorized in two stages, which is why there are two distinct failure messages. The key is either valid or it is not, and a valid key either carries the roles an endpoint requires or it does not. The key also identifies the organization, so requests carry no separate tenant or organization parameter. A key can never read data belonging to another organization.

Nothing else is required to identify yourself. There is no token exchange, no refresh step, and no session to maintain, so the same header works on the first request and the millionth.

A request to the invoices collection looks like this:

```bash theme={null}
curl https://api.dualentry.com/public/v1/invoices \
  -H "X-API-KEY: your_api_key_here" \
  -H "Content-Type: application/json"
```

## Base URL and versions

All requests go to `https://api.dualentry.com`, whether they come from your production organization or a sandbox organization provisioned for testing. A key only ever authenticates against the organization it was issued from, so sandbox and production integrations share the same host: nothing about the request changes except which key you send.

Three mounts are served under this host:

| Mount         | Version | Status                                                       |
| ------------- | ------- | ------------------------------------------------------------ |
| `/public/v2/` | 2.0.0   | Current. Use this for new integrations                       |
| `/public/v1/` | 1.0.0   | Supported                                                    |
| `/public/`    | 1.0.0   | Deprecated, unversioned. Kept for existing integrations only |

The same key works against every mount. Choosing a version is a URL decision, not a credential one, so moving an integration from V1 to V2 needs no new key and no change to the header.

## Getting an API key

An Admin in your organization issues keys from **Settings → Organization → Developer access → API Keys**. Only the Admin role can reach that tab. See [How to Give an Integration Access to DualEntry](/developers/guides/api/api-keys/how-to-give-an-integration-access) for the procedure, or send that page to whoever holds the role.

A key carries its own roles, assigned when it is created. It is not tied to the user who created it and does not inherit that user's permissions or entity scoping, and it reaches every company in the organization. An Admin can change a key's roles later without invalidating the secret, so a key that turns out to be too narrow does not require a redeploy.

Once you receive your key, store it securely in your application. The key is shown only once at creation time and cannot be retrieved later.

<Warning>
  **Security**: Never expose API keys in client-side code, public repositories, or logs. Use environment variables or secure secret management services.
</Warning>

## Key format and statuses

After creation, a key is only ever shown masked: the prefix, four asterisks, and the last four characters of the secret.

```text theme={null}
org_production_18e3****cSk8
```

The prefix reads `org_production_` for every customer-issued key, including keys for a sandbox organization. Sandbox and production organizations both run on the same infrastructure, so the prefix doesn't distinguish them. Use the organization name shown in the API Keys table, not the key prefix, to tell which organization a key belongs to.

A key holds one of two statuses:

| Status    | Effect on requests                                                               |
| --------- | -------------------------------------------------------------------------------- |
| `Active`  | Authenticates normally, subject to the key's roles                               |
| `Revoked` | Rejected on every request, indistinguishable from an unknown key in the response |

Revocation is immediate and one-way. A revoked key cannot be reactivated, and its name stays reserved, so a replacement needs a different one. Keys do not expire on their own; an Active key stays valid until someone revokes it.

For the columns shown in the API Keys table, see [Developer access](/accountants/platform-configuration/organization-settings/developer-access).

## Authentication errors

Both failure modes return `403`. The status code does not distinguish them; the error message does.

| Status Code | Error message                   | Meaning                                                                      | Solution                                                                  |
| ----------- | ------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| **403**     | `API key authentication failed` | The `X-API-KEY` header is missing or empty, or the key is unknown or revoked | Confirm the header is being sent and that the key's status reads `Active` |
| **403**     | `Access denied`                 | The key authenticated, but its roles do not permit this operation            | Ask an Admin to add the required roles to the key                         |

**Error response:**

```json theme={null}
{
  "success": false,
  "errors": {
    "__all__": ["API key authentication failed"]
  }
}
```

A key created with no roles authenticates successfully and then returns `Access denied` on every permission-guarded endpoint.

A revoked key is indistinguishable from an unknown one in the response; both read `API key authentication failed`. Requests refused for exceeding a quota return `429` instead, which is covered in [Rate limiting](/developers/guides/api/core-concepts/rate-limiting).

## Best practices

* Store API keys in environment variables or secret management services
* Use a separate key for a sandbox organization if you have one, so test traffic and production traffic are never authenticated by the same credential
* Issue one key per integration, so revoking one never takes down another
* Assign the narrowest roles the integration needs. A key holding Admin bypasses permission checks entirely, and nothing in the key table records what it was meant to do
* Rotate a key after any suspected exposure, or when its roles no longer match what the integration needs
* Never commit API keys to version control

Check the **Last Used** column periodically and revoke any key whose integration has been retired; a key stays valid until someone revokes it, with no expiry. Actions taken with a key appear in the audit trail attributed to **API Key** rather than to a person, so a key shared between systems produces a history nobody can untangle after the fact. One key per integration is what keeps that record readable.

## Next steps

* [How to Give an Integration Access to DualEntry](/developers/guides/api/api-keys/how-to-give-an-integration-access) for issuing and scoping a key
* [How to Authenticate Your Requests to the DualEntry API](/developers/guides/api/api-keys/how-to-authenticate-your-requests) for a working first call
* [How to Replace an API Key](/developers/guides/api/api-keys/how-to-replace-an-api-key) for rotation and leaked keys
* [Rate limiting](/developers/guides/api/core-concepts/rate-limiting)
