{"openapi":"3.1.0","info":{"title":"Dhavana API","version":"1.0.0","summary":"Dhivehi language AI — generation, translation, speech, OCR and documents.","description":"The Dhavana API lets an organisation call Dhavana's Dhivehi language tools\ndirectly from its own systems (document management, HR portals, council\nrecord systems, media CMSs).\n\n## Authentication\nEvery request carries an API key issued to your organisation:\n\n    Authorization: Bearer dhv_live_<key-id>_<secret>\n\nKeys are **organisation-scoped**: usage is billed to the organisation's shared\ncredit pool, and a key can only ever see its own organisation's data. Use a\n`dhv_test_...` key to integrate against deterministic stub responses that cost\nnothing and never call a model.\n\n## Synchronous vs asynchronous\nFast text tools respond directly. Slower tools (speech, OCR, documents, and\nlong generations) return `202` with a **job**: poll `GET /v1/jobs/{id}` or\nregister a `webhook_url` on the request. Polling is always available — many\ngovernment networks cannot accept inbound webhooks.\n\n## Retries and idempotency\nSend an `Idempotency-Key` header on any POST. A retry with the same key and\nbody replays the original response instead of doing (and charging for) the\nwork twice.\n\n## Billing\nRequests are metered in credits against your organisation's pool. Every\nresponse includes `credits_charged`, every credit movement is recorded in\n`GET /v1/ledger`, and `GET /v1/usage` shows the period total against your\nspend cap. Failed work is refunded automatically.\n","contact":{"name":"Dhavana Support","email":"info@dhavana.com","url":"https://dhavana.com"},"termsOfService":"https://dhavana.com/terms"},"servers":[{"url":"https://api.dhavana.com/v1","description":"Production"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"Text","description":"Generation, translation and editing. Synchronous."},{"name":"Speech","description":"Speech-to-text and text-to-speech. Asynchronous."},{"name":"Documents","description":"OCR, presentations and spreadsheets. Asynchronous."},{"name":"Jobs","description":"Poll, list and cancel asynchronous work."},{"name":"Account","description":"Usage, credit ledger, audit trail and webhook subscriptions."},{"name":"Service","description":"Liveness and per-tool status. No key required."}],"paths":{"/health":{"get":{"tags":["Service"],"summary":"Liveness check","security":[],"responses":{"200":{"description":"Service is up.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","examples":["ok"]},"time":{"type":"string","format":"date-time"}}}}}}}}},"/status":{"get":{"tags":["Service"],"summary":"Per-tool operational status","security":[],"responses":{"200":{"description":"Current status of each tool.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["operational","degraded","down"]},"tools":{"type":"object","additionalProperties":{"type":"string","enum":["operational","degraded","down"]}},"time":{"type":"string","format":"date-time"}}}}}}}}},"/openapi.json":{"get":{"tags":["Service"],"summary":"This API description, as JSON","description":"Import this URL directly into Postman, Insomnia or Bruno\n(**Import → Link**), or point an OpenAPI generator at it to produce a\ntyped client. No key required.\n","security":[],"responses":{"200":{"description":"The OpenAPI 3.1 document.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/openapi.yaml":{"get":{"tags":["Service"],"summary":"This API description, as YAML","security":[],"responses":{"200":{"description":"The OpenAPI 3.1 document.","content":{"application/yaml":{"schema":{"type":"string"}}}}}}},"/docs":{"get":{"tags":["Service"],"summary":"Interactive API reference","description":"A browsable reference with a try-it console, rendered from this\nspecification. No key required to read it. Open in a browser.\n","security":[],"responses":{"200":{"description":"The reference page.","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/docs/standalone.js":{"get":{"tags":["Service"],"summary":"Reference renderer bundle","description":"The self-hosted JavaScript bundle used by `/v1/docs`. Served from this\norigin rather than a CDN so the reference works on networks that block\nthird-party hosts. Not intended to be called directly.\n","security":[],"responses":{"200":{"description":"The renderer bundle.","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/guide.md":{"get":{"tags":["Service"],"summary":"Integration guide (Markdown)","description":"The prose companion to this spec: quickstart, retry and error handling,\nworked examples in PHP, C#, Python and TypeScript, and webhook signature\nverification. Rendered at https://dhavana.com/docs/api. No key required.\n","security":[],"responses":{"200":{"description":"The integration guide.","content":{"text/markdown":{"schema":{"type":"string"}}}}}}},"/me":{"get":{"tags":["Account"],"summary":"Describe the calling key","description":"Confirms a key works and reports what it is allowed to do.","responses":{"200":{"description":"The authenticated key's context.","content":{"application/json":{"schema":{"type":"object","properties":{"key_id":{"type":"string"},"organization":{"type":"string"},"environment":{"type":"string","enum":["live","test"]},"scopes":{"type":"array","items":{"type":"string"},"description":"As granted on the key — may include a group such as `documents:write`."},"effective_scopes":{"type":"array","items":{"type":"string"},"description":"What those scopes actually permit, expanded. Use this when\ndebugging a `403 insufficient_scope` — the granted list may\nbe shorthand.\n"},"retention":{"type":"string","enum":["standard","none"]},"department_id":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/scopes":{"get":{"tags":["Account"],"summary":"List available scopes","description":"The full catalogue of scopes a key can be granted, with the endpoints\neach unlocks. Public — readable before you hold a key — because it is\npart of the contract, not customer data.\n\nScope names follow `<endpoint>:<action>`, so `POST /v1/transcribe`\nrequires `transcribe:write`. Group scopes (`text:write`,\n`documents:write`, `media:write`) are shorthand for several at once, and\n`*` grants everything.\n\nAny tool scope also permits polling and cancelling the jobs it creates,\nso an async integration never needs `jobs:*` spelled out.\n","security":[],"responses":{"200":{"description":"The scope catalogue.","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","examples":["list"]},"data":{"type":"array","items":{"type":"object","properties":{"scope":{"type":"string","examples":["transcribe:write"]},"description":{"type":"string"},"endpoints":{"type":"array","items":{"type":"string"}},"group":{"type":"boolean"}}}}}}}}}}}},"/generate":{"post":{"tags":["Text"],"summary":"Generate content","description":"Produces content of a given `content_type`. Responds synchronously by\ndefault; set `async: true` (or supply `webhook_url`) to receive a job\ninstead — useful for long-form types.\n\nCost depends on the content type and `length` tier.\n","operationId":"generate","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["content_type","prompt"],"properties":{"content_type":{"type":"string","description":"What to write.","enum":["article","news article","blog post","story","poem","speech","letter","email","social media","marketing copy","resume/cv","summarize","meeting minutes","smart templates","custom","lesson plan","worksheet","quiz","reading passage","difficulty adjuster","writing prompts","assignment feedback","gamified content","exam prep"]},"prompt":{"type":"string","description":"What the content should be about."},"language":{"type":"string","enum":["en","dv"],"default":"en","description":"Output language. `dv` is Dhivehi (Thaana)."},"length":{"type":"string","enum":["short","medium","long"],"default":"medium"},"options":{"type":"object","description":"Per-type options.","properties":{"writing_style":{"type":"string","enum":["news","report"],"default":"news","description":"`news article` only. `news` is inverted-pyramid reporting;\n`report` produces a longer sectioned document.\n"},"tone":{"type":"string"}},"additionalProperties":true},"async":{"type":"boolean","default":false,"description":"Return a job instead of waiting for the content."},"webhook_url":{"type":"string","format":"uri","description":"Implies `async`. Receives `job.succeeded` / `job.failed`."},"metadata":{"type":"object","description":"Your own reference data, echoed back to you.","additionalProperties":true}}},"examples":{"newsArticle":{"summary":"A short Dhivehi news article","value":{"content_type":"news article","prompt":"Council opens the new harbour at Hulhumale'","language":"dv","length":"short","options":{"writing_style":"news"},"metadata":{"office_ref":"MOE-2026-0412"}}}}}}},"responses":{"200":{"description":"The generated content.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","examples":["generation"]},"content":{"type":"string"},"content_type":{"type":"string"},"word_count":{"type":"integer"},"credits_charged":{"type":"number"},"request_id":{"type":"string"}}}}}},"202":{"$ref":"#/components/responses/JobAccepted"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/translate":{"post":{"tags":["Text"],"summary":"Translate text","operationId":"translate","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text","source_language","target_language"],"properties":{"text":{"type":"string","description":"Text to translate. Maximum 1500 words."},"source_language":{"type":"string","examples":["en"]},"target_language":{"type":"string","examples":["dv"]}}},"examples":{"toDhivehi":{"value":{"text":"The meeting has been moved to Thursday morning.","source_language":"en","target_language":"dv"}}}}}},"responses":{"200":{"description":"The translation.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","examples":["translation"]},"translated_text":{"type":"string"},"source_language":{"type":"string"},"target_language":{"type":"string"},"word_count":{"type":"integer"},"credits_charged":{"type":"number"},"request_id":{"type":"string"}}}}}},"400":{"description":"Invalid request. `text_too_long` when the input exceeds 1500 words;\n`same_language` when source and target match.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/text/edit":{"post":{"tags":["Text"],"summary":"Proofread and improve Dhivehi text","operationId":"edit","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","description":"Text to edit."}}}}}},"responses":{"200":{"description":"The edited text.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","examples":["edit"]},"edited_text":{"type":"string"},"char_count":{"type":"integer"},"credits_charged":{"type":"number"},"request_id":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/transcribe":{"post":{"tags":["Speech"],"summary":"Transcribe Dhivehi speech to text","description":"Asynchronous. Billed by audio duration.\n\nSet `dialect: \"addu\"` for Addu bas — the transcript then preserves Addu\nforms rather than standard Dhivehi spellings, so choose it deliberately.\nThe result reports which `dialect` produced it.\n","operationId":"transcribe","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["audio_url"],"properties":{"audio_url":{"type":"string","format":"uri","description":"Publicly reachable http(s) URL of the audio file."},"dialect":{"type":"string","enum":["standard","addu"],"default":"standard","description":"Dhivehi dialect model. Ignored when `language` is `en`. `addu` output is not auto-corrected, so dialect words survive."},"language":{"type":"string","enum":["dv","en"],"default":"dv","description":"Language of the audio. `dv` uses the Dhivehi model (and also handles Dhivehi/English code-switched speech). `en` uses the English model — pick it only for audio spoken entirely in English."},"identify_speakers":{"type":"boolean","default":false,"description":"Label distinct speakers. Adds 50% to the cost."},"webhook_url":{"type":"string","format":"uri"}}}}}},"responses":{"202":{"$ref":"#/components/responses/JobAccepted"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/speak":{"post":{"tags":["Speech"],"summary":"Synthesise Dhivehi speech","description":"Asynchronous. Billed per character. Maximum 2000 characters.","operationId":"speak","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text","voice"],"properties":{"text":{"type":"string","maxLength":2000},"voice":{"type":"string","description":"A voice id from `GET /v1/voices`.","examples":["ethan"]},"speed":{"type":"number","default":1,"minimum":0.5,"maximum":1.5},"webhook_url":{"type":"string","format":"uri"}}}}}},"responses":{"202":{"$ref":"#/components/responses/JobAccepted"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/voices":{"get":{"tags":["Speech"],"summary":"List available voices","description":"Read this rather than hardcoding voice ids — the catalogue grows.","responses":{"200":{"description":"Available voices.","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","examples":["list"]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/ocr":{"post":{"tags":["Documents"],"summary":"Extract text from an image or PDF","description":"Asynchronous. Billed per page. Multi-page PDFs are supported; the job\nresult reports `page_count`.\n\n**Choose the mode by what is on the page, not by speed** — the two modes\nare different engines, not quality tiers:\n\n| Mode | For | Languages |\n|---|---|---|\n| `accurate` (default) | Printed and typed documents | English, Dhivehi (Thaana) and Arabic, including mixed pages |\n| `handwritten` | Handwritten documents, across varied hands and writing styles | **Dhivehi only** |\n\n`handwritten` has no competence in other scripts — sending English or\nArabic to it produces nonsense rather than a degraded result. Use\n`accurate` for anything printed, whatever the language.\n","operationId":"ocr","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["file_url"],"properties":{"file_url":{"type":"string","format":"uri","description":"Publicly reachable http(s) URL of an image or PDF.\n\nMaximum file size **100 MB**. Larger files are rejected with\n`400 invalid_request_error` before any credits are charged —\nsplit long documents and submit them as separate jobs.\n\nThe URL must be reachable by our servers without\nauthentication, and is fetched once at job start.\n"},"mode":{"type":"string","enum":["accurate","handwritten"],"default":"accurate","description":"`accurate` — printed text in English, Dhivehi or Arabic.\n`handwritten` — handwritten Dhivehi only. Costs more per page.\n"},"webhook_url":{"type":"string","format":"uri"}}}}}},"responses":{"202":{"$ref":"#/components/responses/JobAccepted"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/presentations":{"post":{"tags":["Documents"],"summary":"Generate a PowerPoint presentation","description":"Asynchronous. The job result contains a time-limited `download_url`.","operationId":"createPresentation","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["topic"],"properties":{"topic":{"type":"string"},"slide_count":{"type":"integer","minimum":1},"tier":{"type":"string","enum":["smart","premium"],"default":"smart"},"language":{"type":"string","enum":["dv","en","auto"],"default":"auto"},"include_images":{"type":"boolean","default":false,"description":"Add photographs and generated backgrounds. Costs extra."},"webhook_url":{"type":"string","format":"uri"}}}}}},"responses":{"202":{"$ref":"#/components/responses/JobAccepted"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/spreadsheets":{"post":{"tags":["Documents"],"summary":"Generate an Excel workbook","description":"Asynchronous. The job result contains a time-limited `download_url`.","operationId":"createSpreadsheet","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string","description":"What the workbook should contain."},"tier":{"type":"string","enum":["smart","premium"],"default":"smart"},"language":{"type":"string","enum":["dv","en","auto"],"default":"auto"},"data":{"type":"object","description":"Optional source data as `{ \"Sheet name\": [[row], [row]] }`.\nSupply this to build a workbook from figures you already hold.\n","additionalProperties":{"type":"array","items":{"type":"array","items":{}}}},"webhook_url":{"type":"string","format":"uri"}}}}}},"responses":{"202":{"$ref":"#/components/responses/JobAccepted"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/jobs/{id}":{"get":{"tags":["Jobs"],"summary":"Retrieve a job","operationId":"getJob","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The job.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/jobs":{"get":{"tags":["Jobs"],"summary":"List jobs","operationId":"listJobs","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["queued","processing","succeeded","failed","canceled"]}},{"name":"tool","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}}],"responses":{"200":{"description":"A list of jobs.","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","examples":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Job"}},"has_more":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/jobs/{id}/cancel":{"post":{"tags":["Jobs"],"summary":"Cancel a job","description":"Only `queued` or `processing` jobs can be cancelled. Credits are refunded.","operationId":"cancelJob","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The cancelled job.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"400":{"description":"The job has already finished.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/usage":{"get":{"tags":["Account"],"summary":"Usage and spend for a period","description":"What your organisation has spent through the API this period, broken down\nby tool, against your spend cap — plus the current pool balance and a\nmonth-end projection.\n","operationId":"getUsage","parameters":[{"name":"period","in":"query","description":"Billing period as `YYYY-MM`. Defaults to the current period.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}$"}}],"responses":{"200":{"description":"Usage for the period.","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","examples":["usage"]},"organization":{"type":"string"},"period":{"type":"string"},"credits_spent":{"type":"number"},"by_tool":{"type":"object","additionalProperties":{"type":"number"}},"spend_cap":{"type":["number","null"],"description":"Null when the organisation has no API spend cap."},"cap_remaining":{"type":["number","null"]},"cap_used_percent":{"type":["number","null"]},"soft_limit_percent":{"type":"number"},"soft_limit_reached":{"type":"boolean"},"balance":{"type":"number"},"low_balance_threshold":{"type":["number","null"]},"low_balance":{"type":"boolean"},"projected_month_end":{"type":"number"},"request_id":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/ledger":{"get":{"tags":["Account"],"summary":"Credit statement","description":"Every credit movement your keys caused, newest first — each traceable to\nthe request or job that produced it. `summary.spend_total` reconciles\nexactly with `credits_spent` from `GET /v1/usage` for the same period.\n\nPass `format=csv` for a spreadsheet-ready export.\n","operationId":"getLedger","parameters":[{"name":"period","in":"query","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}$"}},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"tool","in":"query","schema":{"type":"string"}},{"name":"type","in":"query","schema":{"type":"string","enum":["spend","refund"]}},{"name":"job_id","in":"query","schema":{"type":"string"}},{"name":"request_id","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"minimum":1,"maximum":1000}},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"],"default":"json"}}],"responses":{"200":{"description":"Credit movements.","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","examples":["list"]},"period":{"type":["string","null"]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"type":{"type":"string","enum":["spend","refund"]},"amount":{"type":"number","description":"Signed — negative is charged, positive is returned."},"balance_after":{"type":["number","null"]},"tool":{"type":"string"},"reason":{"type":["string","null"]},"request_id":{"type":["string","null"]},"job_id":{"type":["string","null"]},"key_id":{"type":["string","null"]},"environment":{"type":"string","enum":["live","test"]}}}},"summary":{"type":"object","properties":{"spend_total":{"type":"number"},"refund_total":{"type":"number"},"net":{"type":"number"},"spend_count":{"type":"integer"},"refund_count":{"type":"integer"}}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}},"text/csv":{"schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/audit":{"get":{"tags":["Account"],"summary":"Audit trail","description":"Every authenticated call your keys made — when, from which source IP, with\nwhat outcome. Retained for 12 months. Pass `format=csv` to export.\n","operationId":"getAudit","parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"tool","in":"query","schema":{"type":"string"}},{"name":"key_id","in":"query","schema":{"type":"string"}},{"name":"outcome","in":"query","schema":{"type":"string","enum":["success","error"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"minimum":1,"maximum":1000}},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"],"default":"json"}}],"responses":{"200":{"description":"Audit records.","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","examples":["list"]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"request_id":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"method":{"type":"string"},"path":{"type":"string"},"tool":{"type":["string","null"]},"key_id":{"type":"string"},"environment":{"type":"string","enum":["live","test"]},"source_ip":{"type":"string"},"status_code":{"type":"integer"},"outcome":{"type":"string","enum":["success","error"]},"error_code":{"type":["string","null"]},"credits_charged":{"type":"number"},"duration_ms":{"type":"integer"}}}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}},"text/csv":{"schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/webhook-endpoints":{"get":{"tags":["Account"],"summary":"List webhook endpoints","operationId":"listWebhookEndpoints","responses":{"200":{"description":"Registered endpoints. The signing secret is never returned here.","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","examples":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Account"],"summary":"Register a webhook endpoint","description":"Receives **account** events — credit and spend-cap notifications. Job\nnotifications use the `webhook_url` field on the request that created the\njob instead.\n\nThe signing secret is returned **once**, in this response, and cannot be\nretrieved later. To rotate it, delete the endpoint and create a new one.\n","operationId":"createWebhookEndpoint","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"An https URL on a public host."},"events":{"type":"array","description":"Events to receive. Omit or leave empty for all.","items":{"type":"string","enum":["credits.low","credits.exhausted","api.spend_cap_warning","api.spend_cap_reached"]}},"description":{"type":"string","maxLength":200}}}}}},"responses":{"201":{"description":"Created. Store `secret` now — it is not shown again.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEndpoint"},{"type":"object","properties":{"secret":{"type":"string","description":"Signing secret. Shown only in this response."}}}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/webhook-endpoints/{id}":{"delete":{"tags":["Account"],"summary":"Delete a webhook endpoint","operationId":"deleteWebhookEndpoint","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","examples":["webhook_endpoint"]},"deleted":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","description":"Your organisation's API key, e.g. `dhv_live_k_abc123_secret`. Treat it\nlike a password: server-side only, never in a browser or mobile app.\n"}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"description":"A unique value (a UUID is ideal) identifying this request. Retrying with\nthe same key and body replays the original response rather than doing and\ncharging for the work twice. Records are kept for 24 hours.\n","schema":{"type":"string","minLength":8,"maxLength":255}}},"schemas":{"Error":{"type":"object","description":"Every error uses this shape. Switch on `code` — it is the stable machine\ncontract; `message` is human-readable and its wording may change.\n","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["invalid_request_error","authentication_error","permission_error","not_found_error","rate_limit_error","insufficient_credits","upstream_error","api_error"]},"code":{"type":"string","description":"Specific machine-readable cause, e.g. `invalid_api_key`,\n`insufficient_scope` (see `GET /v1/scopes`),\n`ip_not_allowed`, `insufficient_scope`, `insufficient_credits`,\n`spend_cap_exceeded`, `member_limit_exceeded`,\n`department_budget_exceeded`, `rate_limited`, `content_blocked`,\n`text_too_long`, `idempotency_key_reuse`, `request_in_progress`.\n"},"message":{"type":"string"},"param":{"type":"string","description":"The offending request field, when applicable."},"request_id":{"type":"string"},"doc_url":{"type":"string"}}}}},"Job":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","examples":["job"]},"tool":{"type":"string"},"status":{"type":"string","enum":["queued","processing","succeeded","failed","canceled"]},"progress":{"type":"integer","description":"Best-effort completion percentage, 0-100."},"result":{"type":["object","null"],"description":"Present once `status` is `succeeded`. Shape depends on the tool:\ntranscribe → `{ text, duration, dialect }`;\nspeak → `{ audio_base64, format, voice }`;\nocr → `{ text, page_count, mode }`;\npresentations / spreadsheets → `{ download_url }`;\ngenerate → `{ content, word_count }`.\n","additionalProperties":true},"error":{"type":["object","null"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}},"credits_charged":{"type":"number"},"created_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"}}},"WebhookEndpoint":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","examples":["webhook_endpoint"]},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"}},"description":{"type":["string","null"]},"status":{"type":"string","enum":["active","disabled"]},"last_delivery_at":{"type":["string","null"],"format":"date-time"},"last_delivery_ok":{"type":["boolean","null"]},"consecutive_failures":{"type":"integer"},"created_at":{"type":"string","format":"date-time"}}}},"responses":{"JobAccepted":{"description":"Accepted. The work runs in the background — poll `GET /v1/jobs/{id}` or\nwait for your webhook.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"BadRequest":{"description":"The request was malformed or failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Missing, malformed, revoked or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"The key is valid but not permitted — it lacks the required scope\n(`insufficient_scope`) or the request came from an address outside the\nkey's IP allowlist (`ip_not_allowed`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"No such resource for this organisation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PaymentRequired":{"description":"The request was not billable. Distinguish the causes by `error.code`:\n\n- `insufficient_credits` — the shared credit pool cannot cover it. Buying\n  or granting credits resolves it.\n- `spend_cap_exceeded` — the organisation's API spend cap for the period\n  has been reached. An administrator raises the cap.\n- `department_budget_exceeded` — the organisation HAS the credits, but a\n  department budget refuses the spend. Buying credits will NOT help; an\n  organisation owner or billing admin must raise the budget. The body\n  carries `limit`, `spent` and `raisedBy`.\n- `member_limit_exceeded` — as above, for an individual member's monthly\n  limit. Not reachable with an API key today, since a key spends as the\n  organisation rather than as a member.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Too many requests. Retry after the interval in `Retry-After`. Limits apply\nper key and, at a higher ceiling, per organisation.\n","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."},"RateLimit-Limit":{"schema":{"type":"integer"}},"RateLimit-Remaining":{"schema":{"type":"integer"}},"RateLimit-Reset":{"schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-webhooks-note":"Every delivery carries:\n  X-Dhavana-Signature: t=<unix-seconds>,v1=<hex hmac-sha256>\n  X-Dhavana-Event-Id:  evt_...        (matches `id` in the body)\nVerify by recomputing HMAC-SHA256 over the exact string \"<t>.<raw request\nbody>\" using your endpoint's signing secret, comparing in constant time, and\nrejecting timestamps older than five minutes.\n"},"webhooks":{"accountEvent":{"post":{"summary":"Account event","description":"Sent to your registered webhook endpoints when your organisation's credit\nor spend-cap state changes. Each event fires once per occurrence — you are\nnot flooded while blocked or out of credit.\n\nRespond `2xx` to acknowledge. Failed deliveries are retried with backoff;\ndelivery never affects your requests or job results.\n","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Matches the X-Dhavana-Event-Id header."},"type":{"type":"string","enum":["credits.low","credits.exhausted","api.spend_cap_warning","api.spend_cap_reached"]},"created":{"type":"integer","description":"Unix timestamp."},"data":{"type":"object","properties":{"organization":{"type":"string"},"balance_credits":{"type":"number"},"threshold_credits":{"type":"number"},"needed_credits":{"type":"number"},"period":{"type":"string"},"spend_cap":{"type":"number"},"spent_credits":{"type":"number"},"cap_remaining":{"type":"number"},"soft_limit_percent":{"type":"number"}},"additionalProperties":true}}}}}},"responses":{"200":{"description":"Acknowledged."}}}}}}