{"openapi":"3.1.0","info":{"title":"Dutch Fluency Discovery API","version":"1.0.0","description":"Anonymous read-only public learning-resource discovery. Every response is built from a reviewed public projection. No learner, account, billing, session, or unpublished data is reachable through this API.","license":{"name":"Proprietary","url":"https://www.dutchfluency.com"}},"servers":[{"url":"https://discover.dutchfluency.com"}],"tags":[{"name":"discovery","description":"Public catalogue discovery"},{"name":"guidance","description":"Deterministic learning guidance"}],"paths":{"/api/v1/search":{"get":{"operationId":"searchResources","tags":["discovery"],"summary":"Search public learning resources","description":"Full-text search across reviewed public titles, summaries, topics, and article chunks using the Dutch text-search configuration. Results are deterministically ordered by score, publication date, then resource identifier.","parameters":[{"name":"q","in":"query","required":true,"description":"Free-text query. Never interpolated into SQL.","schema":{"type":"string","minLength":1,"maxLength":500}},{"name":"types","in":"query","required":false,"description":"Comma-separated resource types.","schema":{"type":"string","examples":["transcript,article"]}},{"name":"cefr","in":"query","required":false,"description":"Comma-separated CEFR levels.","schema":{"type":"string","examples":["A2,B1"]}},{"name":"languages","in":"query","required":false,"description":"Comma-separated BCP-47 language subtags.","schema":{"type":"string","examples":["nl,en"]}},{"name":"topics","in":"query","required":false,"description":"Comma-separated public topic identifiers.","schema":{"type":"string","examples":["grammar,uitspraak"]}},{"name":"limit","in":"query","required":false,"description":"Maximum hits to return.","schema":{"type":"integer","minimum":1,"maximum":50,"default":10}},{"name":"cursor","in":"query","required":false,"description":"Opaque bounded pagination cursor returned as nextCursor. Maximum reachable offset is 10000.","schema":{"type":"string","maxLength":128}}],"responses":{"200":{"description":"Ranked public search results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}},"400":{"description":"The request was rejected by the public input contract.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded for the anonymous caller network","headers":{"Retry-After":{"description":"Whole seconds until the current fixed window resets","schema":{"type":"integer","minimum":1,"maximum":3600}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window","schema":{"type":"integer","minimum":0}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"A required backend is unavailable. The service fails closed and never serves unbounded traffic.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/meta":{"get":{"operationId":"getProjectionMetadata","tags":["discovery"],"summary":"Get public projection metadata","description":"Reports the public projection version, the last completed synchronisation timestamp, and per-type resource counts. Private synchronisation failure detail is never exposed.","responses":{"200":{"description":"Sanitised public projection metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectionMetadata"}}}},"400":{"description":"The request was rejected by the public input contract.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded for the anonymous caller network","headers":{"Retry-After":{"description":"Whole seconds until the current fixed window resets","schema":{"type":"integer","minimum":1,"maximum":3600}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window","schema":{"type":"integer","minimum":0}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"A required backend is unavailable. The service fails closed and never serves unbounded traffic.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/resources/{id}":{"get":{"operationId":"getResourceById","tags":["discovery"],"summary":"Get one public resource","description":"Returns a single reviewed public resource. Full-text chunks are returned in exact chunk order only for resources whose reviewed rights basis permits redistribution.","parameters":[{"name":"id","in":"path","required":true,"description":"Stable public resource identifier.","schema":{"type":"string","minLength":3,"maxLength":200}},{"name":"If-None-Match","in":"header","required":false,"description":"Conditional request validator.","schema":{"type":"string"}}],"responses":{"200":{"description":"One reviewed public resource","headers":{"ETag":{"description":"Strong validator for the resource payload","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceDetail"}}}},"304":{"description":"Resource unchanged since the supplied validator"},"400":{"description":"The request was rejected by the public input contract.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No public resource matches the identifier.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded for the anonymous caller network","headers":{"Retry-After":{"description":"Whole seconds until the current fixed window resets","schema":{"type":"integer","minimum":1,"maximum":3600}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window","schema":{"type":"integer","minimum":0}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"A required backend is unavailable. The service fails closed and never serves unbounded traffic.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/topics":{"get":{"operationId":"listTopics","tags":["discovery"],"summary":"Browse public topics","description":"Aggregates public topics with deterministic representative resources, capped at three per topic.","parameters":[{"name":"level","in":"query","required":false,"description":"Restrict aggregation to one CEFR level.","schema":{"type":"string","enum":["A0","A1","A2","B1","B2","C1","C2"]}},{"name":"type","in":"query","required":false,"description":"Restrict aggregation to one resource type.","schema":{"type":"string","enum":["transcript","article","product"]}}],"responses":{"200":{"description":"Public topic aggregates","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopicsResponse"}}}},"400":{"description":"The request was rejected by the public input contract.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded for the anonymous caller network","headers":{"Retry-After":{"description":"Whole seconds until the current fixed window resets","schema":{"type":"integer","minimum":1,"maximum":3600}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window","schema":{"type":"integer","minimum":0}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"A required backend is unavailable. The service fails closed and never serves unbounded traffic.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/products":{"get":{"operationId":"listProducts","tags":["discovery"],"summary":"List reviewed Dutch Fluency products","description":"Returns exactly the reviewed public product entries. No amount, currency, or checkout data is published; consult the canonical page for current terms.","responses":{"200":{"description":"Reviewed public products","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductsResponse"}}}},"400":{"description":"The request was rejected by the public input contract.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded for the anonymous caller network","headers":{"Retry-After":{"description":"Whole seconds until the current fixed window resets","schema":{"type":"integer","minimum":1,"maximum":3600}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window","schema":{"type":"integer","minimum":0}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"A required backend is unavailable. The service fails closed and never serves unbounded traffic.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/recommendations":{"post":{"operationId":"buildLearningPath","tags":["guidance"],"summary":"Build a deterministic learning path","description":"Builds an ordered learning path from reviewed public resources only. Scoring is deterministic, at most six steps are returned, at most one product is included, and a product is never placed before two content resources. No duration, outcome, or resource outside the public projection is invented.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecommendationRequest"}}}},"responses":{"200":{"description":"Deterministic ordered learning path","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecommendationResponse"}}}},"400":{"description":"The request was rejected by the public input contract.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded for the anonymous caller network","headers":{"Retry-After":{"description":"Whole seconds until the current fixed window resets","schema":{"type":"integer","minimum":1,"maximum":3600}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window","schema":{"type":"integer","minimum":0}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"A required backend is unavailable. The service fails closed and never serves unbounded traffic.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Error":{"type":"object","additionalProperties":false,"required":["error"],"properties":{"error":{"type":"object","additionalProperties":false,"required":["code","message","requestId"],"properties":{"code":{"type":"string","description":"Stable machine-readable error code.","examples":["rate_limited","resource_not_found"]},"message":{"type":"string","description":"Bounded public message. Never contains backend detail."},"requestId":{"type":"string","format":"uuid","description":"Correlates with the X-Request-ID response header."}}}}},"ResourceType":{"type":"string","enum":["transcript","article","product"]},"PublicResource":{"type":"object","additionalProperties":false,"required":["id","type","title","summary","canonicalUrl","language","availableLanguages","cefrLevel","topics","publishedAt","updatedAt","sourceName","contentAccess","metadata"],"properties":{"id":{"type":"string","description":"Stable public identifier."},"type":{"$ref":"#/components/schemas/ResourceType"},"title":{"type":"string"},"summary":{"type":["string","null"]},"canonicalUrl":{"type":"string","format":"uri","description":"HTTPS canonical page on a reviewed public host. Never signed and never time-limited."},"language":{"type":"string","description":"Primary BCP-47 subtag."},"availableLanguages":{"type":"array","items":{"type":"string"}},"cefrLevel":{"type":["string","null"],"enum":["A0","A1","A2","B1","B2","C1","C2",null]},"topics":{"type":"array","items":{"type":"string"}},"publishedAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"sourceName":{"type":["string","null"]},"contentAccess":{"type":"string","enum":["full","metadata"],"description":"full means reviewed rights permit redistributing chunk text. metadata means only public descriptive fields are published."},"metadata":{"type":"object","description":"Reviewed per-type public metadata allowlist. Unknown keys are rejected before publication.","additionalProperties":true}}},"SearchHit":{"allOf":[{"$ref":"#/components/schemas/PublicResource"},{"type":"object","required":["excerpt","score"],"properties":{"excerpt":{"type":"string","description":"Bounded public excerpt derived from published text."},"score":{"type":"number","minimum":0}}}]},"SearchResponse":{"type":"object","additionalProperties":false,"required":["hits","total","nextCursor"],"properties":{"hits":{"type":"array","items":{"$ref":"#/components/schemas/SearchHit"}},"total":{"type":"integer","minimum":0},"nextCursor":{"type":["string","null"],"description":"Opaque bounded cursor, or null when no further page is reachable."}}},"ResourceDetail":{"allOf":[{"$ref":"#/components/schemas/PublicResource"},{"type":"object","required":["chunks"],"properties":{"chunks":{"type":"array","description":"Ordered public text chunks. Always empty when contentAccess is metadata.","items":{"type":"string"}}}}]},"TopicSummary":{"type":"object","additionalProperties":false,"required":["id","label","resourceCount","representativeResources"],"properties":{"id":{"type":"string"},"label":{"type":"string"},"resourceCount":{"type":"integer","minimum":0},"representativeResources":{"type":"array","maxItems":3,"items":{"type":"object","additionalProperties":false,"required":["id","title","canonicalUrl"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"canonicalUrl":{"type":"string","format":"uri"}}}}}},"TopicsResponse":{"type":"object","additionalProperties":false,"required":["topics"],"properties":{"topics":{"type":"array","items":{"$ref":"#/components/schemas/TopicSummary"}}}},"ProductsResponse":{"type":"object","additionalProperties":false,"required":["products"],"properties":{"products":{"type":"array","items":{"$ref":"#/components/schemas/PublicResource"}}}},"ProjectionMetadata":{"type":"object","additionalProperties":false,"required":["projectionVersion","lastCompletedSync","counts"],"properties":{"projectionVersion":{"type":"string"},"lastCompletedSync":{"type":["string","null"],"format":"date-time"},"counts":{"type":"object","additionalProperties":{"type":"integer","minimum":0},"description":"Public resource count per resource type."}}},"RecommendationRequest":{"type":"object","additionalProperties":false,"required":["goal"],"properties":{"goal":{"type":"string","minLength":2,"maxLength":500},"currentCefrLevel":{"type":"string","enum":["A0","A1","A2","B1","B2","C1","C2"]},"targetCefrLevel":{"type":"string","enum":["A0","A1","A2","B1","B2","C1","C2"]},"interests":{"type":"array","maxItems":10,"items":{"type":"string","minLength":1,"maxLength":100}},"availableStudyMinutes":{"type":"integer","minimum":5,"maximum":1440},"preferredTypes":{"type":"array","maxItems":3,"items":{"$ref":"#/components/schemas/ResourceType"}}}},"RecommendedStep":{"type":"object","additionalProperties":false,"required":["position","resourceId","title","canonicalUrl","reason"],"properties":{"position":{"type":"integer","minimum":1,"maximum":6},"resourceId":{"type":"string"},"title":{"type":"string"},"canonicalUrl":{"type":"string","format":"uri"},"reason":{"type":"string","description":"Assembled only from fields that actually matched the request."}}},"RecommendationResponse":{"type":"object","additionalProperties":false,"required":["steps"],"properties":{"steps":{"type":"array","maxItems":6,"items":{"$ref":"#/components/schemas/RecommendedStep"}}}}}}}