# Captcha API

Raw markdown documentation for the worker running at https://c.bve.me.

## Overview

This service solves numeric captchas from image input.

Implementation notes:
- A single Cloudflare Worker serves the HTTP API, produces async jobs to a Cloudflare Queue, consumes that same queue, and runs the scheduled cleanup cron.
- Solves are cached in D1 by a SHA-256 hash of the normalized base64 image payload.
- The solver queries every configured vision model concurrently and reconciles their outputs by agreement, confidence, and model order.

## Base URL

```text
https://c.bve.me
```

## Public Endpoints

- `GET /` returns this markdown document with `Content-Type: text/markdown`.
- `GET /health` returns `200 {"status":"ok"}` when the worker is up.
- `GET /ready` probes the upstream vision API with `HEAD` and returns `200 {"success":true}` for any upstream response below `500`. It is a reachability check, not a credential-validation check.

## Authentication

Authentication is required for:
- `POST /solve-captcha`
- `POST /solve-captcha/async`
- `GET /status/:job_id`
- `GET /solve-captcha/status/:job_id`

Accepted auth headers:
- `Authorization: Bearer <AUTH_TOKEN>`
- `x-api-key: <AUTH_TOKEN>`

If both headers are present, the request is accepted when either token matches.

## Request Body Formats

Supported `Content-Type` values:
- `application/json`
- `multipart/form-data`

Accepted JSON image fields:
- `base64`
- `image_base64`
- `image`

Accepted multipart string image fields:
- `base64`
- `image_base64`
- `image`

Accepted multipart file fields:
- `image`
- `file`
- `captcha`

