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

# Unlock Page

> Verify a page passcode and receive a short-lived content token.

## `POST /v1/public/pages/{slug}/unlock`

Verifies the password for a gated page and returns a short-lived content token. The token is passed to the content-origin Worker (`artifactausercontent.com/c/{slug}?token=...`) to fetch the gated bytes.

This endpoint is **unauthenticated** and is called by the Artifacta viewer after the visitor submits the password prompt.

The token expires in **5 minutes** (`expires_in: 300`). After expiry the viewer must call this endpoint again.

## Authentication

None. This is a public endpoint.

## Path parameters

| Parameter | Type   | Description                               |
| --------- | ------ | ----------------------------------------- |
| `slug`    | string | Page slug (`pg_...`) from the public URL. |

## Request body

```json theme={null}
{
  "password": "hunter2"
}
```

| Field      | Type   | Required | Description                                   |
| ---------- | ------ | -------- | --------------------------------------------- |
| `password` | string | Yes      | The passcode set when the page was published. |

## Response

```json theme={null}
{
  "content_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_in": 300
}
```

| Field           | Type    | Description                                                                                                                            |
| --------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `content_token` | string  | Signed JWT (HS256). Carries `slug`, `purpose: "page_content"`, and `exp`. Pass this as a query parameter to the content-origin Worker. |
| `expires_in`    | integer | Token lifetime in seconds. Always `300`.                                                                                               |

## Error codes

| Code                 | Status | When                                                                                      |
| -------------------- | ------ | ----------------------------------------------------------------------------------------- |
| `artifact_not_found` | 404    | Page is missing, unpublished, expired, or access is not `"password"`. No oracle.          |
| `unauthorized`       | 401    | Password is incorrect. Same error code regardless of whether the slug exists (no oracle). |
| `rate_limited`       | 429    | Too many unlock attempts for this slug. Try again after the `Retry-After` header value.   |
| `invalid_request`    | 400    | Missing or empty `password` field, or malformed JSON body.                                |

## Notes

* **Single failure mode**: An incorrect password returns `unauthorized`, not `artifact_not_found`. However, a missing or unpublished slug also returns a non-200 status code — callers cannot distinguish wrong-password from missing-slug. This is intentional.
* **Brute-force protection**: Attempts are rate-limited per slug plus a separate per-IP edge rule. The per-slug cap is the primary control.
* **Token scope**: The token is single-purpose (`purpose: "page_content"`) and accepted only by the content-origin Worker. It cannot be used to call any API endpoint.

## Example

```bash theme={null}
curl -X POST https://api.artifacta.io/v1/public/pages/pg_aB3xK9mP1qR5sT2u/unlock \
  -H "Content-Type: application/json" \
  -d '{"password": "hunter2"}'
```
