{"openapi":"3.1.0","info":{"title":"Askium External API","version":"1.0.0","description":"External API for server-to-server integrations with Askium. Protected endpoints use a per-user private API key sent as a Bearer token. The result verification endpoint is intentionally public and accepts a short-lived one-time verification key."},"servers":[{"url":"https:\/\/askium.org\/api","description":"Production Askium API"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Invitation Links","description":"Manage personal invitation links for a form"},{"name":"Question Import","description":"Import quiz or exam questions from an external payload"},{"name":"Result Verification","description":"Issue and verify public result verification tokens"}],"paths":{"\/forms\/{form_key}\/invitation-links":{"post":{"tags":["Invitation Links"],"summary":"Create an invitation link","description":"Creates a personal invitation link for the specified form. For exam forms, the API also generates a ticket and activates the link automatically.","parameters":[{"$ref":"#\/components\/parameters\/FormKey"}],"requestBody":{"required":false,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/CreateInvitationLinkRequest"},"examples":{"default":{"value":{"first_name":"Ivan","last_name":"Petrov","email":"ivan.petrov@example.com"}}}}}},"responses":{"200":{"description":"Invitation link created","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/CreateInvitationLinkResponse"},"examples":{"exam":{"value":{"status":"ok","link":{"key":"link-abc123","url":"https:\/\/askium.org\/f\/exam-api-001?link=link-abc123","is_active":true},"ticket_questions_count":2}}}}}},"401":{"$ref":"#\/components\/responses\/UnauthorizedResponse"},"403":{"description":"Form access denied","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"},"examples":{"accessDenied":{"value":{"message":"Form access denied."}}}}}},"404":{"description":"Form not found","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"},"examples":{"formNotFound":{"value":{"message":"Form not found."}}}}}},"422":{"description":"Validation failed or form does not support invitation links","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"},"examples":{"unsupported":{"value":{"message":"Form does not support invitation links."}}}}}},"500":{"description":"Invitation link was not created","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/StatusErrorResponse"}}}}}}},"\/forms\/{form_key}\/invitation-links\/{link_key}":{"delete":{"tags":["Invitation Links"],"summary":"Delete an invitation link","parameters":[{"$ref":"#\/components\/parameters\/FormKey"},{"$ref":"#\/components\/parameters\/LinkKey"}],"responses":{"200":{"description":"Invitation link deleted","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DeleteInvitationLinkResponse"},"examples":{"default":{"value":{"status":"ok","message":"Invitation link deleted."}}}}}},"401":{"$ref":"#\/components\/responses\/UnauthorizedResponse"},"403":{"description":"Form access denied","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"404":{"description":"Form or invitation link not found","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"500":{"description":"Invitation link was not deleted","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/StatusErrorResponse"}}}}}}},"\/forms\/{form_key}\/invitation-links\/active":{"get":{"tags":["Invitation Links"],"summary":"List active unused invitation links","description":"Returns only active invitation links that do not yet have a linked result.","parameters":[{"$ref":"#\/components\/parameters\/FormKey"}],"responses":{"200":{"description":"List of active unused links","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ListInvitationLinksResponse"},"examples":{"default":{"value":{"status":"ok","links":[{"key":"link-active-unused","url":"https:\/\/askium.org\/f\/exam-api-list-001?link=link-active-unused","is_active":true,"first_name":"Alice","last_name":"Green","email":"alice@example.com","created_at":"2026-04-04 12:00:00"}]}}}}}},"401":{"$ref":"#\/components\/responses\/UnauthorizedResponse"},"403":{"description":"Form access denied","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"404":{"description":"Form not found","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}}}}},"\/forms\/{form_key}\/questions\/import":{"post":{"tags":["Question Import"],"summary":"Import questions into a form","description":"Imports questions directly into a quiz or exam form. This endpoint requires API authentication, ownership of the target form, and a Plus or Pro subscription. The `points_num` field is conditionally required when the target form uses points.","parameters":[{"$ref":"#\/components\/parameters\/FormKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ImportQuestionsRequest"},"examples":{"default":{"value":{"difficulty":"medium","points_num":2,"questions":[{"question_short_title":"HTTP code","question":"Which status code means success?","options":["200 OK","301","404","500"],"correct_index":0},{"question_short_title":"TCP port","question":"Which port is typically used by HTTPS?","options":["21","25","80","443"],"correct_index":3}]}}}}}},"responses":{"200":{"description":"Questions imported successfully","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ImportQuestionsResponse"},"examples":{"default":{"value":{"status":"ok","message":"Questions imported successfully.","imported_count":2,"import_key":"imp_01HZYXYZ123456789"}}}}}},"401":{"$ref":"#\/components\/responses\/UnauthorizedResponse"},"403":{"description":"Form access denied or subscription plan does not allow import","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"},"examples":{"planForbidden":{"value":{"message":"Question import is only available on Plus and Pro subscriptions."}}}}}},"404":{"description":"Form not found","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"422":{"description":"Validation failed or form type is unsupported","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ValidationErrorResponse"},"examples":{"unsupportedFormType":{"value":{"message":"Question import is only available for forms of type \"Quiz\" and \"Exam\"."}},"validation":{"value":{"message":"Question #1 option #3 cannot be empty.\nQuestion #1 contains duplicate answer options. Each option must be unique.\nQuestion #1 correct answer index is out of range. It must point to one of the provided options.","errors":{"questions.0.options.2":["Question #1 option #3 cannot be empty."],"questions.0.options":["Question #1 contains duplicate answer options. Each option must be unique."],"questions.0.correct_index":["Question #1 correct answer index is out of range. It must point to one of the provided options."]},"details":["Question #1 option #3 cannot be empty.","Question #1 contains duplicate answer options. Each option must be unique.","Question #1 correct answer index is out of range. It must point to one of the provided options."]}}}}}},"500":{"description":"Question import failed","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}}}}},"\/results\/{result_key}\/verification-key":{"post":{"tags":["Result Verification"],"summary":"Issue a one-time verification key for a result","description":"Creates a short-lived one-time verification token for a result owned by the authenticated API user.","parameters":[{"$ref":"#\/components\/parameters\/ResultKey"}],"responses":{"200":{"description":"Verification key issued","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/IssueVerificationKeyResponse"},"examples":{"default":{"value":{"verification_key":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","token_type":"JWT","expires_at":"2026-04-04 12:02:00","ttl_seconds":120}}}}}},"401":{"$ref":"#\/components\/responses\/UnauthorizedResponse"},"404":{"description":"Result not found","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"},"examples":{"notFound":{"value":{"message":"Result not found."}}}}}},"422":{"description":"Verification is unsupported for the result","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"},"examples":{"surveyUnsupported":{"value":{"message":"Verification is not supported for survey forms."}}}}}}}}},"\/results\/verify":{"post":{"tags":["Result Verification"],"summary":"Verify a result using a public one-time verification key","description":"Public endpoint. Does not require an API key. Intended for frontend or public verification pages.","security":[],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/VerifyResultByKeyRequest"},"examples":{"default":{"value":{"verification_key":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}}}}}},"responses":{"200":{"description":"Verified result payload","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/VerifiedResultResponse"},"examples":{"default":{"value":{"key":"res-8f2d1a","is_finished":true,"started_at":"2026-03-01 10:00:00","finished_at":"2026-03-01 10:42:15","created_at":"2026-03-01 10:00:00","correct_percent":84,"total_points":42,"max_points":50,"correct_answers":2,"wrong_answers":1,"answers_total":3,"unchecked_answers":1,"timer_expires_at":"2026-03-01 11:00:00","is_terminated":false,"is_timer_up":false,"invitation_link_key":null,"personal_data":{"first_name":"Ivan","last_name":"Petrov","email":"ivan.petrov@example.com"},"unwanted_events_count":1,"form":{"key":"frm-php-exam","name":"PHP Certification Exam","type":"exam","locale":"en"}}}}}}},"422":{"description":"Verification key is invalid, expired, already used, or the result type is unsupported","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"},"examples":{"invalidKey":{"value":{"message":"Invalid, expired or already used verification key."}}}}}},"404":{"description":"Result not found","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Per-user private API key. Example: Authorization: Bearer your-private-api-key"}},"parameters":{"FormKey":{"name":"form_key","in":"path","required":true,"schema":{"type":"string"},"description":"Unique Askium form key"},"LinkKey":{"name":"link_key","in":"path","required":true,"schema":{"type":"string"},"description":"Unique invitation link key"},"ResultKey":{"name":"result_key","in":"path","required":true,"schema":{"type":"string"},"description":"Unique result key"}},"responses":{"UnauthorizedResponse":{"description":"Missing or invalid API token","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"},"examples":{"default":{"value":{"message":"Unauthorized."}}}}}}},"schemas":{"ErrorResponse":{"type":"object","required":["message"],"properties":{"message":{"type":"string"}},"additionalProperties":true},"StatusErrorResponse":{"type":"object","required":["status","message"],"properties":{"status":{"type":"string","example":"error"},"message":{"type":"string"}}},"ValidationErrorResponse":{"allOf":[{"$ref":"#\/components\/schemas\/ErrorResponse"},{"type":"object","properties":{"errors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}},"details":{"type":"array","items":{"type":"string"}}}}]},"CreateInvitationLinkRequest":{"type":"object","properties":{"email":{"type":"string","format":"email","nullable":true},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true}},"additionalProperties":false},"InvitationLinkPayload":{"type":"object","required":["key","url","is_active"],"properties":{"key":{"type":"string"},"url":{"type":"string"},"is_active":{"type":"boolean"}}},"CreateInvitationLinkResponse":{"type":"object","required":["status","link","ticket_questions_count"],"properties":{"status":{"type":"string","example":"ok"},"link":{"$ref":"#\/components\/schemas\/InvitationLinkPayload"},"ticket_questions_count":{"type":["integer","null"]}}},"DeleteInvitationLinkResponse":{"type":"object","required":["status","message"],"properties":{"status":{"type":"string","example":"ok"},"message":{"type":"string"}}},"InvitationLinkListItem":{"type":"object","required":["key","url","is_active","first_name","last_name","email","created_at"],"properties":{"key":{"type":"string"},"url":{"type":"string"},"is_active":{"type":"boolean"},"first_name":{"type":["string","null"]},"last_name":{"type":["string","null"]},"email":{"type":["string","null"],"format":"email"},"created_at":{"type":["string","null"]}}},"ListInvitationLinksResponse":{"type":"object","required":["status","links"],"properties":{"status":{"type":"string","example":"ok"},"links":{"type":"array","items":{"$ref":"#\/components\/schemas\/InvitationLinkListItem"}}}},"ImportQuestionItem":{"type":"object","required":["question_short_title","question","options","correct_index"],"properties":{"question_short_title":{"type":"string","maxLength":255},"question":{"type":"string"},"options":{"type":"array","minItems":2,"items":{"type":"string"}},"correct_index":{"type":"integer","minimum":0}},"additionalProperties":false},"ImportQuestionsRequest":{"type":"object","required":["difficulty","questions"],"properties":{"difficulty":{"type":"string","enum":["very_easy","easy","medium","hard","very_hard"]},"points_num":{"type":"integer","minimum":1,"maximum":100,"description":"Conditionally required depending on the form configuration"},"questions":{"type":"array","minItems":1,"items":{"$ref":"#\/components\/schemas\/ImportQuestionItem"}}},"additionalProperties":false},"ImportQuestionsResponse":{"type":"object","required":["status","message","imported_count","import_key"],"properties":{"status":{"type":"string","example":"ok"},"message":{"type":"string"},"imported_count":{"type":"integer"},"import_key":{"type":"string"}}},"IssueVerificationKeyResponse":{"type":"object","required":["verification_key","token_type","expires_at","ttl_seconds"],"properties":{"verification_key":{"type":"string"},"token_type":{"type":"string","example":"JWT"},"expires_at":{"type":"string"},"ttl_seconds":{"type":"integer"}}},"VerifyResultByKeyRequest":{"type":"object","required":["verification_key"],"properties":{"verification_key":{"type":"string"}},"additionalProperties":false},"VerifiedResultResponse":{"type":"object","required":["key","is_finished","started_at","finished_at","created_at","correct_percent","total_points","max_points","correct_answers","wrong_answers","answers_total","unchecked_answers","timer_expires_at","is_terminated","is_timer_up","invitation_link_key","personal_data","unwanted_events_count","form"],"properties":{"key":{"type":"string"},"is_finished":{"type":"boolean"},"started_at":{"type":["string","null"]},"finished_at":{"type":["string","null"]},"created_at":{"type":["string","null"]},"correct_percent":{"type":["integer","null"]},"total_points":{"type":["integer","null"]},"max_points":{"type":["integer","null"]},"correct_answers":{"type":"integer"},"wrong_answers":{"type":"integer"},"answers_total":{"type":"integer"},"unchecked_answers":{"type":"integer"},"timer_expires_at":{"type":["string","null"]},"is_terminated":{"type":["boolean","null"]},"is_timer_up":{"type":["boolean","null"]},"invitation_link_key":{"type":["string","null"]},"personal_data":{"type":"object","required":["first_name","last_name","email"],"properties":{"first_name":{"type":["string","null"]},"last_name":{"type":["string","null"]},"email":{"type":["string","null"],"format":"email"}}},"unwanted_events_count":{"type":"integer"},"form":{"type":"object","required":["key","name","type","locale"],"properties":{"key":{"type":["string","null"]},"name":{"type":["string","null"]},"type":{"type":["string","null"]},"locale":{"type":["string","null"]}}}}}}}}