API contract 1.1

Build with the QuizOut API.

Locked synchronous and stateless v1 contract for the QuizOut WordPress plugin and third-party integrations. The platform returns generated quizzes inline and stores no article or quiz resource.

Contract first

Synchronous and stateless.

QuizOut is synchronous: POST /quizzes returns the complete quiz in the same response. There is no polling endpoint or generation webhook.

QuizOut is stateless for content: article input and generated quiz output are not durable product records. The WordPress plugin stores and renders the returned quiz.

The only transient generation record is an encrypted successful idempotency response plus a request digest in Solid Cache for at most 24 hours.

Base URL
https://api.quizout.io/v1
Media type
application/json
Contract version
1.1

Authentication

One site key per integration.

Pass a per-site key as Authorization: Bearer <site_api_key>. Site keys are separate from Devise console sessions and are shown in plaintext only once when issued.

qo_live_ live key

qo_live_ keys use the production generation path. A post's first successful quiz consumes quota; later successful regenerations do not consume another post unit.

qo_test_ test key

qo_test_ keys use a deterministic provider-free generator. They never create usage-ledger entries, consume monthly quota or prepaid credits, or count successful regenerations.

Key mode is not site environment.

A sandbox site is a separate URL classification for localhost, private, .localhost, .test, .local, or staging environments. Site environment and key mode are independent: a live key on a sandbox site still uses real generation and its sandbox allowance; a test key is provider-free on either site environment.

Safe integration loop

Start with a deterministic test key.

Test responses use generic deterministic integration content and never echo the submitted title, article body, or audience. Validation, idempotency, request throttles, key-sharing protection, and last-used tracking still apply. usage.counted is always false and the remaining usage fields are an unchanged snapshot of the site.

When no format hint is supplied, test generation returns comprehension. It honours:

  • context.format_hint
  • context.question_count
  • post.locale
  • the site's attribution entitlement
Shell
export QUIZOUT_API_KEY='qo_test_REPLACE_WITH_YOUR_KEY'

cURL · deterministic quiz
curl --request POST \
  'https://api.quizout.io/v1/quizzes' \
  --header "Authorization: Bearer ${QUIZOUT_API_KEY}" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: 00000000-0000-4000-8000-000000000001' \
  --data-binary '{
  "post": {
    "external_id": "wp_1024",
    "url": "https://blog.example.com/sunset-glam",
    "title": "Sunset Glam Makeup Look",
    "content": "Full article text or HTML",
    "published_at": "2026-07-20T17:16:31Z",
    "locale": "en"
  },
  "context": {
    "format_hint": null,
    "question_count": 4,
    "audience": "beauty readers",
    "allow_scorecard": true
  }
}'

Fair-use controls

Rate limits and permanent guards.

The X-RateLimit-* response headers describe the binding guard. Temporal 429 responses include Retry-After; a nonzero limit with Reset: 0 is permanent.

LimitApplies toCapacityWindow
Authenticated API key Authenticated endpoints except POST /quizzes 120 requests per key 1 minute
Authenticated source IP Authenticated endpoints except POST /quizzes 300 requests per IP 1 minute
Quiz generation key POST /quizzes, including qo_test_ keys 10 requests per key 1 minute
Quiz generation source IP POST /quizzes, including qo_test_ keys 30 requests per IP 1 minute
Provisioning attempts POST /sites 10 requests per IP 1 hour
Distinct verified live sites POST /sites 20 sites per IP UTC day
Distinct sandbox sites POST /sites 5 sites per IP UTC day
Concurrent regeneration

One in flight per site and post

429 rate_limited; reason regeneration_in_progress; Retry-After: 5
Free successful regenerations

3 per site and post for its lifetime

429 rate_limited; reason regeneration_limit_reached; Reset: 0
Paid or credited successful regenerations

10 per site and post for its lifetime

429 rate_limited; reason regeneration_limit_reached; Reset: 0
API key source sharing

10 distinct source fingerprints