Input rules:
- Base64 may be raw or a data URL like `data:image/png;base64,...`.
- The worker normalizes `image/jpg` to `image/jpeg`.
- Supported image MIME types: `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `image/bmp`.
- The decoded image must be non-empty and no larger than 8 MiB.
- The request body is also capped by middleware before parsing; oversized bodies return `413`.

## POST /solve-captcha

Protected synchronous solve endpoint.

JSON example:

```json
{
  "base64": "iVBORw0KGgoAAAANSUhEUgAA..."
}
```

Multipart example:

```text
Content-Type: multipart/form-data
image=<binary file upload>
```

Success response:

```json
{
  "success": true,
  "captcha_text": "2468",
  "confidence": 1,
  "models_agreed": 2
}
```

Behavior:
- On a D1 cache hit, the worker returns the cached solve directly.
- On a cache miss, the worker calls every configured model concurrently.
- The winning result is written back into `captcha_cache` with confidence and vote metadata.

Common statuses:
- `200` solved or cache hit.
- `400` invalid JSON, invalid multipart payload, invalid base64, MIME mismatch, empty image, or unsupported image semantics.
- `401` missing or invalid auth token.
- `413` request body or decoded image too large.
- `415` unsupported `Content-Type`.
- `422` no digits were found or the ensemble could not produce a confident result.
- `502` upstream vision API network, protocol, or response-shape failure.
- `504` upstream vision API timeout.

## POST /solve-captcha/async

Protected asynchronous solve endpoint. It accepts the same image payloads as `POST /solve-captcha`, plus an optional `webhook_url`.

Async JSON example:

```json
{
  "base64": "iVBORw0KGgoAAAANSUhEUgAA...",
  "webhook_url": "https://hooks.example.com/captcha"
}
```

`webhook_url` rules:
- Optional.
- Must be an absolute `http` or `https` URL.
- Empty strings are treated as unset.

Submission behavior is ordered:
1. If `captcha_cache` already has a fresh result for the same normalized image, the API returns `200` with `status: "completed"` and `cached: true`.
2. Otherwise, if the latest async job for the same image hash is already `completed`, the API returns that completed result immediately.
3. Otherwise, if the latest async job for the same image hash is `pending` or `processing`, the API returns `202` with that existing `job_id`.
4. Otherwise, the API inserts a new `pending` D1 row, enqueues a queue message, and returns `202`.

Fresh async submission response:

```json
{
  "success": true,
  "job_id": "7f8c6fd4-7af7-4f34-a9a3-8bd0ec4b8109",
  "status": "pending"
}
```

Immediate completed response:

```json
{
  "success": true,
  "cached": true,
  "status": "completed",
  "captcha_text": "9090",
  "confidence": 1,
  "models_agreed": 1
}
```

Common statuses:
- `200` cache hit or already-completed equivalent job.
- `202` new or existing in-flight job.
- `400`, `401`, `413`, `415` for the same validation/auth issues as the sync endpoint, plus invalid `webhook_url`.
- `503` D1 or queue submission failure inside the async plane.

## GET /status/:job_id

Protected status endpoint for async jobs.

Pending or processing response:

```json
{
  "success": true,
  "job_id": "7f8c6fd4-7af7-4f34-a9a3-8bd0ec4b8109",
  "status": "processing"
}
```

Completed response:

```json
{
  "success": true,
  "job_id": "7f8c6fd4-7af7-4f34-a9a3-8bd0ec4b8109",
  "status": "completed",
  "captcha_text": "9090",
  "confidence": 1,
  "models_agreed": 1
}
```

Behavior notes:
- `captcha_text`, `confidence`, and `models_agreed` are only present when the job status is `completed`.
- Failed jobs return `200` with `status: "failed"`, but the API does not expose `last_error`.
- Unknown job IDs return `404` with the standard error envelope.
- If D1 lookup fails, the API returns `503`.

## GET /solve-captcha/status/:job_id

Protected alias of `GET /status/:job_id`. Both routes use the same handler and return the same payload shape.

## Completion Webhook

If `webhook_url` was provided on async submission and the queue consumer completes the solve successfully, the worker POSTs JSON to that URL.

Webhook payload:

```json
{
  "success": true,
  "status": "completed",
  "job_id": "7f8c6fd4-7af7-4f34-a9a3-8bd0ec4b8109",
  "request_id": "8b69a0dc-dc96-4b01-a8f9-4274bb1f5724",
  "captcha_text": "9090",
  "confidence": 1,
  "models_agreed": 1
}
```

Webhook delivery notes:
- The consumer uses a 10-second timeout for webhook delivery.
- Non-2xx webhook responses are logged but do not roll back the completed job.
- Failed jobs do not emit a webhook.

## Error Envelope

Errors are returned as JSON:

```json
{
  "success": false,
  "error": "Invalid authentication token.",
  "request_id": "8b69a0dc-dc96-4b01-a8f9-4274bb1f5724"
}
```

Error responses also include the `X-Request-Id` header.

## Solver Decision Model

The worker is intentionally stricter than a naive single-model OCR pass.

Decision process:
- The worker uses `MODELS` when present, otherwise it falls back to `MODEL`.
- Each model is queried concurrently against the same image.
- The worker extracts digit groups from each model response and joins them into a candidate digit string.
- Candidates are ranked by: more votes, then higher confidence, then earlier model order.
- A candidate is accepted when at least two models agree or when the best single candidate meets the configured confidence threshold.
- If some models fail but a surviving candidate still clears the acceptance rule, the request succeeds.
- If all model calls fail, the API surfaces an upstream failure instead of a synthetic empty result.

The default confidence threshold in this deployment is `0.8`.

## Async Lifecycle And Retention

Async job states: `pending` -> `processing` -> `completed` or `failed`.

Retention behavior in this deployment:
- `captcha_cache` TTL default: 7 days.
- `async_jobs` TTL default: 24 hours.
- The worker has an hourly cron that prunes expired cache and job rows from D1.

Queue behavior in this deployment:
- `max_batch_size = 1` so each queue invocation handles one solve.
- Transient failures can retry up to 3 times, with a 30-second retry delay.
- Deterministic 4xx solve failures are acknowledged immediately to avoid wasting retries.

## Quick cURL Examples

Synchronous solve:

```bash
curl https://c.bve.me/solve-captcha \
  -H "Authorization: Bearer <AUTH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d "{"base64":"<BASE64_IMAGE>"}"
```

Async solve:

```bash
curl https://c.bve.me/solve-captcha/async \
  -H "Authorization: Bearer <AUTH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d "{"base64":"<BASE64_IMAGE>","webhook_url":"https://hooks.example.com/captcha"}"
```

Status poll:

```bash
curl https://c.bve.me/status/<JOB_ID> \
  -H "Authorization: Bearer <AUTH_TOKEN>"
```