Authdog answers authorization questions over the OpenID Foundation's [AuthZEN Authorization API 1.0](https://openid.github.io/authzen/). Your application acts as the Policy Enforcement Point (PEP) and calls Authdog as the Policy Decision Point (PDP): send a subject, an action, and a resource; get back permit or deny.

Reach for AuthZEN when you want a vendor-neutral wire contract, when a gateway or sidecar in your stack already speaks it, or when you want one decision endpoint no matter which model — RBAC, ABAC, or FGA — the environment enforces.

## Endpoints

The PDP is mounted at the API origin root, outside the `/v1` base path, because the specification fixes these paths and interop tooling probes them verbatim.

| Endpoint | Purpose |
| --- | --- |
| `POST /access/v1/evaluation` | One decision for one subject, action, and resource. |
| `POST /access/v1/evaluations` | Boxcarred batch of up to 100 evaluations. |
| `POST /access/v1/search/subject` | Who can perform this action on this resource? |
| `POST /access/v1/search/resource` | What resources can this subject act on? |
| `POST /access/v1/search/action` | What actions can this subject perform on this resource? |
| `GET /.well-known/authzen-configuration` | Discovery document. Unauthenticated, per section 9.1. |

Discovery advertises all five endpoints and honors `X-Forwarded-Host` and `X-Forwarded-Proto`, so the advertised URLs match the origin your PEP actually calls.

```bash
curl https://api.authdog.com/.well-known/authzen-configuration
```

## Authentication

Every endpoint except discovery is authenticated with an environment API secret (`adenv_...`) as a bearer token. The token alone identifies the environment whose policies decide the request, so no tenant or environment appears in the path.

```bash
curl -X POST "https://api.authdog.com/access/v1/evaluation" \
  -H "Authorization: Bearer $AUTHDOG_ENVIRONMENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "subject":  { "type": "user", "id": "usr_123" },
    "action":   { "name": "can_read" },
    "resource": { "type": "document", "id": "roadmap" }
  }'
```

The response is the spec's decision shape:

```json
{
  "decision": true,
  "context": {
    "reason_admin": { "reason_code": "role_grant" },
    "reason_user": { "en": "Permitted by an assigned role." }
  }
}
```

The environment secret is a PEP credential, not an end-user token. Keep it server-side; never ship it to a browser or mobile client.

## How decisions are made

The PDP dispatches on the environment's permission model, reusing the same engines that enforce access inside Authdog — a decision served over AuthZEN can never disagree with what Authdog enforces internally.

- **RBAC**: the action is normalized to a CRUD verb and checked against the subject's role grants, including roles inherited through groups.
- **ABAC**: the request is converted into policy input and evaluated against the environment's enabled policies.
- **FGA**: the action is mapped onto a relation and checked against the environment's authorization model and relationship tuples.

See [Authorization concepts](/docs/concepts/authorization) for how to choose a model, and [Fine-grained authorization](/docs/fga) for the relationship model.

### Action names

AuthZEN leaves the action namespace to the PDP. Authdog normalizes action names onto four coarse verbs — `create`, `read`, `update`, `delete` — so PEPs can send whichever dialect they already use:

| Sent by the PEP | Normalized verb |
| --- | --- |
| `create`, `can_create`, `insert`, `add`, `write`, `can_write`, `POST` | `create` |
| `read`, `can_read`, `view`, `can_view`, `GET`, `list`, `HEAD`, `search` | `read` |
| `update`, `can_update`, `edit`, `can_edit`, `modify`, `PUT`, `PATCH` | `update` |
| `delete`, `can_delete`, `remove`, `destroy` | `delete` |

An unrecognized action name falls back to `read`, the least privileged verb. That means an unknown action can never widen access — but it also won't grant what you intended, so keep your PEP on a name from the table.

### Subjects

Subject types `user`, `identity`, and `subject` resolve against the environment's directory, matched by user id or external id. A subject that cannot be resolved is denied with reason code `subject_not_found`; a resolved but deactivated user is denied with `subject_inactive`.

Reason codes are stable and safe to log: `role_grant`, `no_matching_role`, `policy_allow`, `policy_deny`, `no_policies`, `relation_holds`, `relation_missing`, `subject_not_found`, `subject_inactive`, and `evaluation_error`.

## Batching

`POST /access/v1/evaluations` evaluates up to 100 items in one request. Top-level `subject`, `action`, `resource`, and `context` act as defaults; each item overrides only what it sets.

```json
{
  "subject": { "type": "user", "id": "usr_123" },
  "resource": { "type": "document", "id": "roadmap" },
  "evaluations": [
    { "action": { "name": "can_read" } },
    { "action": { "name": "can_update" } },
    { "action": { "name": "can_delete" } }
  ],
  "options": { "evaluations_semantic": "deny_on_first_deny" }
}
```

Supported semantics are `execute_all` (the default), `deny_on_first_deny`, and `permit_on_first_permit`. When a batch short-circuits, the remaining entries are filled with the short-circuit decision rather than omitted, so the response array always lines up with the request array.

A batch shares one environment lookup and memoizes subject and grant reads, so evaluating 50 items costs a handful of database round trips, not 50.

## Search

The search endpoints answer the reverse questions — enumeration rather than a yes/no. Results are paginated forward-only: pass the `next_token` from a response's `page` block to fetch the next page.

```bash
curl -X POST "https://api.authdog.com/access/v1/search/resource" \
  -H "Authorization: Bearer $AUTHDOG_ENVIRONMENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": { "type": "user", "id": "usr_123" },
    "action":  { "name": "can_read" },
    "resource": { "type": "document" }
  }'
```

Search evaluates every candidate it enumerates, so it costs far more than a single evaluation. Use it for admin surfaces and access reviews, not on a hot request path.

## Failure behavior

The PDP is fail-closed. An unresolvable subject, an unknown permission model, or an internal error produces `decision: false` with a reason — never a partial permit.

| Status | Meaning |
| --- | --- |
| `400` | Malformed JSON or a body that fails spec validation. |
| `401` | Missing or invalid environment API secret. |
| `502` | The decision point was unreachable or returned no decision. |

Errors use one envelope on every endpoint, so a PEP can log a single shape:

```json
{ "error": { "status": 400, "message": "subject.id is required" } }
```

A `502` means no decision was made. Your PEP must treat it as a deny, not as a permit.

## Related

- [Authorization concepts](/docs/concepts/authorization)
- [Roles and permissions](/docs/permissions)
- [Fine-grained authorization](/docs/fga)
- [API reference](/docs/api)
