{"openapi":"3.1.0","info":{"title":"Mdkit","description":"Programmatic access to **Mdkit**.\n\n### Authentication\nPass your key in the **`X-API-Key`** request header. Calls without a key are\nserved anonymously at a per-IP tier; a valid key lifts you to your plan's tier.\n\n### Rate limits\nEvery response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and\n`X-RateLimit-Reset` (seconds until the window resets). Over-budget calls return\n`429` with a `Retry-After` header. Check your live budget at `GET /v1/me`.\n\n### Pagination\nList endpoints return a keyset envelope — `{\"items\": [...], \"next_cursor\": \"...\"}`.\nPass `next_cursor` back as the `?cursor=` query param for the next page; it is\n`null` on the last page.\n\n### Errors\nErrors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) `problem+json` with a\nstable machine-readable **`code`** (e.g. `rate_limited`, `credits_exhausted`,\n`validation_error`). Branch on `code` — `title`/`detail` text may be reworded.\n\n### Quickstart\n```sh\ncurl -H \"X-API-Key: $API_KEY\" https://api.mdkit.online/v1/me\n```\n","version":"1"},"servers":[{"url":"https://api.mdkit.online"}],"paths":{"/v1/me":{"get":{"tags":["Account"],"summary":"Your plan, remaining requests, and remaining credits","description":"Check where you stand before a call fails.\n\nReturns the plan this request is being served on, whether it was\nrecognised as yours (send your `X-API-Key`; without one you are served as\nan anonymous visitor on the free allowance), how many requests are left in\nthe current window, how many credits are left in the current billing period\n(on plans priced in credits), and the features your plan includes.\n\nChecking is always free: this call costs no credit and does not count\nagainst your request allowance, so the numbers it reports are the numbers\nyou actually have.","operationId":"get_me","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}}},"security":[{"APIKeyHeader":[]}]}},"/v1/jobs/{job_id}":{"get":{"tags":["Jobs"],"summary":"Fetch one async job by id","description":"Return the caller's OWN job. A missing id, or one owned by a different\ncaller, is a 404 (never 403 — don't reveal another owner's job exists).","operationId":"get_job","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"422":{"description":"Validation Error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/jobs":{"get":{"tags":["Jobs"],"summary":"List the caller's async jobs (newest first)","description":"Keyset-paged NEWEST-FIRST BY ``created_at``, owner-scoped to the caller.\n\nOrdering deliberately does NOT use the id: a job id is a uuid4 hex string,\nso ``ORDER BY id DESC`` is a random permutation dressed up as \"newest first\"\n— a customer polling their list would see an arbitrary sequence. ``id`` is\nkept only as the tiebreaker that makes the order total (two jobs submitted\nin the same clock tick still get a stable, repeatable position), which is\nalso what makes the keyset cursor below skip-free.","operationId":"list_jobs","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponsePage"}}}},"422":{"description":"Validation Error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/convert":{"post":{"tags":["Convert"],"summary":"Convert a document to Markdown, immediately","description":"SYNC lane: convert a small document to Markdown in-process, immediately.","operationId":"convert_document","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_convert_document"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SyncConvertResponse"}}}},"422":{"description":"Validation Error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"security":[{"APIKeyHeader":[]}]}},"/v1/convert/jobs":{"post":{"tags":["Convert"],"summary":"Submit a large document for background conversion","description":"ASYNC lane: stash the upload in storage and submit a docling job.\n\n503 when the docling engine isn't deployed (``DOCLING_URL`` unset). The 10\ncredits are charged once, inside ``submit`` — the access marker is cost=0.","operationId":"submit_conversion_job","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_submit_conversion_job"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobSubmitResponse"}}}},"422":{"description":"Validation Error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"security":[{"APIKeyHeader":[]}]}},"/v1/convert/jobs/{job_id}/result":{"get":{"tags":["Convert"],"summary":"Download a finished conversion's Markdown or JSON","description":"Stream a finished docling job's output from storage.\n\nOwner-scoped: another owner's (or a missing) job is 404, never 403 — job ids\nstay unenumerable. 409 until the job has succeeded and recorded its result.","operationId":"get_conversion_result","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","default":"markdown","title":"Format"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}},"components":{"schemas":{"Body_convert_document":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_convert_document"},"Body_submit_conversion_job":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"webhook_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webhook Url"}},"type":"object","required":["file"],"title":"Body_submit_conversion_job"},"CreditsInfo":{"properties":{"balance":{"type":"integer","title":"Balance"},"granted":{"type":"integer","title":"Granted"},"period_end":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Period End"}},"type":"object","required":["balance","granted"],"title":"CreditsInfo","description":"Your credit balance for the current billing period. `granted` is what\nyour plan included, `balance` what is still unspent, and `period_end` the\ndate (ISO 8601) your allowance refills. Present only on plans that are\npriced in credits."},"JobResponse":{"properties":{"id":{"type":"string","title":"Id"},"kind":{"type":"string","title":"Kind"},"status":{"type":"string","title":"Status"},"result":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Result"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Started At"},"finished_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Finished At"}},"type":"object","required":["id","kind","status"],"title":"JobResponse","description":"The client-facing view of a job — everything a poller needs, and NOT the\nwebhook secret (returned once at submit)."},"JobSubmitResponse":{"properties":{"job_id":{"type":"string","title":"Job Id"},"status":{"type":"string","title":"Status"},"webhook_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webhook Secret"},"result_url":{"type":"string","title":"Result Url"},"poll_url":{"type":"string","title":"Poll Url"}},"type":"object","required":["job_id","status","result_url","poll_url"],"title":"JobSubmitResponse","description":"The asynchronous (docling sidecar) lane submit result.\n\n``webhook_secret`` is exposed ONCE here — appkit's poll surface\n(``GET /v1/jobs/{id}``) never returns it again. Owner id is NEVER a field:\nit is derived server-side from the request Identity."},"MeResponse":{"properties":{"tier":{"type":"string","title":"Tier","description":"The plan this request is served on."},"is_keyed":{"type":"boolean","title":"Is Keyed","description":"Whether the request carried a valid API key. False means you are being served as an anonymous visitor on the free allowance."},"rate_limit":{"$ref":"#/components/schemas/RateLimitInfo"},"credits":{"anyOf":[{"$ref":"#/components/schemas/CreditsInfo"},{"type":"null"}],"description":"Null on plans that are not priced in credits."},"features":{"items":{"type":"string"},"type":"array","title":"Features","description":"The features your plan includes."}},"type":"object","required":["tier","is_keyed","rate_limit","features"],"title":"MeResponse","description":"The `GET /v1/me` payload: who the request is authenticated as, what is\nleft of your request and credit allowances, and which features your plan\nincludes."},"RateLimitInfo":{"properties":{"limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"},"remaining":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Remaining"},"reset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Reset"}},"type":"object","title":"RateLimitInfo","description":"How many requests you may still make in the current window — the same\nnumbers as the `X-RateLimit-*` response headers. `limit` is the size of the\nwindow, `remaining` what is left of it, and `reset` how many SECONDS until\nit refills (not a timestamp). All three are `null` in the rare case the\ncounter cannot be read; treat that as \"unknown\", not zero."},"SyncConvertResponse":{"properties":{"markdown":{"type":"string","title":"Markdown"},"meta":{"additionalProperties":true,"type":"object","title":"Meta"}},"type":"object","required":["markdown"],"title":"SyncConvertResponse","description":"The synchronous (in-process markitdown) lane result — markdown returned\ninline, plus whatever lightweight metadata the converter surfaced."},"Problem":{"description":"The RFC 9457 body that EVERY error response carries.\n\nPublished by :mod:`appkit.openapi` as the schema of every 4xx/5xx in the\nOpenAPI document, replacing FastAPI's stock ``HTTPValidationError`` — which\nno handler in this shell has ever returned, so it documented a shape\ncustomers could not receive. Keep the fields in step with\n:func:`problem_response` below, which builds the body.","properties":{"type":{"default":"about:blank","description":"Problem type URI (RFC 9457).","title":"Type","type":"string"},"title":{"description":"Short human-readable summary of the status.","title":"Title","type":"string"},"status":{"description":"The HTTP status code, repeated in the body.","title":"Status","type":"integer"},"detail":{"description":"What went wrong with THIS request.","title":"Detail","type":"string"},"instance":{"description":"The request path that produced the error.","title":"Instance","type":"string"},"code":{"description":"Stable machine-readable error code — branch on this, never on the prose.","title":"Code","type":"string"},"request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Correlation id for this request; quote it when asking for support.","title":"Request Id"},"errors":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"default":null,"description":"Per-field validation failures. Present on 422 only.","title":"Errors"},"upgrade_url":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Where to add credits or move to a larger plan. Present on 402/403/429.","title":"Upgrade Url"}},"required":["title","status","detail","instance","code"],"title":"Problem","type":"object"},"JobResponsePage":{"properties":{"items":{"items":{"$ref":"#/components/schemas/JobResponse"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","required":["items"],"title":"JobResponsePage"}},"securitySchemes":{"APIKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key"}}},"tags":[{"name":"Convert"},{"name":"Jobs","description":"Track work submitted to an async endpoint — poll a job by id, or list your recent ones. The submitting operation hands you the job id and its poll URL."},{"name":"Account","description":"Check your plan, how many requests and credits you have left, and what your plan includes — `GET /v1/me`, which is always free to call."}]}