Rolling 24-hour window; 429 rate_limited with reason api_key_ip_limit

Common response headers

HeaderMeaning
X-Request-IdCaller-supplied correlation ID, or a server-generated one.
X-RateLimit-LimitLimit for the currently binding request, abuse, or regeneration guard. Zero means that no limit is enforced for this operation.
X-RateLimit-RemainingCapacity remaining for the binding guard. Zero is also the no-enforcement sentinel when limit is zero.
X-RateLimit-ResetUnix reset timestamp. Zero means either no enforced limit when Limit is zero, or no automatic reset for the lifetime regeneration cap when Limit is nonzero.
QuizOut-Spec-VersionQuizOut response contract version.
Retry-AfterSeconds until a rate-limited request should be retried.

Predictable failures

One error envelope.

Every non-2xx response uses { "error": { "code", "message", "details" } }. The details value is always an object.

HTTPCodeCanonical messageWhen it happens
401 unauthorized Invalid or missing API key. Missing or invalid API key.
403 forbidden This API key is not permitted for this resource. API key not permitted for the resource.
422 invalid_request The request is invalid. Request validation failed.
402 quota_exceeded Monthly post limit reached. Monthly post quota reached for a new post.
429 rate_limited Too many requests. Too many requests. Temporal limits include Retry-After. details.reason is regeneration_in_progress for a concurrent same-post regeneration (Retry-After: 5), regeneration_limit_reached for the permanent Free 3 or paid 10 lifetime cap (Limit: 3 or 10, Remaining: 0, Reset: 0, and no Retry-After), or api_key_ip_limit after an API key exceeds 10 distinct source fingerprints in a rolling 24-hour window (Limit: 10, Remaining: 0, Reset: window end, and Retry-After present).
502 generation_failed Quiz generation failed. Retry the request. Quiz generation failed or the same idempotent request is still running.
500 server_error An unexpected error occurred. Unexpected server error.
501 not_implemented This endpoint is not implemented yet. Transitional response until this endpoint's owning backlog item ships.

Endpoint reference

All six v1 operations.

Paths below are relative to https://api.quizout.io/v1. Request and response examples come directly from the OpenAPI document.

Sites

POST

/sites

Auto-provision an anonymous free site

Public HTTPS sites prove control through a redirect-free POST of { proof } to {home_url}/wp-json/quizout/v1/provisioning-proof. The callback must return JSON containing exactly { verified: true, proof: <same>, home_url: <canonical URL> }. Unreachable localhost, private-address, .localhost, .test, and .local sites are provisioned as tightly limited sandboxes without an outbound callback; a public hostname with a staging label is sandboxed but still verified. Anonymous retries reuse the Site and site token while replacing the prior bootstrap key. Provisioning is limited to 10 attempts per IP/hour and 20 distinct verified live or 5 distinct sandbox sites per IP/UTC day.

Authentication: Public endpoint

Request fields

FieldTypeRequiredDescription and constraints
home_url string Yes format: uri
wp_version string No minimum length: 1
timezone string No minimum length: 1
proof string Yes Short-lived token exposed by the QuizOut WordPress provisioning-proof callback and echoed in its verification response. minimum length: 1

Copy-paste request

cURL
curl --request POST \
  'https://api.quizout.io/v1/sites' \
  --header 'Content-Type: application/json' \
  --data-binary '{
  "home_url": "https://blog.example.com",
  "wp_version": "6.8",
  "timezone": "Europe/London",
  "proof": "proof_abc123"
}'

Request body

JSON
{
  "home_url": "https://blog.example.com",
  "wp_version": "6.8",
  "timezone": "Europe/London",
  "proof": "proof_abc123"
}

Success response

JSON
{
  "site_token": "sst_example123",
  "api_key": "qo_live_example123",
  "plan": "free",
  "usage": {
    "quota": 10,
    "used": 0,
    "remaining": 10,
    "plan_quota": 10,
    "plan_used": 0,
    "pack_remaining": 0,
    "period_end": "2026-08-01T00:00:00Z"
  }
}

