{"openapi":"3.1.0","info":{"title":"HamCaptcha API","description":"Standalone CAPTCHA service using ham radio callsigns. The client page embeds the widget (/v2/api.js) with a public projectKey — the widget shows a callsign in a PNG image, the user transcribes it and the widget obtains a single-use token. The project backend then verifies the token at /v2/verify with its secret. Tokens are valid for 300 seconds and can be verified only once.","version":"2.0"},"servers":[{"url":"https://hamcaptcha.ok1xoe.dev","description":"Generated server url"}],"tags":[{"name":"Widget","description":"Public widget API (projectKey authentication)"},{"name":"Verify","description":"Server-side token validation (secret authentication in the request body)"}],"paths":{"/v2/verify":{"post":{"tags":["Verify"],"summary":"Verify and consume a token (JSON)","description":"Request fields: secret (project secret key), token (from the widget), optionally idempotency_key (UUID for safe retries — the same key returns the original result instead of token-already-used). A token can be verified only once and is valid for 300 seconds after the challenge was solved. The response is always HTTP 200; the result is in the success field, failure reasons in errors.","operationId":"verifyForm","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/MultiValueMapStringString"}},"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/VerifyResponse"}}}}}}},"/v2/challenge":{"post":{"tags":["Widget"],"summary":"Issue a new CAPTCHA challenge for the widget","description":"Called by the client widget (api.js) with the public projectKey. Returns a single-use challenge id and a PNG image with a callsign as a data URL. An invalid projectKey returns success=false with code invalid-projectkey (HTTP 200).","operationId":"challenge","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetChallengeRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/WidgetChallengeResponse"}}}}}}},"/v2/answer":{"post":{"tags":["Widget"],"summary":"Evaluate the user's answer and issue a token on success","description":"Called by the client widget after the user transcribes the text. The challenge is always consumed by the answer. A correct answer returns a single-use token (valid 300 s) that the client page sends to its backend for verification at /v2/verify. A wrong answer returns wrong-answer; an unknown or expired challenge returns invalid-challenge.","operationId":"answer","parameters":[{"name":"Origin","in":"header","required":false,"schema":{"type":"string"}},{"name":"Referer","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetAnswerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/WidgetAnswerResponse"}}}}}}}},"components":{"schemas":{"MultiValueMapStringString":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"properties":{"all":{"type":"object","additionalProperties":{"type":"string"},"writeOnly":true},"empty":{"type":"boolean"}}},"VerifyResponse":{"type":"object","description":"Result of server-side token validation","properties":{"success":{"type":"boolean","description":"true = the token is valid and was just consumed"},"solved_at":{"type":"string","description":"Time the challenge was solved (ISO 8601)","example":"2026-06-11T15:14:30.096Z"},"hostname":{"type":"string","description":"Hostname of the page where the challenge was solved (from the Origin header)"},"errors":{"type":"array","description":"Error codes: missing-secret, invalid-secret, missing-token, invalid-token, token-already-used, bad-request, internal-error","items":{"type":"string"}},"action":{"type":"string","description":"Action identifier passed by the widget (data-action)"},"metadata":{"type":"string","description":"Client data passed by the widget (data-metadata)"}}},"WidgetChallengeRequest":{"type":"object","description":"Widget request for a new CAPTCHA challenge","properties":{"projectKey":{"type":"string","description":"Public projectKey of the project (from the widget's data-projectkey attribute)","example":"hc-pub-3f2a1c9d8e7b4a5f9c0d1e2f3a4b5c6d","minLength":1}},"required":["projectKey"]},"WidgetChallengeResponse":{"type":"object","description":"Generated CAPTCHA challenge for the widget","properties":{"success":{"type":"boolean","description":"false = the challenge could not be issued (see errors)"},"challengeId":{"type":"string","description":"Single-use challenge identifier"},"imageDataUrl":{"type":"string","description":"PNG image with the challenge text as a data URL"},"expiresInSeconds":{"type":"integer","format":"int32","description":"Challenge validity in seconds"},"errors":{"type":"array","description":"Error codes (e.g. invalid-projectkey)","items":{"type":"string"}}}},"WidgetAnswerRequest":{"type":"object","description":"User's answer to the CAPTCHA challenge","properties":{"projectKey":{"type":"string","description":"Public projectKey of the project","minLength":1},"challengeId":{"type":"string","description":"Challenge identifier from the /v2/challenge response","minLength":1},"answer":{"type":"string","description":"Text the user transcribed from the image","example":"OK1XOE","minLength":1},"action":{"type":"string","description":"Optional action identifier (widget data-action); returned in the validation result","maxLength":32,"minLength":0,"pattern":"[a-zA-Z0-9_-]*"},"metadata":{"type":"string","description":"Optional client data (widget data-metadata); returned in the validation result","maxLength":255,"minLength":0}},"required":["answer","challengeId","projectKey"]},"WidgetAnswerResponse":{"type":"object","description":"Result of the answer to the CAPTCHA challenge","properties":{"success":{"type":"boolean","description":"true = correct answer, token issued"},"token":{"type":"string","description":"Single-use token for server-side validation (valid 300 s)"},"errors":{"type":"array","description":"Error codes (wrong-answer, invalid-challenge, invalid-projectkey)","items":{"type":"string"}}}}}}}