# Turing Verify REST API — v1

Base URL: **`https://api.verify.turingcerts.com`**
Protocol: HTTPS + JSON. All timestamps are RFC-3339 UTC (`YYYY-MM-DDTHH:MM:SSZ`).

This page is the canonical reference for the `/v1` public API. It is served
at `/v1/docs.md` (raw markdown) and rendered as HTML at `/v1/docs/reference`.
For the OpenAPI 3.1 schema use `/v1/openapi.json`. For the Model Context
Protocol connection, see `/mcp/docs`.

- [Authentication](#authentication)
- [Environments: test vs live](#environments)
- [Error envelope](#error-envelope)
- [Rate limits](#rate-limits)
- [Idempotency](#idempotency)
- [Pagination](#pagination)
- [Endpoints](#endpoints)
  - [POST /v1/verifications](#post-v1verifications)
  - [GET /v1/verifications/{id}](#get-v1verificationsid)
  - [GET /v1/verifications](#get-v1verifications)
  - [POST /v1/verifications/batch](#post-v1verificationsbatch)
  - [POST /v1/webhooks](#post-v1webhooks)
  - [GET /v1/webhooks](#get-v1webhooks)
  - [GET /v1/account](#get-v1account)
  - [GET /v1/account/usage](#get-v1accountusage)
- [Webhooks](#webhooks)
- [MCP](#mcp)
- [SDKs](#sdks)

## Authentication

Every request must carry a bearer token:

```
Authorization: Bearer tv_live_sk_<secret>
```

Tokens are minted from the Developer Console (`/developers/tokens`). Prefixes:

- `tv_live_sk_...` — production, charges credits.
- `tv_test_sk_...` — deterministic, zero-credit.

## Environments

- **Test mode** — tokens starting with `tv_test_sk_`. Deterministic verdicts
  seeded from `sha256(bytes) % 4`, no model calls, zero credits. Use in CI and
  for offline integration tests.
- **Live mode** — tokens starting with `tv_live_sk_`. Runs the full forensic
  pipeline, charges 1 credit per verification (2 for deep mode). Response
  headers carry `X-Credits-Remaining` and, when quota crosses 80 %,
  `X-Credits-Warning: approaching-quota`.

The server echoes the mode in `X-Turing-Environment: test|live` on every
response.

## Error envelope

Non-2xx responses always have this shape:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Human-friendly description.",
    "request_id": "req_01HTN8Z9MJB00000000000000",
    "docs_url": "https://api.verify.turingcerts.com/v1/docs#errors"
  }
}
```

Common `code` values:

| `code`                   | HTTP | Meaning                                         |
| ------------------------ | ---- | ----------------------------------------------- |
| `authentication_required`| 401  | Missing or malformed `Authorization` header.    |
| `invalid_api_key`        | 401  | Token unknown, revoked, or expired.             |
| `insufficient_scope`     | 403  | Token is valid but lacks the required scope.    |
| `plan_required`          | 403  | Endpoint is gated to Pro / Business.            |
| `not_found`              | 404  | The resource does not exist (or is not yours).  |
| `idempotency_conflict`   | 409  | `Idempotency-Key` reused with a different body. |
| `invalid_request`        | 400  | Validation failed (see `message`).              |
| `rate_limited`           | 429  | Token bucket exhausted or quota depleted.       |
| `internal_error`         | 500  | Server problem; retry with exponential backoff. |

`X-Request-ID` is also sent as a response header — include it when filing
support tickets.

## Rate limits

Per-token, token-bucket scheme. Headers on every response:

```
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 597
X-RateLimit-Reset: 42
```

- `Limit` — sustained requests/minute for your tier.
- `Remaining` — requests left in the current window.
- `Reset` — seconds until the window resets.

Tiers (defaults; contact sales for custom):

| Plan     | Burst | Sustained (req/min) |
| -------- | ----- | ------------------- |
| Pro      | 20    | 600                 |
| Business | 40    | 3000                |

When you breach the limit you get `429 rate_limited` with a `Retry-After`
header (seconds).

## Idempotency

`POST /v1/verifications` supports an `Idempotency-Key` header. Replays
within 24 hours return the cached response with
`Idempotency-Status: replayed`. Reusing the same key with a different body
returns `409 idempotency_conflict`.

Every official SDK generates a UUID4 key automatically — you only need to
set this header by hand if you're calling the API directly.

## Pagination

All list endpoints use cursor pagination. Query params:

- `limit` — page size, 1–100 (default 20).
- `starting_after` — opaque cursor from the previous page's
  `next_cursor`.

Response shape:

```json
{
  "object": "list",
  "data": [ ... ],
  "has_more": true,
  "next_cursor": "ver_01HTN..."
}
```

## Endpoints

### POST /v1/verifications

Submit a document for verification. Accepts a multipart upload (`file`) or a
JSON body with `document_url` or `document_base64` (+ `content_type`).

**Request (multipart)**

```bash
curl -X POST https://api.verify.turingcerts.com/v1/verifications \
  -H "Authorization: Bearer tv_live_sk_..." \
  -F "file=@diploma.pdf"
```

**Response 200**

```json
{
  "id": "ver_01HTN...",
  "status": "completed",
  "verdict": "ACCEPT",
  "confidence": 0.94,
  "doc_type": "diploma",
  "livemode": true,
  "created_at": "2026-04-17T09:12:33Z",
  "poll_url": "/v1/verifications/ver_01HTN..."
}
```

### GET /v1/verifications/{id}

Fetch a previously submitted verification.

```bash
curl https://api.verify.turingcerts.com/v1/verifications/ver_01HTN... \
  -H "Authorization: Bearer tv_live_sk_..."
```

404 if the id is unknown **or** the verification belongs to another user —
we don't leak existence.

### GET /v1/verifications

Cursor-paginated list of your verifications, newest first.

Query params: `limit`, `starting_after`, `verdict` (optional filter).

### POST /v1/verifications/batch

Business-tier only. Submit up to 1 000 documents in a single request.

```json
{
  "documents": [
    { "base64": "JVBERi...", "content_type": "application/pdf" },
    { "url": "https://example.com/cert.pdf" }
  ]
}
```

### POST /v1/webhooks

Register a webhook endpoint. Returns the signing secret **once**.

```json
{
  "url": "https://example.com/hooks/turing-verify",
  "events": ["verification.completed", "verification.failed"],
  "description": "Prod ATS ingestion"
}
```

### GET /v1/webhooks

List endpoints you own.

### GET /v1/account

```json
{
  "user_id": "usr_01HTN...",
  "email": "alice@example.com",
  "plan": { "tier": "pro", "api_access": true, "rate_limit_burst": 20, "rate_limit_sustained": 600 },
  "credits": { "included": 150, "used": 12, "remaining": 138, "reset_at": "2026-05-01T00:00:00Z", "overage_opt_in": false }
}
```

### GET /v1/account/usage

Month-to-date usage histogram.

Query params:

- `period` — `YYYY-MM`, defaults to the current calendar month.
- `group_by` — `day` (default), `endpoint`, or `token`.

## Webhooks

Events are delivered as HTTPS POSTs with this envelope:

```json
{
  "id": "evt_01HTN...",
  "type": "verification.completed",
  "api_version": "2026-04-01",
  "created": "2026-04-17T09:12:33Z",
  "data": { "verification": { ... } }
}
```

### HMAC signing

Every delivery includes:

```
X-TuringVerify-Signature: t=1713347553,v1=<hex digest>
```

The digest is computed as:

```
HMAC_SHA256(secret, f"{timestamp}.{raw_body}")
```

#### Python

```python
from turingverify import verify_signature, SignatureVerificationError

body = await request.body()
try:
    verify_signature(body, request.headers["X-TuringVerify-Signature"], SECRET)
except SignatureVerificationError:
    return 400
```

#### TypeScript

```ts
import { verifyWebhookSignature } from '@turingverify/sdk';

verifyWebhookSignature(
  req.rawBody,
  req.header('X-TuringVerify-Signature') ?? '',
  process.env.WEBHOOK_SECRET!,
);
```

#### Go (hand-rolled)

```go
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(fmt.Sprintf("%d.", ts)))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
```

### Retry ladder

Failed deliveries retry at
`0s, 30s, 2m, 10m, 30m, 2h, 6h, 12h, 24h` (nine attempts over ~45 h). After
the last failure the delivery row is marked `permanently_failed`; the
developer portal lets you replay one manually.

### Event list (launch set)

- `verification.completed`
- `verification.failed`
- `verification.manual_review_required`
- `batch.completed`
- `token.rotated`
- `webhook.ping` (synthetic test fires)

## MCP

The Model Context Protocol surface lives at
**`https://mcps.verify.turingcerts.com/mcp`**. See `/mcp/docs` for the full
manual (5 tools, 3 resources, 2 prompts). One-click client manifests are
served at `/.well-known/mcp-config`.

## SDKs

- **Python** — `pip install turingverify` — source in
  [`./sdks/python`](./sdks/python).
- **TypeScript / Node** — `npm install @turingverify/sdk` — source in
  [`./sdks/node`](./sdks/node).

Both SDKs wrap the endpoints documented here, handle retries, inject an
auto-generated `Idempotency-Key`, and ship a `verify_signature` /
`verifyWebhookSignature` helper for inbound webhooks.