Response statuses

HTTPMeaning
201Anonymous site provisioned.
422Request validation failed.
429Too many requests. Temporal limits include Retry-After. details.reason is regeneration_in_progress for a concurrent same-post regeneration (Retry-After: 5), regeneration_limit_reached for the permanent Free 3 or paid 10 lifetime cap (Limit: 3 or 10, Remaining: 0, Reset: 0, and no Retry-After), or api_key_ip_limit after an API key exceeds 10 distinct source fingerprints in a rolling 24-hour window (Limit: 10, Remaining: 0, Reset: window end, and Retry-After present).
500Unexpected server error.

Quiz generation

POST

/quizzes

Generate a quiz synchronously

Validates the article and returns the complete quiz inline; no quiz resource is stored. Limited to 10 requests per API key/minute and 30 requests per IP/minute. With a qo_live_ key, a site/post receives 3 lifetime successful regenerations on Free or 10 on Growth, Pro, or while active prepaid credits remain. With a qo_test_ key, generation is deterministic and provider-free, usage.counted is false, quota and regeneration allowances are untouched, and the normal validation, idempotency, and temporal limits still apply.

Authentication: Bearer site key required

Request fields

FieldTypeRequiredDescription and constraints
post object Yes
post.external_id string Yes minimum length: 1
post.url string No format: uri
post.title string Yes minimum length: 1
post.content string Yes Article text or HTML, limited to 102400 UTF-8 bytes. minimum length: 1; maximum bytes: 102400
post.published_at string No format: date-time
post.locale string No minimum length: 2
context object No
context.format_hint string or null No Optional explicit format. Null selects automatic live detection or deterministic comprehension for qo_test_ generation. values: null, comprehension, true_false, myth_busting, myth-busting, scenario, scorecard, recommendation
context.question_count integer No Number of questions to return; defaults to 4. minimum: 1; maximum: 5
context.audience string or null No
context.allow_scorecard boolean No

Copy-paste request

cURL
curl --request POST \
  'https://api.quizout.io/v1/quizzes' \
  --header "Authorization: Bearer ${QUIZOUT_API_KEY}" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: 00000000-0000-4000-8000-000000000001' \
  --data-binary '{
  "post": {
    "external_id": "wp_1024",
    "url": "https://blog.example.com/sunset-glam",
    "title": "Sunset Glam Makeup Look",
    "content": "Full article text or HTML",
    "published_at": "2026-07-20T17:16:31Z",
    "locale": "en"
  },
  "context": {
    "format_hint": null,
    "question_count": 4,
    "audience": "beauty readers",
    "allow_scorecard": true
  }
}'

Request body

JSON
{
  "post": {
    "external_id": "wp_1024",
    "url": "https://blog.example.com/sunset-glam",
    "title": "Sunset Glam Makeup Look",
    "content": "Full article text or HTML",
    "published_at": "2026-07-20T17:16:31Z",
    "locale": "en"
  },
  "context": {
    "format_hint": null,
    "question_count": 4,
    "audience": "beauty readers",
    "allow_scorecard": true
  }
}

Success response

JSON
{
  "quiz": {
    "generation_id": "gen_9f3",
    "format": "comprehension",
    "scoring_kind": "graded",
    "locale": "en",
    "questions": [
      {
        "position": 1,
        "prompt": "Which shade went in the inner corners?",
        "answer_type": "single_select",
        "explanation": "The look starts with canary yellow in the inner corners.",
        "options": [
          {
            "position": 1,
            "text": "Canary yellow",
            "correct": true,
            "points": null,
            "outcome_key": null
          },
          {
            "position": 2,
            "text": "Rose pink",
            "correct": false,
            "points": null,
            "outcome_key": null
          },
          {
            "position": 3,
            "text": "Bronze",
            "correct": false,
            "points": null,
            "outcome_key": null
          }
        ]
      }
    ],
    "result": {
      "kind": "graded",
      "max_score": null,
      "bands": null,
      "outcomes": null
    },
    "attribution": {
      "powered_by": true,
      "url": "https://quizout.io/?utm_source=badge"
    },
    "meta": {
      "spec_version": "1.0",
      "generated_at": "2026-07-21T10:00:05Z"
    }
  },
  "usage": {
    "plan": "growth",
    "quota": 100,
    "used": 38,
    "remaining": 62,
    "plan_quota": 100,
    "plan_used": 38,
    "pack_remaining": 0,
    "period_end": "2026-08-01T00:00:00Z",
    "counted": true
  }
}

