openapi: "3.1.0"

info:
  title: Myra EU CAPTCHA API
  version: "1.0"
  description: |
    The EU CAPTCHA verification API exposes a single endpoint your server calls
    after receiving a form submission. It validates the token generated by the
    `verify.js` widget on the client and returns whether the request is legitimate.

    Authentication uses the `secret` field in the request body. **Never call this
    endpoint from client-side code** — the secret must remain on your server.

    | Version | Reason for change |
    | --- | --- |
    | 1.0 | Initial release |
  contact:
    name: Myra Security Support
    url: https://www.myrasecurity.com/en/contact/
  termsOfService: https://www.myrasecurity.com/en/terms-and-conditions/

externalDocs:
  description: Integration guide and code examples
  url: https://docs.eu-captcha.eu/

servers:
  - url: https://api.eu-captcha.eu/v1
    description: Production

tags:
  - name: Verification
    description: Token verification endpoint for the EU CAPTCHA service.
    externalDocs:
      description: Backend integration guide
      url: https://docs.eu-captcha.eu/Content/first_steps/Captcha_backend.htm

paths:
  /verify:
    post:
      operationId: verifyClientToken
      summary: Verify submitted client token
      tags:
        - Verification
      description: |
        Validates the proof-of-work token generated by `verify.js` on the client.
        Checks that the calculations were performed correctly, that the token has
        not already been used, and that the sitekey and secret are valid.

        The response field `success` indicates the verification result. Always
        check the `train` field as well — see its description for important
        security implications.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerifyRequest"
            example:
              sitekey: "1c87e240-0000-0000-0000-23ac9f99da68"
              secret: "LqFgQA+ExampleSecretKeyForDocsOnly+wdjvI="
              client_ip: "5.6.7.8"
              client_token: "XVtHUQExampleTokenUF9ESkQUAxUTChJEXFFVX1ZXURs="
              client_user_agent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
      responses:
        "200":
          description: Verification result. Always check both `success` and `train`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerifyResponse"
              examples:
                verified:
                  summary: Token is valid
                  value:
                    success: true
                    train: false
                blocked:
                  summary: Token is invalid or already used
                  value:
                    success: false
                    train: false
                bypass:
                  summary: Validation was skipped (misconfiguration)
                  value:
                    success: true
                    train: true
        "400":
          description: The request body is missing required fields or is malformed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "missing_field"
                message: "Required field 'sitekey' is missing."
        "429":
          description: |
            Rate limit exceeded. Back off and retry after the number of seconds
            indicated by the `Retry-After` header.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
                example: 10
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "rate_limit_exceeded"
                message: "Too many requests. Please retry after 10 seconds."
        "500":
          description: Unexpected server-side error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "internal_error"
                message: "An unexpected error occurred. Please try again."

components:
  schemas:
    VerifyRequest:
      type: object
      required:
        - sitekey
        - secret
        - client_ip
        - client_token
        - client_user_agent
      properties:
        sitekey:
          type: string
          description: |
            The public sitekey associated with the domain where the widget is
            embedded. Found in the dashboard under **Sitekeys → Settings → Details**.
          example: "1c87e240-0000-0000-0000-23ac9f99da68"
        secret:
          type: string
          description: |
            The secret key paired with the sitekey. Found in the dashboard under
            **Sitekeys → Settings → Details**.

            Keep this value on your server — never expose it in client-side code.
          example: "LqFgQA+ExampleSecretKeyForDocsOnly+wdjvI="
        client_ip:
          type: string
          description: |
            The IPv4 or IPv6 address of the end-user's client.

            Submit the actual client IP, not the address of an intermediate proxy
            or CDN. Use `X-Forwarded-For` or `X-Client-IP` headers where
            appropriate to obtain the real client address.
          examples:
            ipv4:
              value: "5.6.7.8"
            ipv6:
              value: "2001:db8::1"
        client_token:
          type: string
          minLength: 0
          description: |
            The token generated by `verify.js`, submitted to your server in the
            form field named `eu-captcha-response`. May be an empty string if the
            widget did not complete (e.g. JavaScript was disabled or the page
            loaded too slowly).

            Always submit this field, even when empty. Sending incomplete or
            empty tokens allows the service to learn about all verification
            attempts, including failed ones, which improves detection accuracy.
          example: "XVtHUQExampleTokenUF9ESkQUAxUTChJEXFFVX1ZXURs="
        client_user_agent:
          type: string
          description: |
            The `User-Agent` request header sent by the end-user's client. Used
            to identify the type of client when a token could not be computed
            (e.g. bots that do not run JavaScript).
          example: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"

    VerifyResponse:
      type: object
      required:
        - success
        - train
      properties:
        success:
          type: boolean
          description: |
            `true` if the token passed verification; `false` if it did not.

            **Always check the `train` field alongside this value.** When
            `train` is `true`, this field is forced to `true` regardless of
            whether real validation occurred.
          example: true
        train:
          type: ["boolean", "null"]
          description: |
            Indicates whether actual validation was performed.

            - `false` or `null` — normal operation; `success` reflects the real
              verification result.
            - `true` — validation was **skipped** and `success` is forced to
              `true`. This occurs when:
              - The `sitekey` does not exist
              - The `secret` does not match the sitekey
              - The sitekey's protection toggle is disabled
              - Any other condition prevents the server from validating

            **Security warning:** A `train: true` response means your
            verification request was not validated. All submissions will appear
            successful. Check your `sitekey` and `secret` values immediately if
            you receive this in production.
          example: false

    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: A machine-readable error code.
          example: "missing_field"
        message:
          type: string
          description: A human-readable description of the error.
          example: "Required field 'sitekey' is missing."
