{"openapi":"3.1.0","info":{"title":"Levano KYC API","version":"1.0.0","description":"Levano's own KYC/AML API. Authenticate with a per-organisation bearer token\n(Settings \u2192 KYC settings \u2192 API). A token is scoped to exactly one organisation\nand can never read another's data.\n\n**Guides and examples: [https://docs.levano.io](https://docs.levano.io).** This document is the contract;\nthe portal is how to use it.\n\n**Rate limits.** Per token, not per IP. `429` carries the standard\n`Retry-After` header.\n\n**Status is derived.** A case's `status` is computed from its steps; there is\nno endpoint that sets it. Approval is a compliance decision a named person\nsigns off inside Levano, and it is deliberately not delegable to a machine.\n\n**Webhooks.** Subscribe under Settings \u2192 KYC settings \u2192 API to receive case-status,\nhit and expiry events. Every body is signed; see the `WebhookEvent` schema."},"servers":[{"url":"https://app.levano.io/api/kyc/v1"}],"externalDocs":{"description":"Guides, examples and the rendered reference.","url":"https://docs.levano.io/reference/kyc/"},"security":[{"bearerAuth":[]}],"tags":[{"name":"Cases","description":"Kundekendskabsprocedurer"},{"name":"Clients","description":"Look-up only, to resolve a client id"}],"paths":{"/cases":{"get":{"tags":["Cases"],"summary":"List cases","parameters":[{"name":"status","in":"query","required":false,"description":"Filter by case status.","schema":{"type":"string"}},{"name":"client_id","in":"query","required":false,"description":"Only cases for this client.","schema":{"type":"string","format":"uuid"}},{"name":"active_only","in":"query","required":false,"description":"Exclude superseded cases.","schema":{"type":"boolean"}},{"name":"per_page","in":"query","required":false,"description":"Page size (max 100).","schema":{"type":"integer"}},{"name":"page","in":"query","required":false,"description":"Page number.","schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Case"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Scope missing or module disabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Cases"],"summary":"Open a case","description":"Requires the `kyc:write` scope. Pass exactly one of `client_id`, `company_id`, or `person_id`. A case opened from `company_id` or `person_id` has NO engagement Client behind it, so `client_id` comes back null: read the subject from `subject_company_id` / `subject_person_id` instead. Idempotent: when a case is already in force for the subject, that case is returned with `200` instead of a second one being opened.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"client_id":{"type":"string","format":"uuid","description":"Engagement client. Mutually exclusive with company_id / person_id."},"company_id":{"type":"string","format":"uuid","description":"Company subject. Mutually exclusive with client_id / person_id."},"person_id":{"type":"string","format":"uuid","description":"Person subject. Mutually exclusive with client_id / company_id."},"profile":{"type":"string","description":"Defaults from the subject type when omitted."}}}}}},"responses":{"201":{"description":"Case opened","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Case"}}}}}},"200":{"description":"A case was already in force","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Case"}}}}}},"404":{"description":"No such client/company/person in this organisation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Invalid payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/cases/{case}":{"get":{"tags":["Cases"],"summary":"Read a case","parameters":[{"name":"case","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Case"}}}}}},"404":{"description":"No such case in this organisation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/cases/{case}/steps":{"get":{"tags":["Cases"],"summary":"List the case's steps","parameters":[{"name":"case","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Step"}}}}}}},"404":{"description":"No such case in this organisation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/clients":{"get":{"tags":["Clients"],"summary":"Find a client id","parameters":[{"name":"search","in":"query","required":false,"description":"Substring of the client name.","schema":{"type":"string"}},{"name":"email","in":"query","required":false,"description":"Exact email match.","schema":{"type":"string"}},{"name":"per_page","in":"query","required":false,"description":"Page size (max 100).","schema":{"type":"integer"}},{"name":"page","in":"query","required":false,"description":"Page number.","schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Client"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}}}}}}}},"/clients/{client}":{"get":{"tags":["Clients"],"summary":"Read a client","parameters":[{"name":"client","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Client"}}}}}},"404":{"description":"No such client in this organisation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/openapi.json":{"get":{"summary":"This document","security":[],"responses":{"200":{"description":"OK"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"A per-organisation API token, minted under Settings \u2192 KYC settings \u2192 API. Scopes: `kyc:read`, `kyc:write`."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthenticated","invalid_token","insufficient_scope","module_disabled","not_found","validation_failed","rate_limited"]},"message":{"type":"string"},"errors":{"type":"object","additionalProperties":true}},"required":["code","message"]}}},"Case":{"type":"object","description":"A kundekendskabsprocedure. `status` is DERIVED from the case's steps and is never set directly \u2014 approving a case with an unmet required step is not possible through this API or any other.","properties":{"id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid","nullable":true},"subject_company_id":{"type":"string","format":"uuid","nullable":true},"subject_person_id":{"type":"string","format":"uuid","nullable":true},"profile":{"type":"string","enum":["person_dk","person_foreign","company_dk","company_foreign","association","public_authority"]},"risk_level":{"type":"string","enum":["low","medium","high","prohibited"]},"status":{"type":"string","enum":["draft","awaiting_client","awaiting_office","awaiting_review","pending_second_approval","approved","rejected","expired","aborted"]},"opened_at":{"type":"string","format":"date-time","nullable":true},"approved_at":{"type":"string","format":"date-time","nullable":true},"rejected_at":{"type":"string","format":"date-time","nullable":true},"valid_until":{"type":"string","format":"date","nullable":true},"days_until_expiry":{"type":"integer","nullable":true},"superseded_by_id":{"type":"string","format":"uuid","nullable":true},"steps":{"type":"array","items":{"$ref":"#/components/schemas/Step"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Step":{"type":"object","description":"One requirement inside a case. `origin` is `profile` when the client type calls for it and `risk` when the risk level added it. The evidence itself \u2014 documents, screening detail, waiver reasoning \u2014 is NOT exposed.","properties":{"id":{"type":"string","format":"uuid"},"kyc_case_id":{"type":"string","format":"uuid"},"kind":{"type":"string"},"origin":{"type":"string","enum":["profile","risk"]},"required":{"type":"boolean"},"status":{"type":"string","enum":["pending","satisfied","waived","failed"]},"fulfilled_by":{"type":"string"},"requested_document_type":{"type":"string","nullable":true},"is_met":{"type":"boolean"},"satisfied_at":{"type":"string","format":"date-time","nullable":true},"expires_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Client":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["person","company"]},"name":{"type":"string"},"email":{"type":"string","nullable":true},"cvr":{"type":"string","nullable":true},"kyc_status":{"type":"string"},"risk_level":{"type":"string","nullable":true},"active_kyc_case_id":{"type":"string","format":"uuid","nullable":true}}},"PaginationMeta":{"type":"object","properties":{"current_page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"},"last_page":{"type":"integer"}}},"WebhookEvent":{"type":"object","description":"POSTed to every subscribing endpoint. Verify `X-Levano-Signature: sha256=<hex>` as HMAC-SHA256 of the RAW request body using the subscription secret before trusting anything in it. Deliveries are retried with exponential backoff and may arrive more than once \u2014 deduplicate on `id`, which is stable across retries of the same event.","properties":{"id":{"type":"string","format":"uuid"},"event":{"type":"string","enum":["kyc.case.status_changed","kyc.case.approved","kyc.case.rejected","kyc.case.expired","kyc.case.expiring","kyc.step.failed"]},"timestamp":{"type":"string","format":"date-time"},"organization_id":{"type":"string","format":"uuid"},"data":{"type":"object","description":"Identifiers and status only \u2014 never names, identity numbers, documents, screening detail or rejection reasons. Fetch what you need from the API with your own token.","properties":{"case":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid","nullable":true},"profile":{"type":"string"},"risk_level":{"type":"string"},"status":{"type":"string","enum":["draft","awaiting_client","awaiting_office","awaiting_review","pending_second_approval","approved","rejected","expired","aborted"]},"approved_at":{"type":"string","format":"date-time","nullable":true},"valid_until":{"type":"string","format":"date","nullable":true}}},"previous_status":{"type":"string","description":"Case events only."},"days_until_expiry":{"type":"integer","description":"`kyc.case.expiring` only."},"step":{"type":"object","description":"`kyc.step.failed` only."}}}}}}},"webhooks":{"kyc.case.status_changed":{"post":{"summary":"A case moved to a status with no event of its own.","security":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Any 2xx marks the delivery succeeded. Anything else is retried with exponential backoff."}}}},"kyc.case.approved":{"post":{"summary":"A case was approved and is now in force.","security":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Any 2xx marks the delivery succeeded. Anything else is retried with exponential backoff."}}}},"kyc.case.rejected":{"post":{"summary":"A case was rejected.","security":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Any 2xx marks the delivery succeeded. Anything else is retried with exponential backoff."}}}},"kyc.case.expired":{"post":{"summary":"A case passed its validity date or a step lapsed.","security":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Any 2xx marks the delivery succeeded. Anything else is retried with exponential backoff."}}}},"kyc.case.expiring":{"post":{"summary":"An approved case is 60/30/7 days from expiry.","security":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Any 2xx marks the delivery succeeded. Anything else is retried with exponential backoff."}}}},"kyc.step.failed":{"post":{"summary":"A screening step failed \u2014 a sanctions, PEP or adverse-media hit.","security":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Any 2xx marks the delivery succeeded. Anything else is retried with exponential backoff."}}}}}}