Response statuses

HTTPMeaning
200Quiz generated synchronously.
401Missing or invalid API key.
402Monthly post quota reached for a new post.
403API key not permitted for the resource.
422Request validation failed.
429Too many requests. Temporal limits include Retry-After. details.reason is regeneration_in_progress for a concurrent same-post regeneration (Retry-After: 5), regeneration_limit_reached for the permanent Free 3 or paid 10 lifetime cap (Limit: 3 or 10, Remaining: 0, Reset: 0, and no Retry-After), or api_key_ip_limit after an API key exceeds 10 distinct source fingerprints in a rolling 24-hour window (Limit: 10, Remaining: 0, Reset: window end, and Retry-After present).
500Unexpected server error.
502Quiz generation failed or the same idempotent request is still running.

Usage

GET

/usage

Get current site usage and attribution entitlement

Returns the allowance for the authenticated site's live or sandbox environment. Key mode does not create a separate usage pool; qo_test_ quiz requests leave these values unchanged.

Authentication: Bearer site key required

Copy-paste request

cURL
curl --request GET \
  'https://api.quizout.io/v1/usage' \
  --header "Authorization: Bearer ${QUIZOUT_API_KEY}"

Success response

JSON
{
  "usage": {
    "plan": "growth",
    "quota": 100,
    "used": 38,
    "remaining": 62,
    "plan_quota": 100,
    "plan_used": 38,
    "pack_remaining": 0,
    "period_end": "2026-08-01T00:00:00Z"
  },
  "attribution": {
    "powered_by": false,
    "url": "https://quizout.io/?utm_source=badge"
  }
}

Response statuses

HTTPMeaning
200Current usage and attribution entitlement returned.
401Missing or invalid API key.
403API key not permitted for the resource.
429Too many requests. Temporal limits include Retry-After. details.reason is regeneration_in_progress for a concurrent same-post regeneration (Retry-After: 5), regeneration_limit_reached for the permanent Free 3 or paid 10 lifetime cap (Limit: 3 or 10, Remaining: 0, Reset: 0, and no Retry-After), or api_key_ip_limit after an API key exceeds 10 distinct source fingerprints in a rolling 24-hour window (Limit: 10, Remaining: 0, Reset: window end, and Retry-After present).
500Unexpected server error.

Sites

GET

/site

Get the site profile, plan, and usage

Returns the authenticated site profile and allowance. The current deployment may return the documented transitional 501 response until this operation ships.

Authentication: Bearer site key required

Copy-paste request

cURL
curl --request GET \
  'https://api.quizout.io/v1/site' \
  --header "Authorization: Bearer ${QUIZOUT_API_KEY}"

Success response

JSON
{
  "site": {
    "id": "ste_1",
    "name": "Example Blog",
    "plan": "growth",
    "usage": {
      "quota": 100,
      "used": 38,
      "remaining": 62,
      "plan_quota": 100,
      "plan_used": 38,
      "pack_remaining": 0,
      "period_end": "2026-08-01T00:00:00Z"
    }
  }
}

Response statuses

HTTPMeaning
200Site profile, plan, and usage returned.
401Missing or invalid API key.
403API key not permitted for the resource.
429Too many requests. Temporal limits include Retry-After. details.reason is regeneration_in_progress for a concurrent same-post regeneration (Retry-After: 5), regeneration_limit_reached for the permanent Free 3 or paid 10 lifetime cap (Limit: 3 or 10, Remaining: 0, Reset: 0, and no Retry-After), or api_key_ip_limit after an API key exceeds 10 distinct source fingerprints in a rolling 24-hour window (Limit: 10, Remaining: 0, Reset: window end, and Retry-After present).
500Unexpected server error.
501Transitional response until this endpoint's owning backlog item ships.

