API reference
Plain JSON over HTTPS at https://openckey.org. This page lists every public endpoint, what it accepts and returns, its status codes and its limits.
Conventions
- Requests with a body send
Content-Type: application/json. Other types are refused with415, and bodies that aren’t valid JSON with400. - Errors are JSON objects of the form
{"error": "…"}with a sentence that says what failed. - Caching: dynamic responses carry
Cache-Control: no-store, except the telemetry summary, which may be cached for five minutes. - Rate limits are counted per client over a 60-second window and answer
429when exceeded. The client’s IP address is used as the key and is never stored. - Identifiers are seven URL-safe Base64 characters (
A–Z a–z 0–9 - _). Hosted text and short links have separate namespaces, and an identifier is never reused. - Verification: creating a destination and filing a report need a Cloudflare Turnstile token issued on openckey.org, so these two endpoints are meant for the site’s own forms.
| Endpoint | Purpose | Limit |
|---|---|---|
POST /api/entries | Create hosted text or a short link | 5 a minute |
GET /api/status/{kind}/{id} | Check a destination’s status | — |
GET /text/{id}, /sq/{id} | Open hosted text, follow a short link | — |
POST /api/reports | Report a destination | 5 a minute |
POST /api/telemetry/v1/batch | Receive an SDK telemetry batch | 10 a minute, plus a daily cap |
GET /api/telemetry/v1/summary | Read summed telemetry | — |
GET /api/config | Public configuration for the site’s forms | — |
Destinations
POST /api/entries
Creates hosted text (kind: "text") or a short link (kind: "sq"). It needs a Turnstile token for the action create_entry, so it is the API behind the form on the home page.
{
"kind": "text",
"body": "Hello",
"style": { "theme": "cream", "font": "serif", "size": "medium", "width": "comfortable", "align": "left" },
"expiry": "7d",
"turnstileToken": "…"
}
{
"kind": "sq",
"destination": "https://example.org/page",
"expiry": "30d",
"expiryBehavior": "root",
"turnstileToken": "…"
}
| Field | Rule |
|---|---|
kind | text or sq. |
body | Text only. 1 to 100,000 characters, stored exactly as written. |
style | Text only, optional. Omit it for literal text/plain. Fixed values only: theme paper, cream, sage or night; font sans, serif or mono; size small, medium or large; width narrow, comfortable or wide; align left or center. No creator HTML or CSS ever runs. |
destination | Short links only. A public https: URL with a domain name, at most 2,048 characters. IP addresses, local names, user names or passwords, ports other than 443, internationalised (punycode) host names and openckey.org itself are refused. |
expiry | Optional: 1d, 7d or 30d. Without it, the record lasts up to 24 months before retirement. |
expiryBehavior | Short links with an expiry. root, the default, sends visitors to the destination’s home page after expiry, and that page is safety-scanned too. message shows “The creator of this link has revoked redirects”. |
turnstileToken | Required. A Turnstile token for create_entry on openckey.org. |
A successful request answers 201. Hosted text is active at once; a short link is pending until its safety scan comes back clean.
{
"kind": "sq",
"id": "Ab3_Xy7",
"status": "pending",
"url": "https://openckey.org/sq/Ab3_Xy7",
"expiresAt": "2026-12-05T10:00:00.000Z"
}
| Code | Meaning |
|---|---|
| 201 | Created. |
| 400 | A field is missing or outside its rule; the message names it. |
| 403 | Turnstile verification failed. Get a new token and retry. |
| 413 | The body is larger than 120,000 bytes. |
| 415 | The body isn’t JSON. |
| 429 | More than 5 attempts in a minute. Wait and retry. |
| 503 | Creation or the safety scanner is unavailable, or no identifier could be allocated. Retry later. |
GET /api/status/{kind}/{id}
Returns a destination’s status without following it. kind is text or sq. The link status page uses it.
curl https://openckey.org/api/status/sq/Ab3_Xy7
{ "status": "active", "expiresAt": null, "preserveUntil": "2028-11-05 10:00:00" }
| Status | Meaning |
|---|---|
pending | A short link waiting for its safety scan. It doesn’t redirect yet. |
active | Working. |
blocked | Stopped by a failed scan or revoked after review. It never reactivates. |
expired | Past its chosen expiry. |
retired | Past its 24 months. The content is erased; only the reserved identifier remains. |
expiresAt is the chosen expiry or null, and preserveUntil is when the record retires. An unknown identifier answers 404.
GET /text/{id} and GET /sq/{id}
These are the addresses a CKey carries. Both accept GET and HEAD; other methods answer 405.
| Code | When |
|---|---|
| 200 | /text: the text as text/plain, or as a minimal reading page when the creator chose a style. |
| 302 | /sq: a redirect to the destination once its scan is clean. After an expiry with root behaviour, a redirect to the destination’s home page. |
| 503 | /sq: the safety scan is still pending, with Retry-After: 120. |
| 410 | Blocked, expired or retired. A retired record says “This record has been retired”; an expired short link with message behaviour says “The creator of this link has revoked redirects”. |
| 404 | No record has this identifier. |
Records can’t be edited. To change text or a destination, create a new record and a new CKey.
Reports
POST /api/reports
Reports hosted text or a short link for review. The report form supplies the Turnstile token, for the action report_entry. No IP address or other detail about the reporter is stored.
{ "kind": "sq", "id": "Ab3_Xy7", "reason": "phishing", "details": "Asks for a bank password", "turnstileToken": "…" }
reasonis one ofphishing,malware,spam,illegalorother.detailsis optional, at most 1,000 characters. It is erased after 24 months.- No other fields are accepted, and the body may be at most 8,192 bytes.
{ "reportId": "6f1c2b9e-0d7a-4c55-9b1e-2a8f3d4c5e60", "received": true }
Other answers: 400 for an invalid route, reason or field; 403 when verification fails; 404 when the identifier doesn’t exist; 413, 415; 429 after 5 reports in a minute; 503 when reporting is unavailable.
Telemetry
Both telemetry endpoints allow cross-origin requests (Access-Control-Allow-Origin: *) and answer OPTIONS preflights. Telemetry explains what is counted and why.
POST /api/telemetry/v1/batch
Receives one aggregate batch, schema 1, from an SDK. The body must be application/json (optionally with charset=utf-8) and at most 4,096 bytes.
| Field | Rule |
|---|---|
schema | Exactly 1. |
sdk | c, swift, android, web or dotnet. |
sdk_version | Matches 0.N.N, optionally followed by -build.N or -dev.N. |
platform | ios, macos, android, web, windows, linux or other. |
day | A UTC date, YYYY-MM-DD, from 35 days ago to 2 days ahead. |
sessions | Exactly started, succeeded and failed. |
frames | An integer. |
decoded | Exactly p0, p0_1, p1, p2 and p3. |
time_to_decode_ms, frame_ms | Exactly count, sum and buckets: 10 integers that add up to count. |
Every count is an integer from 0 to 10,000,000, and a sum from 0 to 1012. Unknown fields at any depth, duplicate keys and non-integer numbers are refused. Histogram buckets end at 25, 50, 100, 200, 400, 800, 1,600, 3,200 and 6,400 ms for time to decode, and at 2, 5, 10, 20, 40, 80, 160, 320 and 640 ms per frame; the last bucket holds everything above.
| Code | Meaning | SDK behaviour |
|---|---|---|
| 204 | Accepted and added to the daily totals. | Clear the sent counts. |
| 400 | Not a valid schema 1 batch; the message names the field. | Drop the batch. |
| 413 | Larger than 4,096 bytes. | Drop the batch. |
| 415 | Not application/json. | Drop the batch. |
| 429 | More than 10 batches a minute from one sender (Retry-After: 60), or the day’s cap is reached (Retry-After: 3600). | Keep it for later. |
| 503 | Telemetry is unavailable. | Keep it for later. |
GET /api/telemetry/v1/summary
Returns summed aggregates for the last days days, from 1 to 400; the default is 30. Any other value answers 400. Responses may be cached for 300 seconds, and HEAD is accepted.
curl "https://openckey.org/api/telemetry/v1/summary?days=30"
{
"label": "Reported by SDKs with telemetry on; not total usage, unique users, or worldwide scans.",
"methodology": "Sums of anonymous aggregate batches (schema 1) …",
"days": 30, "from": "2026-10-07", "to": "2026-11-07",
"totals": {
"batches": 0,
"sessions": { "started": 0, "succeeded": 0, "failed": 0 },
"frames": 0,
"decoded": { "p0": 0, "p0_1": 0, "p1": 0, "p2": 0, "p3": 0 },
"time_to_decode_ms": { "count": 0, "sum": 0, "buckets": [0, …], "bucket_upper_edges": [25, 50, …, null] },
"frame_ms": { "count": 0, "sum": 0, "buckets": [0, …], "bucket_upper_edges": [2, 5, …, null] }
},
"by_sdk": [{ "sdk": "web", "batches": 0, "sessions_started": 0, "sessions_succeeded": 0, "sessions_failed": 0 }],
"by_platform": [{ "platform": "web", "…": "…" }],
"by_day": [{ "day": "2026-11-05", "…": "…" }]
}
to runs two days past today, because batches may be dated slightly ahead of the server clock. Always show the label with any figure you publish.
GET /api/config
Returns the public Turnstile site key the site’s forms use: {"turnstileSiteKey": "…"}, or null when it isn’t configured.
Operator endpoints
These are for the site’s operator only. They need Authorization: Bearer with the operator’s token and answer 401 without it. They are listed so that what an operator can do is public.
POST /api/admin/revokeblocks a record. It never deletes or reassigns the identifier, and a retired record is left as it is.POST /api/admin/extendextends a record’s preservation by 1 to 24 months, with a written reason of 10 to 500 characters that is logged. A retired record cannot be restored.GET /api/admin/reportslists reports that are open, reviewed or all.POST /api/admin/reports/reviewdismisses a report or revokes the reported record.