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 with 415, and bodies that aren’t valid JSON with 400.
  • 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 429 when 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.
Endpoints at a glance
EndpointPurposeLimit
POST /api/entriesCreate hosted text or a short link5 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/reportsReport a destination5 a minute
POST /api/telemetry/v1/batchReceive an SDK telemetry batch10 a minute, plus a daily cap
GET /api/telemetry/v1/summaryRead summed telemetry—
GET /api/configPublic 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": "…"
}
Fields
FieldRule
kindtext or sq.
bodyText only. 1 to 100,000 characters, stored exactly as written.
styleText 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.
destinationShort 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.
expiryOptional: 1d, 7d or 30d. Without it, the record lasts up to 24 months before retirement.
expiryBehaviorShort 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”.
turnstileTokenRequired. 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"
}
Status codes
CodeMeaning
201Created.
400A field is missing or outside its rule; the message names it.
403Turnstile verification failed. Get a new token and retry.
413The body is larger than 120,000 bytes.
415The body isn’t JSON.
429More than 5 attempts in a minute. Wait and retry.
503Creation 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" }
Statuses
StatusMeaning
pendingA short link waiting for its safety scan. It doesn’t redirect yet.
activeWorking.
blockedStopped by a failed scan or revoked after review. It never reactivates.
expiredPast its chosen expiry.
retiredPast 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.

CodeWhen
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.
410Blocked, 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”.
404No 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": "…" }
  • reason is one of phishing, malware, spam, illegal or other.
  • details is 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.

Batch fields
FieldRule
schemaExactly 1.
sdkc, swift, android, web or dotnet.
sdk_versionMatches 0.N.N, optionally followed by -build.N or -dev.N.
platformios, macos, android, web, windows, linux or other.
dayA UTC date, YYYY-MM-DD, from 35 days ago to 2 days ahead.
sessionsExactly started, succeeded and failed.
framesAn integer.
decodedExactly p0, p0_1, p1, p2 and p3.
time_to_decode_ms, frame_msExactly 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.

Status codes
CodeMeaningSDK behaviour
204Accepted and added to the daily totals.Clear the sent counts.
400Not a valid schema 1 batch; the message names the field.Drop the batch.
413Larger than 4,096 bytes.Drop the batch.
415Not application/json.Drop the batch.
429More 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.
503Telemetry 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/revoke blocks a record. It never deletes or reassigns the identifier, and a retired record is left as it is.
  • POST /api/admin/extend extends 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/reports lists reports that are open, reviewed or all.
  • POST /api/admin/reports/review dismisses a report or revokes the reported record.