Sites

POST

/site/connect

Verify an API key and register site metadata

Authenticates only with the pasted site Bearer key, refreshes WordPress metadata and canonical URL/environment for that same Site, and preserves any console-defined name and existing keys. URL moves are allowed unless the canonical URL belongs to another Site; current Free-plan usage is returned for the site's live or sandbox pool.

Authentication: Bearer site key required

Request fields

FieldTypeRequiredDescription and constraints
home_url string Yes format: uri
wp_version string No minimum length: 1
timezone string No minimum length: 1

Copy-paste request

cURL
curl --request POST \
  'https://api.quizout.io/v1/site/connect' \
  --header "Authorization: Bearer ${QUIZOUT_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data-binary '{
  "home_url": "https://blog.example.com",
  "wp_version": "6.8",
  "timezone": "Africa/Lagos"
}'

Request body

JSON
{
  "home_url": "https://blog.example.com",
  "wp_version": "6.8",
  "timezone": "Africa/Lagos"
}

Success response

JSON
{
  "site": {
    "id": "ste_1",
    "name": "Example Blog",
    "plan": "growth",
    "usage": {
      "quota": 100,
      "used": 38,
      "remaining": 62,
      "plan_quota": 100,
      "plan_used": 38,
      "pack_remaining": 0,
      "period_end": "2026-08-01T00:00:00Z"
    }
  }
}

Response statuses

HTTPMeaning
200Site profile, plan, and usage returned.
401Missing or invalid API key.
403API key not permitted for the resource.
422Request validation failed.
429Too many requests. Temporal limits include Retry-After. details.reason is regeneration_in_progress for a concurrent same-post regeneration (Retry-After: 5), regeneration_limit_reached for the permanent Free 3 or paid 10 lifetime cap (Limit: 3 or 10, Remaining: 0, Reset: 0, and no Retry-After), or api_key_ip_limit after an API key exceeds 10 distinct source fingerprints in a rolling 24-hour window (Limit: 10, Remaining: 0, Reset: window end, and Retry-After present).
500Unexpected server error.

Sites

POST

/site/claim

Initiate a site claim by email magic link

Creates a 24-hour pending claim and emails a scanner-safe console link. This API call never authenticates a human or moves the Site; the console confirmation POST completes account creation or merge.

Authentication: Bearer site key required

Request fields

FieldTypeRequiredDescription and constraints
email string Yes format: email

Copy-paste request

cURL
curl --request POST \
  'https://api.quizout.io/v1/site/claim' \
  --header "Authorization: Bearer ${QUIZOUT_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data-binary '{
  "email": "owner@example.com"
}'

Request body

JSON
{
  "email": "owner@example.com"
}

Success response

JSON
{
  "status": "claim_initiated"
}

Response statuses

HTTPMeaning
202Claim email accepted; completion happens in the Devise console.
401Missing or invalid API key.
403API key not permitted for the resource.
422Request validation failed.
429Too many requests. Temporal limits include Retry-After. details.reason is regeneration_in_progress for a concurrent same-post regeneration (Retry-After: 5), regeneration_limit_reached for the permanent Free 3 or paid 10 lifetime cap (Limit: 3 or 10, Remaining: 0, Reset: 0, and no Retry-After), or api_key_ip_limit after an API key exceeds 10 distinct source fingerprints in a rolling 24-hour window (Limit: 10, Remaining: 0, Reset: window end, and Retry-After present).
500Unexpected server error.

Early partner programme

Make your next post more playable.

Tell us where you publish. We’ll share the partner preview and the right next step for your team.

We store only this partner inquiry. Quiz and reader data stay on your WordPress site.