# Dhanu Developer documentation (complete) > Every page of the Developer section of docs.dhanu.ai in one file. The index with one line per page is at https://docs.dhanu.ai/llms.txt. --- # Quickstart Source: https://docs.dhanu.ai/developer/overview/quickstart Get a key, verify it, and make your first generation against the Dhanu public API in a few minutes. import CodeGroup from "@/components/code-group.astro"; The Dhanu public API lets your own server generate images and text, metered against your workspace's credit balance. Everything below works with `curl`, the Node SDK, or the Python SDK, nothing here requires installing a package. **Base URL**: `https://api.dhanu.ai`, with every public endpoint under `/api/pub/v1`. ## 1. Create a project, get a key Open **Developers → Projects** in the Dhanu app and create a project. A project groups the keys for one app or environment, and creating one **mints its first secret key**. Start here: the **API keys** page stays empty until a project exists, because every key belongs to at least one project. Keys look like `dhanu_sk_test_…` and are **shown once**, store it somewhere safe immediately. If you lose one, delete it and create a new key. Each key carries two scopes, both chosen when you create it: - **Projects**: which projects the key acts on, and which project each generation is metered against. - **API scope**: how much of the API it may call: the whole app, selected packages, or selected APIs. See [API keys](/developer/authentication/api-keys) for both in full. > Your secret key is a server-side secret. Never ship it to a browser, a mobile > app, or a public repository, anyone holding it can spend your workspace's > credits. ## 2. Verify it `GET /me` is the cheapest way to confirm a key works and see what it can reach. ```sh curl https://api.dhanu.ai/api/pub/v1/me \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts import { Dhanu } from "dhanu"; const dhanu = new Dhanu(process.env.DHANU_API_KEY!); const me = await dhanu.me(); console.log(me.projects, me.scope); ``` ```python import os from dhanu import Dhanu dhanu = Dhanu(os.environ["DHANU_API_KEY"]) me = dhanu.me() print(me.projects, me.scope) ``` ```json { "app": { "id": "…", "name": "Website" }, "projects": [{ "id": "…", "name": "Website" }], "key": { "name": "production" }, "scope": { "level": "app", "packages": ["text", "images"], "operations": ["text.generate", "images.generate"] } } ``` `projects` lists everything the key can reach. `app` is the single scoped project, or `null` when the key is scoped to more than one. `scope` shows which packages and APIs the key may call; `GET /reference` expands that into the full catalogue. ## 3. Generate something ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/text \ -H "Authorization: Bearer $DHANU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "Write a haiku about Kathmandu"}' ``` ```ts import { Dhanu } from "dhanu"; const dhanu = new Dhanu(process.env.DHANU_API_KEY!); const reply = await dhanu.text.generate({ prompt: "Write a haiku about Kathmandu", }); ``` ```python import os from dhanu import Dhanu dhanu = Dhanu(os.environ["DHANU_API_KEY"]) reply = dhanu.text.generate(prompt="Write a haiku about Kathmandu") ``` ```json { "text": "…", "model": "…", "usage": { "inputTokens": 12, "outputTokens": 41 }, "cost": { "amount": 28, "currency": "NPR" } } ``` `cost.amount` is in the currency's **minor unit**, 28 means NPR 0.28. If your key covers **more than one project**, name the one to meter against with `projectId`. Without it the call fails with `project_id_required` (422): ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/text \ -H "Authorization: Bearer $DHANU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "Write a haiku about Kathmandu", "projectId": "…"}' ``` A single-project key can omit it, `GET /me` tells you which case you are in. ## 4. Or use an SDK Both SDKs are thin wrappers over the endpoints above, with no dependencies. ```ts // npm i dhanu import { Dhanu } from "dhanu"; const dhanu = new Dhanu(process.env.DHANU_API_KEY!); const reply = await dhanu.text.generate({ prompt: "Write a haiku about Kathmandu", }); ``` ```python # pip install dhanu import os from dhanu import Dhanu dhanu = Dhanu(os.environ["DHANU_API_KEY"]) reply = dhanu.text.generate(prompt="Write a haiku about Kathmandu") ``` See [Node SDK](/developer/sdks/node) and [Python SDK](/developer/sdks/python) for the full surface. ## 5. Discover the rest `GET /reference` returns every package and API, flagged with what your key is allowed to call, the whole catalogue over plain HTTP, no package needed: ```sh curl https://api.dhanu.ai/api/pub/v1/reference \ -H "Authorization: Bearer $DHANU_API_KEY" ``` **Developers → Usage** in the app shows what you have spent. ## What to read next - [API keys](/developer/authentication/api-keys): project and API scopes, revoking - [Generate an image](/developer/reference/generate-an-image) - [Generate text](/developer/reference/generate-text) - [List the API catalogue](/developer/reference/list-the-api-catalogue) - [Errors](/developer/reference/errors) and [Rate limits](/developer/reference/rate-limits) --- # छिटो सुरुवात Source: https://docs.dhanu.ai/ne/developer/overview/quickstart केही मिनेटमै धनुको सार्वजनिक एपीआईमा साँचो लिनुहोस्, जाँच्नुहोस्, र पहिलो जेनेरेसन गर्नुहोस्। import CodeGroup from "@/components/code-group.astro"; > **महत्त्वपूर्ण:** यो पृष्ठ मेसिनले गरेको अनुवादको मस्यौदा हो र समीक्षामा छ। कुनै कुरा > नमिलेमा [अङ्ग्रेजी पृष्ठ](/developer/overview/quickstart) लाई आधिकारिक मान्नुहोस्। धनुको सार्वजनिक एपीआईले तपाईंको आफ्नै सर्भरलाई तस्बिर र पाठ जेनेरेट गर्न दिन्छ, र खर्च तपाईंको कार्यस्थलको क्रेडिट ब्यालेन्सबाट गनिन्छ। तलका सबै कुरा `curl`, Node SDK वा Python SDK बाट चल्छन्, यहाँ कुनै प्याकेज इन्स्टल गर्नु पर्दैन। **आधार URL**: `https://api.dhanu.ai`, र हरेक सार्वजनिक एन्डपोइन्ट `/api/pub/v1` अन्तर्गत हुन्छ। ## १. परियोजना बनाउनुहोस्, साँचो लिनुहोस् धनु एपमा **Developers → Projects** खोल्नुहोस् र एउटा परियोजना बनाउनुहोस्। परियोजनाले एउटा एप वा वातावरणका साँचोहरू समेट्छ, र परियोजना बनाउँदा **त्यसको पहिलो गोप्य साँचो बन्छ**। यहीँबाट सुरु गर्नुहोस्: परियोजना नभएसम्म **API keys** पृष्ठ खाली रहन्छ, किनभने हरेक साँचो कम्तीमा एउटा परियोजनाको हुन्छ। साँचो `dhanu_sk_test_…` जस्तो देखिन्छ र **एक पटक मात्र देखाइन्छ**, तुरुन्तै सुरक्षित ठाउँमा राख्नुहोस्। हराएमा त्यसलाई मेटाएर नयाँ साँचो बनाउनुहोस्। हरेक साँचोमा दुईवटा दायरा हुन्छन्, दुवै साँचो बनाउँदा रोजिन्छन्: - **Projects**: साँचोले कुन परियोजनाहरूमा काम गर्छ, र हरेक जेनेरेसनको खर्च कुन परियोजनामा गनिन्छ। - **API scope**: साँचोले एपीआईको कति भाग चलाउन पाउँछ: पूरै एप, छानिएका प्याकेजहरू, वा छानिएका एपीआईहरू। दुवैको पूरा विवरण [API keys](/ne/developer/authentication/api-keys) मा हेर्नुहोस्। > तपाईंको गोप्य साँचो सर्भरमा मात्र राखिने गोप्य कुरा हो। यसलाई ब्राउजर, मोबाइल एप वा > सार्वजनिक रिपोजिटरीमा कहिल्यै नपठाउनुहोस्, यो जससँग हुन्छ उसले तपाईंको कार्यस्थलको > क्रेडिट खर्च गर्न सक्छ। ## २. जाँच्नुहोस् साँचो चल्छ कि चल्दैन र त्यसले के-के छुन सक्छ भनी हेर्ने सबैभन्दा सस्तो तरिका `GET /me` हो। ```sh curl https://api.dhanu.ai/api/pub/v1/me \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts import { Dhanu } from "dhanu"; const dhanu = new Dhanu(process.env.DHANU_API_KEY!); const me = await dhanu.me(); console.log(me.projects, me.scope); ``` ```python import os from dhanu import Dhanu dhanu = Dhanu(os.environ["DHANU_API_KEY"]) me = dhanu.me() print(me.projects, me.scope) ``` ```json { "app": { "id": "…", "name": "Website" }, "projects": [{ "id": "…", "name": "Website" }], "key": { "name": "production" }, "scope": { "level": "app", "packages": ["text", "images"], "operations": ["text.generate", "images.generate"] } } ``` `projects` ले साँचोले छुन सक्ने सबै कुरा देखाउँछ। `app` भनेको दायरामा परेको एउटै परियोजना हो, र साँचो एकभन्दा बढी परियोजनामा छ भने यो `null` हुन्छ। `scope` ले साँचोले कुन प्याकेज र एपीआई चलाउन पाउँछ भनी देखाउँछ; `GET /reference` ले त्यसलाई पूरा सूचीमा फैलाउँछ। ## ३. केही जेनेरेट गर्नुहोस् ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/text \ -H "Authorization: Bearer $DHANU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "Write a haiku about Kathmandu"}' ``` ```ts import { Dhanu } from "dhanu"; const dhanu = new Dhanu(process.env.DHANU_API_KEY!); const reply = await dhanu.text.generate({ prompt: "Write a haiku about Kathmandu", }); ``` ```python import os from dhanu import Dhanu dhanu = Dhanu(os.environ["DHANU_API_KEY"]) reply = dhanu.text.generate(prompt="Write a haiku about Kathmandu") ``` ```json { "text": "…", "model": "…", "usage": { "inputTokens": 12, "outputTokens": 41 }, "cost": { "amount": 28, "currency": "NPR" } } ``` `cost.amount` मुद्राको **सानो एकाइ** मा हुन्छ, 28 भनेको NPR 0.28 हो। तपाईंको साँचो **एकभन्दा बढी परियोजना** मा छ भने खर्च गनिने परियोजना `projectId` ले तोक्नुहोस्। नतोके कल `project_id_required` (422) भएर असफल हुन्छ: ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/text \ -H "Authorization: Bearer $DHANU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "Write a haiku about Kathmandu", "projectId": "…"}' ``` एउटै परियोजनाको साँचोमा यो छोड्न सकिन्छ, तपाईं कुन अवस्थामा हुनुहुन्छ भनी `GET /me` ले बताउँछ। ## ४. वा SDK प्रयोग गर्नुहोस् दुवै SDK माथिका एन्डपोइन्टहरूमाथिका पातला आवरण हुन्, कुनै निर्भरता बिना। ```ts // npm i dhanu import { Dhanu } from "dhanu"; const dhanu = new Dhanu(process.env.DHANU_API_KEY!); const reply = await dhanu.text.generate({ prompt: "Write a haiku about Kathmandu", }); ``` ```python # pip install dhanu import os from dhanu import Dhanu dhanu = Dhanu(os.environ["DHANU_API_KEY"]) reply = dhanu.text.generate(prompt="Write a haiku about Kathmandu") ``` पूरा सुविधाका लागि [Node SDK](/ne/developer/sdks/node) र [Python SDK](/ne/developer/sdks/python) हेर्नुहोस्। ## ५. बाँकी पत्ता लगाउनुहोस् `GET /reference` ले हरेक प्याकेज र एपीआई फर्काउँछ, र तपाईंको साँचोले के चलाउन पाउँछ भनी चिन्ह लगाउँछ, पूरा सूची सादा HTTP मै, कुनै प्याकेज चाहिँदैन: ```sh curl https://api.dhanu.ai/api/pub/v1/reference \ -H "Authorization: Bearer $DHANU_API_KEY" ``` तपाईंले कति खर्च गर्नुभयो भनी एपको **Developers → Usage** मा देखिन्छ। ## अब के पढ्ने - [API keys](/ne/developer/authentication/api-keys): परियोजना र एपीआई दायरा, साँचो रद्द गर्ने - [तस्बिर जेनेरेट गर्नुहोस्](/ne/developer/reference/generate-an-image) - [पाठ जेनेरेट गर्नुहोस्](/ne/developer/reference/generate-text) - [एपीआई सूची हेर्नुहोस्](/ne/developer/reference/list-the-api-catalogue) - [त्रुटिहरू](/ne/developer/reference/errors) र [दर सीमा](/ne/developer/reference/rate-limits) --- # Sandbox workspace Source: https://docs.dhanu.ai/developer/overview/sandbox One click in the developer console creates a throwaway workspace with sample staff, a project, tasks, a workflow and a file, plus a test key, so you can try every read endpoint against real rows. A [test key](/developer/guides/test-to-live) answers generation calls with fixtures, but the endpoints that **read**, tasks, staff, workflows, files, projects, return whatever the workspace holds. A brand-new workspace holds nothing, which makes the first hour with the API oddly empty. The sandbox fixes that. ## Create one In the developer console, **Get started → Create a sandbox workspace**. Dhanu makes a new personal workspace called *API Sandbox* owned by you and seeds it with: - two staff members hired from the template catalogue - a project with two tasks - a workflow with one `{{topic}}` field - a README file in your drive - a developer project named *Sandbox* with a **test-mode key**, shown once Your browser switches to the sandbox and the key is on screen. Nothing in it costs credits to read, and generation calls with the test key spend nothing either. ```sh export DHANU_API_KEY=dhanu_sk_test_… curl https://api.dhanu.ai/api/pub/v1/staff -H "Authorization: Bearer $DHANU_API_KEY" curl https://api.dhanu.ai/api/pub/v1/workflows -H "Authorization: Bearer $DHANU_API_KEY" curl https://api.dhanu.ai/api/pub/v1/tasks -H "Authorization: Bearer $DHANU_API_KEY" ``` ## When you are done It is an ordinary workspace: it counts towards your workspace limit, and you delete it from **Settings → Workspaces** like any other. Make as many as the limit allows; each one is seeded the same way. --- # API keys Source: https://docs.dhanu.ai/developer/authentication/api-keys How Dhanu API keys authenticate, how project and capability scopes limit what a key can reach, and how to rotate or revoke one. import CodeGroup from "@/components/code-group.astro"; Every public API request authenticates with a bearer key: ``` Authorization: Bearer dhanu_sk_test_… ``` There is no other credential. Session cookies cannot reach the public API, and an API key cannot reach the app's management API, the two are kept completely separate. ## Test keys and live keys Every key is minted in one of two **modes**, baked into its prefix and fixed for the life of the key: | Prefix | Mode | What happens on a call | | ---------------- | ---- | ----------------------------------------------------------------------------------------------- | | `dhanu_sk_test_` | Test | Never spends credits. Generation endpoints return fixture output; a trigger returns a fixture task and creates nothing. | | `dhanu_sk_live_` | Live | Runs real models and real staff, billed to the workspace's credits. | Use a test key in CI and local development, and switch to a live key when you deploy. Both modes go through the same authentication, scope checks, validation, rate limits and idempotency, and return the same response shapes, a client that works against a test key works against a live one. `GET /me` reports the mode as `key.mode`. Keys created before modes existed are test keys; mint a live key to spend credits. ## Expiry A key can be given an expiry when it is created, 7, 30, 90 or 365 days, or never. An expired key is refused with `api_key_expired` (401) exactly like a revoked one, and the console shows how long each key has left. Short-lived keys for CI and one-off integrations limit how long a leaked key can be used; a service that must run unattended can still hold a key that never expires. ## Monthly spend cap A key can carry a **monthly spend cap** in credits. Once the key's generation calls have spent that much in a calendar month (UTC), further calls fail with `key_spend_limit_reached` (402) until the month rolls over or the cap is raised; the key's creator is notified at 80% and at 100%. The cap counts image and text generation made with that key, staff runs started through a trigger are billed to the task and are not counted. The console shows each key's spend against its cap. ## IP allowlist A key can be limited to a list of up to 20 IPv4 or IPv6 addresses or CIDR ranges. A call from any other address is refused with `api_key_ip_not_allowed` (403) before it runs. Leave the list empty to allow any address. The console shows the address each key was last used from, and flags keys that have gone 90 days without use so you can revoke what you no longer need. ## Keys are shown once When you create a key, the secret is displayed exactly once. Only a hash of it is stored, so it cannot be recovered afterwards. If you lose it, delete the key and create a new one, there is no way to reveal an existing secret again. ## Project scopes A key belongs to a workspace and is scoped to an explicit set of **projects**. You must select at least one project when creating a key, and every generation is metered against a project in that scope. How the acting project is chosen: - **Single-project key**: the project is implied. You can omit `projectId`. - **Multi-project key**: you must pass `projectId` on each generation request. Omitting it fails with `project_id_required` (422). - **Out of scope**: passing a `projectId` the key does not cover fails with `api_key_project_scope` (403). A key whose scope resolves to zero active projects, because they were all deselected or the projects were archived, stops working entirely, with `api_key_project_scope` (403), before any endpoint runs. ## Capability scopes Projects answer *whose data* a key acts on. Capability scope answers *which APIs* it may call. You choose one of three levels when you create the key: | Level | The key can call | | --------- | ----------------------------------------------------------------------- | | `app` | Every API, including ones added to the platform later. The default. | | `package` | Only the packages you pick, growing as APIs are added to those packages. | | `api` | Only the exact APIs you pick. Nothing is granted automatically. | Packages group related APIs, `text` covers `text.generate`, `images` covers `images.generate`. Pick `package` when you want a key that follows a product area as it grows; pick `api` when you want a key frozen to exactly what it does today. Calling an API outside the key's capability scope fails with `api_key_operation_scope` (403) before the request runs, so a narrowed key never spends credits on a call it was not meant to make. Both scopes apply together: a request must be inside the key's project scope **and** its capability scope. ## Discovering what a key can call `/reference` lists the whole catalogue and marks what the calling key is allowed to use, so you can build against the API without installing an SDK: ```sh curl https://api.dhanu.ai/api/pub/v1/reference \ -H "Authorization: Bearer $DHANU_API_KEY" ``` Each package lists its APIs with the `method`, `path`, whether the call is `billed`, and an `allowed` flag for this key. `/me` and `/reference` are introspection endpoints, any live key can call them at any scope level, since they only reveal what the holder of the key already has. ## Access is tied to its creator A key keeps working only while the person who created it still has developer access in that workspace. If their Developer role is removed, or they are removed from or demoted in the workspace, the key stops working with `developer_access_revoked` (403). This is deliberate: a key that leaked, or that an ex-colleague kept, dies with their access rather than outliving it. ## If a key leaks to GitHub Dhanu is registered with GitHub's secret scanning: when a `dhanu_sk_…` key is pushed to a public repository, GitHub reports it to us, we **revoke it at once**, record it in the workspace's security audit log as `api_key.leaked`, and email the person who created the key with where it was found. Anything using the key fails from that moment, create a replacement and remove the old value from wherever it was published. ## Replacing a leaked key To roll a key, create the replacement **first**, deploy it, then delete the old one, that way the integration never has a window with no working key. Both actions live in **Developers → API keys**; deleting is immediate and permanent. A key can also be revoked through the API (`POST /developer-keys/:keyId/revoke`), which keeps the row for audit instead of removing it. A revoked key fails with `api_key_revoked` (401) and cannot be brought back. Narrowing a key's scope is the softer option: if a key reaches further than it should, edit its projects or API scope rather than replacing it. ## Checking a key ```sh curl https://api.dhanu.ai/api/pub/v1/me \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts import { Dhanu } from "dhanu"; const dhanu = new Dhanu(process.env.DHANU_API_KEY!); const me = await dhanu.me(); console.log(me.projects, me.scope); ``` ```python import os from dhanu import Dhanu dhanu = Dhanu(os.environ["DHANU_API_KEY"]) me = dhanu.me() print(me.projects, me.scope) ``` A 200 tells you the key is live and shows exactly which projects it can reach and which packages and APIs its scope allows. A 401 means the key is unknown or revoked; a 403 means it is real but no longer permitted. --- # Build a support bot on a staff member Source: https://docs.dhanu.ai/developer/guides/support-bot Give a staff member your support inbox, trigger it from your backend for every ticket, read the reply back, and post it where your customers are. import CodeGroup from "@/components/code-group.astro"; A staff member is an AI colleague with a name, a brief and the workspace's knowledge behind it. Handing it a support ticket is one request; reading its answer back is one more. This guide wires that into a backend that receives tickets from anywhere, a help-desk webhook, a form, a chat app. ## 1. Set up the staff member In the app, create a staff member (Team → Staff) and write its brief: what your product is, how to answer, when to escalate. Add the pages it should know to the brain. On its **Channels** tab, switch on the **API** channel, that mints a trigger address like `stf_a1b2c3` that you will paste into your backend. ## 2. Mint a key In the developer console, create a key scoped to the `tasks` package with the **test** mode while you build (it answers with fixtures and spends nothing), then a live one when you go to production. Keep it in `DHANU_API_KEY`. ## 3. Trigger the staff member for each ticket Trigger with the ticket text as `input` and the ticket id as `threadId`, so a follow-up from the same customer continues the same task instead of starting a new one. Pass the ticket id as `idempotencyKey` too, so a retried webhook cannot create two tasks. ```ts import { Dhanu } from "dhanu"; const dhanu = new Dhanu(process.env.DHANU_API_KEY!); export async function handleTicket(ticket: { id: string; subject: string; body: string }) { const task = await dhanu.tasks.trigger({ address: process.env.SUPPORT_STAFF_ADDRESS!, input: ticket.body, subject: ticket.subject, threadId: ticket.id, idempotencyKey: `ticket-${ticket.id}`, }); return task.id; } ``` ```python import os from dhanu import Dhanu dhanu = Dhanu(os.environ["DHANU_API_KEY"]) def handle_ticket(ticket): task = dhanu.tasks.trigger( address=os.environ["SUPPORT_STAFF_ADDRESS"], input=ticket["body"], subject=ticket["subject"], thread_id=ticket["id"], idempotency_key="ticket-" + ticket["id"], ) return task.id ``` ## 4. Read the reply back The trigger returns as soon as the task exists; the staff member answers in the background. Two ways to get the answer: **Stream it**: hold a connection until the reply lands. Good for a chat widget or a worker that handles one ticket at a time. ```ts for await (const message of dhanu.tasks.stream(taskId)) { await helpdesk.reply(ticket.id, message.text); } ``` ```python for message in dhanu.tasks.stream(task_id): helpdesk.reply(ticket["id"], message.text) ``` **Subscribe to the webhook**: register an endpoint for `task.message.created` (see [receive webhooks](/developer/guides/receive-webhooks)); each delivery carries the task id and the message, so a stateless backend can post the reply without holding anything open. ## 5. Keep it safe Everything you send is treated as untrusted text: the staff member reports on a ticket that tells it to "ignore your instructions", it never obeys it. The run is non-interactive, so it can reach the brain and its tools but never a person's local folders or browser. Scope the key to `tasks.trigger` and `tasks.read` so a leaked key can hand the bot work but nothing else, and set a monthly spend cap on it. --- # Run a workflow from your backend Source: https://docs.dhanu.ai/developer/guides/run-a-workflow Workflows are reusable requests with a form attached, list them, fill the form from your own data, and run them on a schedule or on an event. import CodeGroup from "@/components/code-group.astro"; A workflow is a prompt someone in the workspace has already written and tested, with **typed fields** for what changes each time, the week to report on, the customer to write to, the file to summarise. Running one from your backend means the prompt lives with the people who own it, and your code only supplies the values. ## 1. Find the workflow and its fields Every workflow the key's creator can run comes back with its fields, key, type, label, whether it is required, and the options of a select. ```ts const workflows = await dhanu.workflows.list(); const weekly = workflows.find((w) => w.title === "Weekly sales summary")!; console.log(weekly.fields); // [{ key: "week", type: "text", required: true }, { key: "region", type: "select", options: [...] }] ``` ```python weekly = next(w for w in dhanu.workflows.list() if w.title == "Weekly sales summary") print(weekly.fields) ``` ## 2. Run it with values Values are validated **on the server** against the stored fields, a missing required field or an option that is not in the list answers `invalid_workflow_values`: and the run starts an ordinary task assigned to whatever the workflow says. Send an idempotency key derived from your schedule so a retried cron job runs it once. ```ts const task = await dhanu.workflows.run( weekly.id, { week: "2026-W38", region: "Bagmati" }, { idempotencyKey: "weekly-sales-2026-W38" }, ); ``` ```python task = dhanu.workflows.run( weekly.id, {"week": "2026-W38", "region": "Bagmati"}, idempotency_key="weekly-sales-2026-W38", ) ``` ## 3. Collect the result The task's reply arrives like any other: stream it, or subscribe to `task.completed` and read the messages when the delivery lands. A workflow that produces a document, a page, a sheet, a deck, leaves it on the task; read the task's messages for the artifact id, then [export it](/developer/reference/export-a-file) as HTML, XLSX or PDF. ```ts for await (const message of dhanu.tasks.stream(task.id)) { await mail.send({ to: "sales@example.com", text: message.text }); } ``` ```python for message in dhanu.tasks.stream(task.id): mail.send(to="sales@example.com", text=message.text) ``` ## Files as field values A `file` field takes a file id. Upload the file first with [`files.upload`](/developer/reference/upload-a-file) and pass the returned id in `values`; the run attaches it to the task's first message. --- # Receive webhooks Source: https://docs.dhanu.ai/developer/guides/receive-webhooks Register an endpoint, verify every delivery, respond fast and process later, and test it all from your laptop with the CLI. import CodeGroup from "@/components/code-group.astro"; Webhooks tell your backend when something happened without polling: a staff member replied, a job finished, a video is ready, credits are low. This guide takes an endpoint from registration to a handler you can trust. ## 1. Register an endpoint In the developer console (Developers → Webhooks) add an https URL and pick the events. The **secret** (`whsec_…`) is shown once, store it beside your API key as `DHANU_WEBHOOK_SECRET`. The full event list with a payload example each is on [events](/developer/webhooks/events). ## 2. Verify, acknowledge, then work Verify the signature **on the raw body** before parsing anything, answer `2xx` within a few seconds, and do the real work afterwards. A slow handler is retried as if it had failed, and a handler that trusts an unsigned body can be fed anything by whoever guesses your URL. ```ts import express from "express"; import { Dhanu, WebhookSignatureError, type DhanuWebhookEvent } from "dhanu"; const dhanu = new Dhanu(process.env.DHANU_API_KEY!); const app = express(); app.post("/hooks/dhanu", express.raw({ type: "application/json" }), async (req, res) => { let event: DhanuWebhookEvent; try { event = await dhanu.webhooks.verify( req.body.toString("utf8"), req.header("Dhanu-Signature"), process.env.DHANU_WEBHOOK_SECRET!, ); } catch (err) { if (err instanceof WebhookSignatureError) return res.sendStatus(400); throw err; } res.sendStatus(200); await queue.add(event.type, event); }); ``` ```python from flask import Flask, request, abort from dhanu import Dhanu, WebhookSignatureError dhanu = Dhanu(os.environ["DHANU_API_KEY"]) app = Flask(__name__) @app.post("/hooks/dhanu") def hooks(): try: event = dhanu.webhooks.verify( request.get_data(as_text=True), request.headers.get("Dhanu-Signature"), os.environ["DHANU_WEBHOOK_SECRET"], ) except WebhookSignatureError: abort(400) queue.enqueue(process, event) return "", 200 ``` ## 3. Handle each event once Deliveries can arrive twice, a retry after a timeout, a resend from the console. Key your processing on `event.id` and skip one you have seen. ## 4. Test from your laptop Dhanu delivers only to public https URLs, so during development point the endpoint at a tunnel or your staging server and let the [CLI](/developer/sdks/cli) replay what it receives on your machine: ```sh dhanu webhooks listen --endpoint --forward-to localhost:3000/hooks/dhanu --secret $DHANU_WEBHOOK_SECRET dhanu trigger task.completed --endpoint ``` `listen` re-posts every new delivery to your local handler with a `Dhanu-Signature` computed from the secret you pass, so the verification above runs for real; `trigger` fires a test event of any type. ## 5. Rotate without downtime Rotating the secret keeps the old one valid for 24 hours, and every delivery in that window carries two `v1` signatures. Deploy the new secret, then rotate. --- # Go from test to live Source: https://docs.dhanu.ai/developer/guides/test-to-live Build against a test key that spends nothing, then switch to a live key, what changes, what does not, and the checklist before you flip. A key's mode is baked into its prefix, `dhanu_sk_test_…` or `dhanu_sk_live_…`, and chosen when it is created. A test key goes through authentication, scope, validation, rate limits and idempotency exactly like a live one, then answers with **fixtures** instead of running a model or starting a task, and never spends a credit. Everything that works against a test key works against a live one. ## What a test key returns | Call | Test mode | | --- | --- | | `images.generate` | A 1×1 PNG, same bytes every time | | `text.generate` / `text.stream` | A short reply that names itself as a fixture | | `tasks.trigger` / `tasks.create` | A `task_test_…` task that is created nowhere | | `tasks.messages` | A two-message transcript for that task | | `embeddings.create` | Deterministic 8-dimensional vectors | | `transcriptions.create` | A fixture transcript | | `ocr.extract` | An already-succeeded job | | `videos.generate` | A `ready` video with no bytes to download | | `workflows.run` | A fixture task | Reads that need real rows, listing staff, workflows, files, tasks, return the workspace's real data with either key, since reading spends nothing. ## The checklist 1. **Scope the live key as narrowly as the test key.** Same packages or operations; the console shows what each key may call and `me()` confirms it from code. 2. **Set a monthly spend cap and an expiry** on the live key. A cap turns a runaway loop into a `key_spend_limit_reached` error instead of a bill. 3. **Send idempotency keys** on every `POST` that creates something. The SDKs add one automatically when they retry; pass your own (an order id, a ticket id) so a retry from *your* side is safe too. 4. **Handle `insufficient_credits` and `rate_limited`.** A test key never returns them; a live key will, on the day the workspace runs dry or a burst hits the limit. Back off on `Retry-After`. 5. **Verify webhooks with the live endpoint's secret**: each endpoint has its own. 6. **Keep the test key** for CI. Contract tests against fixtures catch a broken integration before it touches credits. ## Flipping Mint the live key, put it in the production environment as `DHANU_API_KEY`, deploy. Nothing in your code changes: the SDK reads the mode from the key, and `me().key.mode` tells you which one a process is holding. ```ts const me = await dhanu.me(); if (me.key.mode !== "live") throw new Error("production is holding a test key"); ``` Leaked keys are revoked for you: GitHub's secret scanning reports a `dhanu_sk_` that lands in a public repository, and the key is disabled and the workspace notified within minutes. Revoke a key yourself in the console or with `dhanu keys revoke `. --- # Export activity to your own collector Source: https://docs.dhanu.ai/developer/guides/telemetry-export Point a workspace at an OpenTelemetry collector and every audited action arrives there as OTLP log records, for your SIEM, your alerting, your retention policy. Everything notable that happens in a workspace, sign-ins, key creation, webhook changes, file shares, OAuth consents, API calls that mutate, is recorded in the workspace's security audit log. **Telemetry export** streams those same records to an OTLP/HTTP collector you run: Splunk, Datadog, Honeycomb, Elastic, Loki, ClickHouse, or the OpenTelemetry Collector in front of any of them. ## Set it up Workspace admins with the *security* permission find it under **Developer console → Monitor → Telemetry export**. 1. **Endpoint**: the collector's logs URL, usually ending in `/v1/logs`. It must be https; `http://localhost` works for a collector on the same machine. 2. **Headers**: one per line as `Name: value`, for example `Authorization: Bearer …` or `DD-API-KEY: …`. They are stored sealed and never shown again; leave the box empty on later saves to keep them. 3. **Switches**: export on/off, and three opt-ins that are **off by default**: prompt text, tool arguments, file paths. Nothing sensitive leaves unless you turn it on. 4. **Send a test record** to confirm the collector accepts the payload, then save. ## What arrives Standard OTLP `resourceLogs` JSON. Resource attributes: `service.name = dhanu`, `service.version`, `deployment.environment`, `dhanu.workspace.id`. Each record's body is the event type (`api_key.create`, `oauth_consent.grant`, …) with the structure in attributes: | Attribute | Meaning | | --- | --- | | `dhanu.event.id` | Stable id, dedupe on it | | `dhanu.event.type` | The event type, same as the body | | `dhanu.request.id` | The request that caused it, when there was one | | `enduser.id`, `dhanu.actor.email` | Who did it | | `http.request.method`, `url.path`, `http.response.status_code`, `client.address` | The request envelope | | `dhanu.workspace.id`, `dhanu.task.id`, `dhanu.project.id`, `dhanu.target.id` | What it touched | | `dhanu.detail.*` | Everything else the event recorded | Severity is `INFO`, or `ERROR` for failed requests, so you can alert on severity without knowing our event names. ## Delivery guarantees Records are sent in order, in batches, within about a minute of happening, and **at least once**: a collector that timed out may receive a batch again, which is why the id is stable. A failing collector is retried every minute; after twenty consecutive failures export switches itself off and the settings page says why, so a dead endpoint never becomes an unbounded queue. Save the page again to resume from where it stopped. --- # Give staff your own HTTP tools Source: https://docs.dhanu.ai/developer/guides/http-tools Describe an endpoint of your API, name, method, URL, input schema, and every AI staff member you grant it to can call it, no MCP server required. A staff member can already use MCP servers. If what you have is a plain HTTP API, you do not need to wrap it: register its endpoints as **HTTP tools** and Dhanu calls them directly, with the arguments the model supplies, inside the same approval and audit gates every other tool goes through. ## 1. Describe the tools **Settings → Plugins → Add → Custom connector**, choose the **HTTP tools** kind, and paste a JSON array: ```json [ { "name": "lookup_order", "description": "Look an order up by its id and return its status and line items.", "method": "GET", "url": "https://api.acme.example/orders", "inputSchema": { "type": "object", "properties": { "id": { "type": "string", "description": "The order id, e.g. ORD-1042" } }, "required": ["id"] } }, { "name": "open_ticket", "description": "Open a support ticket. Use when a customer reports a problem you cannot solve.", "method": "POST", "url": "https://api.acme.example/tickets", "inputSchema": { "type": "object", "properties": { "subject": { "type": "string" }, "body": { "type": "string" } }, "required": ["subject", "body"] } } ] ``` The rules: up to 20 tools; names are 2–40 lowercase letters, digits or underscores; URLs are https; `inputSchema` is a JSON schema of type `object`. The **description is what the model reads** to decide when to call the tool, write it for a colleague, not a compiler. A `GET` tool sends the arguments as query parameters; a `POST` tool sends them as a JSON body. If your API needs a token, choose **Access token** as the authentication and Dhanu sends it as `Authorization: Bearer …` on every call, it is stored sealed and never shown again. ## 2. Grant it to a staff member The connector appears in the plugin list like any other. Open a staff member, enable the plugin, and it can call `lookup_order` and `open_ticket` in its runs; the tool calls show in the task, and every call is audited. ## 3. What the model sees A successful call returns the response body (up to 20 000 characters) with the status; a failure, non-2xx, timeout after 15 seconds, or a URL Dhanu refuses to call, comes back as an error the model can read and recover from, never as a crash. Dhanu refuses loopback, private-range and cloud-metadata addresses both when you register a tool and every time it is called, so a tool can never be pointed at something inside our network. ## Why not MCP? Use MCP when the server already exists or you want to ship it to other products. HTTP tools are for the API you already run: no new process, nothing to host, and the definition lives in the workspace next to the staff that use it. --- # Publish to the marketplace Source: https://docs.dhanu.ai/developer/guides/marketplace Share a skill, a workflow or a staff brief with every Dhanu workspace, submit it from the developer console, pass a short review, and it appears in the marketplace. Everything a workspace builds for itself, a skill that writes briefs the way your industry needs, a workflow with the right form, a staff member with a brief that took weeks to get right, can be published for other workspaces to add with one click. ## Publish **Developers → Marketplace → Publish.** Pick the kind (skill, workflow or staff brief), pick which of yours to publish, write a title and a one-line summary, and submit. Dhanu takes a **copy** at that moment: the listing is what a reviewer sees and what other workspaces install, and editing your original later does not change it. Publish again to ship a new version. A staff listing carries the member's name, role, brief and access level, not its memory, files or connections. A workflow listing carries the prompt and its form fields. A skill listing carries the skill body; bundled files are not included yet. ## Review Listings start **pending**. Review checks that the content is what the summary says, contains nothing that only works in your workspace (a hard-coded folder, a private connector), and nothing that would mislead the person adding it. Rejections say why and you can resubmit. Published listings show a "Community" tag and your workspace's name. ## What installing does Adding a community listing creates an ordinary private item in the installing workspace, a skill in their library, a workflow in their workflows, a staff member on their team, owned by them, editable by them, and disconnected from yours. The listing's install count goes up; nothing about the installer is shared with you. ## Unlisting **Unlist** hides a listing from the marketplace at once. Workspaces that already added it keep their copy. --- # Embed a chat widget Source: https://docs.dhanu.ai/developer/guides/embed-a-widget Put one of your staff members on your own website with a single script tag and a publishable key that only works on the origins you allow. A widget is a chat bubble on your site whose answers come from one of your Dhanu staff members. Visitors do not sign in; each conversation becomes a task in your workspace, assigned to that staff member, so you can read every exchange where the rest of your work lives. ## 1. Create the widget **Developers → Widgets → Add widget.** Pick the staff member who answers, name the widget, and list the **origins** it may run on, `https://www.example.com`, `*.example.com` for every subdomain, `http://localhost:3000` while you build. Set a greeting and an accent colour if you like. Dhanu shows the embed code once: ```html ``` ## 2. Paste it into your site Anywhere before ``. The script draws a bubble in the bottom-right corner inside a shadow root, so your CSS and its CSS never touch. Clicking it opens a panel with the greeting; each message the visitor sends goes to your staff member and the reply streams back into the panel. ## Why the key is safe in a page A publishable key (`dhanu_pk_…`) can do exactly two things: start a widget conversation and continue one it started, and only from a page whose origin is on the widget's allowlist, the browser sends the origin, Dhanu checks it on every call. It cannot read your workspace, call the API, or reach any other staff member, and it is never accepted where a secret key is. Rotate it from the console if it leaks anyway; revoke the widget to stop it everywhere at once. ## Costs and limits Replies spend your workspace's credits like any staff run. Requests are rate-limited per key. Messages are capped at 4 000 characters, and the staff member sees the visitor's text as untrusted input, the same as an email or an API trigger. ## What the visitor sees Only what the staff member replies. The task title is *Website visitor · your domain*; the visitor is anonymous, and nothing about your workspace is exposed to the page beyond the staff member's name, the greeting and the colour. --- # Check the key Source: https://docs.dhanu.ai/developer/reference/check-the-key GET /api/pub/v1/me, confirm a key works and see which projects and operations it can reach. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/me ``` The quickest way to confirm a key works. It is free: no model runs, no credits are spent, and it is not metered against a project. ## Request No body and no parameters, only the bearer key. ```sh curl https://api.dhanu.ai/api/pub/v1/me \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts import { Dhanu } from "dhanu"; const dhanu = new Dhanu(process.env.DHANU_API_KEY!); const me = await dhanu.me(); console.log(me.projects.map((project) => project.name)); ``` ```python import os from dhanu import Dhanu dhanu = Dhanu(os.environ["DHANU_API_KEY"]) me = dhanu.me() print([project.name for project in me.projects]) ``` ## Response `200 OK` ```json { "app": { "id": "…", "name": "Website" }, "projects": [{ "id": "…", "name": "Website" }], "key": { "name": "production" }, "scope": { "level": "package", "packages": ["images"], "operations": ["images.generate"] } } ``` `projects` lists every project the key can reach. `app` is that single project when there is exactly one, and `null` when the key spans several, in which case generation calls need a `projectId`. ## Errors The full list is on [Errors](/developer/reference/errors). --- # Generate an image Source: https://docs.dhanu.ai/developer/reference/generate-an-image POST /api/pub/v1/images, generate an image from a prompt and get the bytes back as base64. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` POST https://api.dhanu.ai/api/pub/v1/images ``` Generates an image and returns the bytes as base64. **Dhanu stores nothing**, no task, no document, no version history. Save the bytes yourself. ## Request ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/images \ -H "Authorization: Bearer $DHANU_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "a red bicycle on a white background", "aspectRatio": "1:1" }' ``` ```ts import { Dhanu } from "dhanu"; const dhanu = new Dhanu(process.env.DHANU_API_KEY!); const image = await dhanu.images.generate({ prompt: "a red bicycle on a white background", aspectRatio: "1:1", }); ``` ```python import os from dhanu import Dhanu dhanu = Dhanu(os.environ["DHANU_API_KEY"]) image = dhanu.images.generate( prompt="a red bicycle on a white background", aspect_ratio="1:1", ) ``` ## Response `201 Created` ```json { "image": { "b64": "…", "mimeType": "image/png" }, "model": "…", "usage": { "inputTokens": 9, "outputTokens": 0 }, "cost": { "amount": 800, "currency": "NPR" } } ``` `cost.amount` is in the currency's minor unit, so `800` is NPR 8.00, roughly what a typical image costs. ## Saving the bytes ```ts import { writeFileSync } from "node:fs"; const response = await fetch("https://api.dhanu.ai/api/pub/v1/images", { method: "POST", headers: { Authorization: `Bearer ${process.env.DHANU_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ prompt: "a red bicycle", aspectRatio: "1:1" }), }); const { image } = await response.json(); writeFileSync("out.png", Buffer.from(image.b64, "base64")); ``` ```python import base64 import json import os import urllib.request request = urllib.request.Request( "https://api.dhanu.ai/api/pub/v1/images", data=json.dumps({"prompt": "a red bicycle", "aspectRatio": "1:1"}).encode(), headers={ "Authorization": f"Bearer {os.environ['DHANU_API_KEY']}", "Content-Type": "application/json", }, ) with urllib.request.urlopen(request) as response: payload = json.load(response) with open("out.png", "wb") as handle: handle.write(base64.b64decode(payload["image"]["b64"])) ``` ## Errors The full list is on [Errors](/developer/reference/errors). --- # Generate text Source: https://docs.dhanu.ai/developer/reference/generate-text POST /api/pub/v1/text, generate text from a prompt, with an optional system instruction and token cap. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` POST https://api.dhanu.ai/api/pub/v1/text ``` Generates text from a prompt. Nothing is stored, no task, no document, no version history. You get the text back and a metering record is written against the acting project. ## Request ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/text \ -H "Authorization: Bearer $DHANU_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "Write a haiku about Kathmandu", "system": "You are a concise poet.", "maxTokens": 200 }' ``` ```ts import { Dhanu } from "dhanu"; const dhanu = new Dhanu(process.env.DHANU_API_KEY!); const reply = await dhanu.text.generate({ prompt: "Write a haiku about Kathmandu", system: "You are a concise poet.", maxTokens: 200, }); console.log(reply.text); ``` ```python import os from dhanu import Dhanu dhanu = Dhanu(os.environ["DHANU_API_KEY"]) reply = dhanu.text.generate( prompt="Write a haiku about Kathmandu", system="You are a concise poet.", max_tokens=200, ) print(reply.text) ``` ## Response `201 Created` ```json { "text": "…", "model": "…", "usage": { "inputTokens": 12, "outputTokens": 41 }, "cost": { "amount": 28, "currency": "NPR" } } ``` `cost.amount` is in the currency's minor unit, so `28` is NPR 0.28. A reply of roughly 500 tokens in and 500 out costs on the order of 28 paisa. ## Streaming Send `"stream": true` (or `Accept: text/event-stream`) to receive the reply as server-sent events: `delta` events carrying `{ "text": "…" }` as tokens arrive, then one `done` event with the same body a non-streaming call returns. The SDKs expose it as an iterator: ```ts for await (const event of dhanu.text.stream({ prompt: "Write a haiku about Kathmandu" })) { if (event.type === "delta") process.stdout.write(event.text); else console.log("\ncost", event.result.cost); } ``` ```python for event in dhanu.text.stream(prompt="Write a haiku about Kathmandu"): print(getattr(event, "text", ""), end="") ``` ## Errors The full list is on [Errors](/developer/reference/errors). --- # Trigger a staff member Source: https://docs.dhanu.ai/developer/reference/trigger-a-staff-member POST /api/pub/v1/staff/{address}/trigger, hand an AI staff member a piece of work from your own server and read the reply back. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` POST https://api.dhanu.ai/api/pub/v1/staff/{address}/trigger ``` Hands a staff member a piece of work through its **API address**, the opaque `stf_…` string on the staff member's **Channels** tab. A task is created in the staff member's workspace and the reply is written to it in the background, so the call returns at once with the task and you [read its messages](/developer/reference/read-task-messages) to see the answer. The run spends workspace credits like any other task; the trigger itself is not billed. Everything you send is treated as untrusted text: the staff member reports on it, it never obeys instructions inside it. ## Request ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/staff/stf_abc123/trigger \ -H "Authorization: Bearer $DHANU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "input": "Draft minutes for today\u0027s stand-up.", "subject": "Stand-up minutes" }' ``` ```ts import { Dhanu } from "dhanu"; const dhanu = new Dhanu(process.env.DHANU_API_KEY!); const task = await dhanu.tasks.trigger({ address: "stf_abc123", input: "Draft minutes for today's stand-up.", subject: "Stand-up minutes", idempotencyKey: crypto.randomUUID(), }); ``` ```python import os import uuid from dhanu import Dhanu dhanu = Dhanu(os.environ["DHANU_API_KEY"]) task = dhanu.tasks.trigger( address="stf_abc123", input="Draft minutes for today's stand-up.", subject="Stand-up minutes", idempotency_key=str(uuid.uuid4()), ) ``` Pass `threadId`, a task id from an earlier trigger, to continue that task instead of opening a new one. ## Response `202 Accepted` ```json { "task": { "id": "…", "title": "Stand-up minutes", "createdAt": "2026-09-17T10:15:00.000Z", "updatedAt": "2026-09-17T10:15:00.000Z" } } ``` ## Errors The full list is on [Errors](/developer/reference/errors). --- # Read a task Source: https://docs.dhanu.ai/developer/reference/read-a-task GET /api/pub/v1/tasks/{taskId}, read a task the key's workspace owns. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/tasks/{taskId} ``` Reads one task. The task must belong to the key's workspace; anything else is `not_found`, so a key can never confirm that a task exists elsewhere. ## Request ```sh curl https://api.dhanu.ai/api/pub/v1/tasks/$TASK_ID \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts const task = await dhanu.tasks.get(taskId); ``` ```python task = dhanu.tasks.get(task_id) ``` ## Response `200 OK` ```json { "task": { "id": "…", "title": "Stand-up minutes", "createdAt": "2026-09-17T10:15:00.000Z", "updatedAt": "2026-09-17T10:16:40.000Z" } } ``` ## Errors The full list is on [Errors](/developer/reference/errors). --- # Read a task's messages Source: https://docs.dhanu.ai/developer/reference/read-task-messages GET /api/pub/v1/tasks/{taskId}/messages, page through a task's messages, oldest first, to read a staff member's reply. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/tasks/{taskId}/messages ``` The messages on a task, **oldest first**, one page at a time. This is how you read the reply after a [trigger](/developer/reference/trigger-a-staff-member): poll until an `assistant` message appears. ## Request ```sh curl "https://api.dhanu.ai/api/pub/v1/tasks/$TASK_ID/messages?limit=50" \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts // One page const page = await dhanu.tasks.messagesPage(taskId, { limit: 50 }); const next = await dhanu.tasks.messagesPage(taskId, { cursor: page.nextCursor }); // Or everything, following pages to the end const all = await dhanu.tasks.messages(taskId); ``` ```python # One page page = dhanu.tasks.messages_page(task_id, limit=50) next_page = dhanu.tasks.messages_page(task_id, cursor=page.next_cursor) # Or everything, following pages to the end rows = dhanu.tasks.messages(task_id) ``` ## Response `200 OK` ```json { "data": [ { "id": "…", "role": "user", "text": "Draft minutes for today's stand-up.", "createdAt": "…" }, { "id": "…", "role": "assistant", "text": "Here are the minutes…", "createdAt": "…" } ], "nextCursor": null, "hasMore": false, "messages": [ … ] } ``` Pass `nextCursor` back as `cursor` to fetch the next page; when `hasMore` is `false` you hold the last one. `messages` mirrors `data` for clients written before pagination and is deprecated, see [Versioning](/developer/reference/versioning). ## Errors The full list is on [Errors](/developer/reference/errors). --- # Read a job Source: https://docs.dhanu.ai/developer/reference/read-a-job GET /api/pub/v1/jobs/{jobId}, follow background work the API started for you until it succeeds or fails. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/jobs/{jobId} ``` Anything that takes longer than a request, a video render, an export, returns `202` at once with a **job**, and finishes in the background. Read the job until `status` is `succeeded` or `failed`, or subscribe to the `job.succeeded` and `job.failed` [webhooks](/developer/webhooks/overview) and skip polling entirely. ## Request ```sh curl https://api.dhanu.ai/api/pub/v1/jobs/$JOB_ID \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts const job = await dhanu.jobs.get(jobId); // or block until it finishes (polls every 2 s, up to 10 min) const done = await dhanu.jobs.wait(jobId); ``` ```python job = dhanu.jobs.get(job_id) # or block until it finishes done = dhanu.jobs.wait(job_id) ``` ## Response `200 OK` ```json { "job": { "id": "…", "type": "video.generate", "status": "succeeded", "result": { "videoId": "…" }, "error": null, "createdAt": "2026-09-17T10:15:00.000Z", "completedAt": "2026-09-17T10:18:12.000Z" } } ``` `status` moves `queued` → `running` → `succeeded` | `failed` (or `cancelled`). `result` is set on success and `error` on failure; both are `null` until then. ## Errors The full list is on [Errors](/developer/reference/errors). --- # List the API catalogue Source: https://docs.dhanu.ai/developer/reference/list-the-api-catalogue GET /api/pub/v1/reference, every package and API Dhanu exposes, with a flag for what the calling key is allowed to use. import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/reference ``` Returns the whole API catalogue, packages, the APIs inside each, and whether the key you called with is allowed to use them. It is how you discover the API straight over HTTP, without installing the Node or Python package first. It is free: no model runs, no credits are spent, and it is not metered against a project. ## Request No body and no parameters, only the bearer key. ```sh curl https://api.dhanu.ai/api/pub/v1/reference \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ## Response `200 OK` ```json { "scope": { "level": "package", "packages": ["images"], "operations": ["images.generate"] }, "packages": [ { "id": "text", "title": "Text", "description": "Language model completions.", "operations": [ { "id": "text.generate", "method": "POST", "path": "/text", "title": "Generate text", "description": "Generate a completion from a prompt.", "billed": true, "allowed": false } ] }, { "id": "images", "title": "Images", "description": "Image generation.", "operations": [ { "id": "images.generate", "method": "POST", "path": "/images", "title": "Generate an image", "description": "Generate an image from a prompt and return it as base64.", "billed": true, "allowed": true } ] } ] } ``` `scope` describes the key you called with. `packages` is the full catalogue regardless of scope, so you can see what exists and what you would need to widen the key to reach. | Field | Meaning | | ----------- | -------------------------------------------------------------------- | | `path` | Append it to `https://api.dhanu.ai/api/pub/v1` to get the full URL | | `billed` | Whether calling it spends workspace credits | | `allowed` | Whether **this** key's scope covers it | Calling an API where `allowed` is `false` fails with `api_key_operation_scope` (403) before anything runs. Widen the key's scope in **Developers → API keys**, or use a broader key. ## Errors Any live key can call this endpoint at any scope level, so the only failures are authentication ones. The full list is on [Errors](/developer/reference/errors). --- # Errors Source: https://docs.dhanu.ai/developer/reference/errors Every Dhanu API error uses one envelope with a machine-readable code. Here is the full list and what to do about each. Every failed request returns the same shape: ```json { "error": { "code": "invalid_api_key", "message": "Missing or unknown API key" } } ``` import ErrorCatalog from "@/components/error-catalog.astro"; Branch on `code`, not on `message`, messages are written for humans and may be reworded. The tables below are rendered from the same catalogue the server checks its responses against, so a code that can reach you is listed here. ## Request IDs Every response carries a request id, and it is included in the error envelope. You can supply your own with the `x-request-id` header, it must match `[A-Za-z0-9._~-]{8,128}`, otherwise Dhanu generates one, and it is echoed back on the response. **Include the request id when you contact support.** It is the fastest way for us to find what happened. You can also look it up yourself: **Developers → Requests** in the console keeps every call made with your keys for 30 days, with its status, latency, cost and redacted request and response bodies. ## Handling errors with the SDK The SDK throws `DhanuError`, carrying the same code plus the request id: ```ts import { DhanuError } from "dhanu"; try { await dhanu.images.generate({ prompt: "…" }); } catch (err) { if (err instanceof DhanuError) { console.error(err.code, err.status, err.message, err.requestId); } } ``` --- # Rate limits Source: https://docs.dhanu.ai/developer/reference/rate-limits Rate limits are per workspace, reported on every response, and signalled with 429 plus Retry-After when exceeded. Public API requests are rate limited **per workspace**, not per key. Minting more keys does not raise your ceiling, every key in a workspace draws on the same budget. ## A ceiling per key On top of the workspace limit, a key can be given its own **requests per minute** when it is created or edited, a lower ceiling for a key you hand to a partner or a script you do not fully trust. A key's limit can only tighten the workspace limit, never raise it, and the headers below always describe whichever limit is closer to being hit. The console's key list shows each key's effective limit and the workspace ceiling. ## Reading your budget Every response, successful or not, carries three headers: | Header | Meaning | | --------------------- | --------------------------------------------- | | `RateLimit-Limit` | Requests allowed in the current window | | `RateLimit-Remaining` | Requests left in this window | | `RateLimit-Reset` | Seconds until the window resets | Read `RateLimit-Remaining` as you go rather than waiting to be refused, it lets you slow down before you hit the wall. ## When you exceed it You get `429` with the standard envelope and `code: "rate_limited"`, plus a `Retry-After` header in seconds. ``` HTTP/1.1 429 Too Many Requests Retry-After: 34 RateLimit-Reset: 34 ``` ```json { "error": { "code": "rate_limited", "message": "Too many requests. Please try again later." } } ``` ## Backing off well Wait for `Retry-After` before retrying, retrying sooner just consumes the next window. For batch work, spread requests out rather than firing them in parallel and absorbing the 429s. Rate limiting is separate from **credits**. A `429` means "too fast"; a `402` (`insufficient_credits`) means "out of balance". Slowing down fixes the first and does nothing for the second. --- # OpenAPI document Source: https://docs.dhanu.ai/developer/reference/openapi GET /api/pub/v1/openapi.json, the machine-readable description of the whole public API, for generating clients, importing into Postman, or feeding an agent. ``` GET https://api.dhanu.ai/api/pub/v1/openapi.json ``` Returns an **OpenAPI 3.1** document describing every public endpoint: its method, path, request body schema, response schema, the scope it needs (`x-dhanu-scope`) and the error codes it can return (`x-dhanu-error-codes`). It needs no key and spends no credits. ```sh curl https://api.dhanu.ai/api/pub/v1/openapi.json ``` The docs site serves the same document at [docs.dhanu.ai/developer/openapi.json](/developer/openapi.json), and a **Postman collection** built from it, one folder per tag, bearer auth over an `apiKey` variable, a required-fields body example per request, at [docs.dhanu.ai/developer/postman.json](/developer/postman.json). Bruno and Insomnia import that collection too. For an AI assistant, [`/llms.txt`](/llms.txt) indexes every page and [`/llms-full.txt`](/llms-full.txt) is the whole Developer section in one file; any page is Markdown with `.md` appended to its URL, and every reference page carries the same call as curl, Node and Python with a copy button. ## What it is for - **Generating a client** in a language we do not ship an SDK for (`openapi-generator`, `openapi-typescript`, `kiota`, …). - **Importing into Postman, Bruno or Insomnia** to try calls by hand. - **Giving an agent the API**: point a coding assistant at the URL and it can write a correct call. ## How it stays true The document is built from the same schemas the server validates requests with. A request body that the spec says is valid is one the server accepts, and a route that exists but is missing from the spec fails the server's own test suite. It is never edited by hand. Authentication is declared as the `apiKey` security scheme, an HTTP bearer token, and every operation except this one requires it. --- # Versioning and deprecation Source: https://docs.dhanu.ai/developer/reference/versioning How the Dhanu API changes, what ships inside v1 without notice, what needs a v2, and how a deprecation announces itself in the response. The version is in the path: `https://api.dhanu.ai/api/pub/v1`. There is no version header to send. ## What can change without a new version These are **additive** and can land in `v1` at any time. Write your client so they never break it, ignore fields you do not know, and never enumerate an object's keys to find the one you want: - a new endpoint - a new **optional** request field - a new response field - a new value on an enum documented as open (for example an error `code`) - a new error code ## What needs a new version These are **breaking** and only ever ship as `/api/pub/v2`: - removing or renaming a field - changing a field's type or format - tightening validation so a request that used to succeed now fails - changing a status code - removing an endpoint When `v2` ships, `v1` keeps working unchanged for **at least twelve months** from the announcement. ## How a deprecation announces itself A deprecated endpoint says so on every response: ``` Deprecation: @1789603200 Sunset: Fri, 17 Sep 2027 00:00:00 GMT Link: ; rel="deprecation" ``` - `Deprecation` is the moment it was deprecated, as a Unix timestamp ([RFC 9745](https://www.rfc-editor.org/rfc/rfc9745)). - `Sunset` is when it stops working ([RFC 8594](https://www.rfc-editor.org/rfc/rfc8594)), never less than a year after `Deprecation`. - `Link` points at the replacement, or at this page. A deprecated **field** cannot carry headers, so it is listed on the [OpenAPI document](/developer/reference/openapi) under `x-dhanu-deprecated-fields` on its operation, with the same sunset date and its replacement. Today that list holds one entry: `messages` on `GET /tasks/{taskId}/messages` mirrors `data` until 17 September 2027. Every deprecation and every `v2` change is also written up in the [changelog](/changelog) under the **API** area. ## Test keys follow the same contract A response from a test-mode key has the same shape as a live one, so a client that works in test mode works in production. --- # Create a task Source: https://docs.dhanu.ai/developer/reference/create-a-task POST /api/pub/v1/tasks, start a task from a first message, optionally assigned to a staff member. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` POST https://api.dhanu.ai/api/pub/v1/tasks ``` Starts a task from a first message. With `staffId` the reply comes from that staff member's run; without it the workspace assistant replies. Returns at once with the task, then [read its messages](/developer/reference/read-task-messages), [stream them](/developer/reference/stream-a-task), or subscribe to the `task.message.created` [webhook](/developer/webhooks/overview). The reply spends workspace credits like any other task. Everything you send is treated as untrusted text: the assistant reports on it and never obeys instructions inside it. ## Request ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/tasks \ -H "Authorization: Bearer $DHANU_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": "Summarise this week\u0027s support tickets.", "staffId": "…" }' ``` ```ts const task = await dhanu.tasks.create({ input: "Summarise this week's support tickets.", staffId: "…", }); ``` ```python task = dhanu.tasks.create(input="Summarise this week's support tickets.", staff_id="…") ``` ## Response `202 Accepted` ```json { "task": { "id": "…", "title": "Summarise this week's support tickets.", "assigneeStaffIds": ["…"], "createdAt": "…", "updatedAt": "…" } } ``` ## Errors --- # List tasks Source: https://docs.dhanu.ai/developer/reference/list-tasks GET /api/pub/v1/tasks, page through the tasks the key's creator can see, newest first. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/tasks ``` The tasks the key's creator can see in the workspace, **newest first**, one page at a time. ## Request ```sh curl "https://api.dhanu.ai/api/pub/v1/tasks?limit=20" \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts const page = await dhanu.tasks.list({ limit: 20 }); ``` ```python page = dhanu.tasks.list(limit=20) ``` ## Response `200 OK`: `{ data, nextCursor, hasMore }` as on every list endpoint. ## Errors --- # Send a message Source: https://docs.dhanu.ai/developer/reference/send-a-message POST /api/pub/v1/tasks/{taskId}/messages, add a message to a task and start the reply. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` POST https://api.dhanu.ai/api/pub/v1/tasks/{taskId}/messages ``` Adds a message to an existing task and starts the reply. Returns the message you sent; the reply follows in the background. ## Request ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/tasks/$TASK_ID/messages \ -H "Authorization: Bearer $DHANU_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": "Now group them by product." }' ``` ```ts const message = await dhanu.tasks.send(taskId, "Now group them by product."); ``` ```python message = dhanu.tasks.send(task_id, "Now group them by product.") ``` ## Response `202 Accepted` ```json { "message": { "id": "…", "role": "user", "text": "Now group them by product.", "createdAt": "…" } } ``` ## Errors --- # Stream a task Source: https://docs.dhanu.ai/developer/reference/stream-a-task GET /api/pub/v1/tasks/{taskId}/stream, receive completed replies as server-sent events instead of polling. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/tasks/{taskId}/stream ``` A `text/event-stream` that emits one `message` event per **completed** assistant reply arriving after `cursor` (or after now when omitted), then `done` when the connection closes. The connection is held for up to 60 seconds; reconnect with the last event's `id` as the next `cursor`. The SDKs do the reconnecting for you. ## Request ```sh curl -N "https://api.dhanu.ai/api/pub/v1/tasks/$TASK_ID/stream" \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts for await (const reply of dhanu.tasks.stream(taskId)) { console.log(reply.text); } ``` ```python for reply in dhanu.tasks.stream(task_id): print(reply.text) ``` ## Events ``` event: message id: data: {"id":"…","role":"assistant","text":"…","createdAt":"…"} event: done data: {} ``` ## Errors --- # List staff Source: https://docs.dhanu.ai/developer/reference/list-staff GET /api/pub/v1/staff, every active staff member, with the trigger address of each one that has the API channel on. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/staff ``` Every active staff member in the workspace. `apiAddress` is the value to use in [`POST /staff/{address}/trigger`](/developer/reference/trigger-a-staff-member), or `null` when that staff member's API channel is switched off; `id` is what [`POST /tasks`](/developer/reference/create-a-task) takes as `staffId`. ```sh curl https://api.dhanu.ai/api/pub/v1/staff \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts const staff = await dhanu.staff.list(); ``` ```python staff = dhanu.staff.list() ``` ## Response `200 OK` ```json { "staff": [ { "id": "…", "name": "Anisha", "role": "Analyst", "status": "active", "apiAddress": "stf_…" } ] } ``` ## Errors --- # Read a staff member Source: https://docs.dhanu.ai/developer/reference/read-a-staff-member GET /api/pub/v1/staff/{staffId}, one staff member by id. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/staff/{staffId} ``` ## Request ```sh curl https://api.dhanu.ai/api/pub/v1/staff/$STAFF_ID \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts const member = await dhanu.staff.get(staffId); ``` ```python member = dhanu.staff.get(staff_id) ``` ## Errors --- # List workflows Source: https://docs.dhanu.ai/developer/reference/list-workflows GET /api/pub/v1/workflows, saved workflows with the typed fields their forms ask for. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/workflows ``` The saved workflows the key's creator can see. Each carries `fields`, the typed form (`text`, `textarea`, `select`, `checkboxes`, `toggle`, `number`, `file`) that [`run`](/developer/reference/run-a-workflow) validates values against. ```sh curl https://api.dhanu.ai/api/pub/v1/workflows \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts const workflows = await dhanu.workflows.list(); ``` ```python workflows = dhanu.workflows.list() ``` ## Errors --- # Run a workflow Source: https://docs.dhanu.ai/developer/reference/run-a-workflow POST /api/pub/v1/workflows/{workflowId}/run, fill a workflow's fields and start the task it composes. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` POST https://api.dhanu.ai/api/pub/v1/workflows/{workflowId}/run ``` Fills the workflow's fields and starts the task its prompt composes, assigned to the workflow's suggested staff member when it has one. Values are validated server-side against the stored field definitions; a missing required field or a value of the wrong type is `invalid_workflow_values` with an `issues` list. ## Request ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/workflows/$WORKFLOW_ID/run \ -H "Authorization: Bearer $DHANU_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "values": { "topic": "Q3 results", "audience": "board" } }' ``` ```ts const task = await dhanu.workflows.run(workflowId, { topic: "Q3 results", audience: "board" }); ``` ```python task = dhanu.workflows.run(workflow_id, {"topic": "Q3 results", "audience": "board"}) ``` ## Response `202 Accepted`: the task, as on [create a task](/developer/reference/create-a-task). ## Errors --- # List projects Source: https://docs.dhanu.ai/developer/reference/list-projects GET /api/pub/v1/projects, the projects this key can act in. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/projects ``` The projects this key is scoped to, the same list [`GET /me`](/developer/reference/check-the-key) returns, on its own for clients that only need it. Pass a project's `id` as `projectId` on generation calls when the key spans more than one. ```sh curl https://api.dhanu.ai/api/pub/v1/projects \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts const projects = await dhanu.projects.list(); ``` ```python projects = dhanu.projects.list() ``` ## Errors --- # Upload a file Source: https://docs.dhanu.ai/developer/reference/upload-a-file POST /api/pub/v1/files, upload a file into the key creator's drive in one multipart request. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` POST https://api.dhanu.ai/api/pub/v1/files ``` Uploads one file in a single multipart request (field `file`) into the personal drive of the person who created the key. The returned id works everywhere a file id does, attach it to a task with `fileIds` on [create a task](/developer/reference/create-a-task) or [send a message](/developer/reference/send-a-message), or download it later. For anything larger than the single-request limit, use the [presigned upload](/developer/reference/start-an-upload) instead. Uploads count against the workspace storage quota; a full drive answers `storage_quota_exceeded`. ## Request ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/files \ -H "Authorization: Bearer $DHANU_API_KEY" \ -F "file=@notes.txt" ``` ```ts const file = await dhanu.files.upload({ data: await fs.readFile("notes.txt"), filename: "notes.txt", mimeType: "text/plain", }); ``` ```python with open("notes.txt", "rb") as fh: file = dhanu.files.upload(data=fh.read(), filename="notes.txt", mime_type="text/plain") ``` ## Response `201 Created` ```json { "file": { "id": "…", "filename": "notes.txt", "mimeType": "text/plain", "sizeBytes": 5, "kind": "file", "createdAt": "…" } } ``` ## Errors --- # Start an upload Source: https://docs.dhanu.ai/developer/reference/start-an-upload POST /api/pub/v1/files/uploads, start a presigned multipart upload for a large file. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` POST https://api.dhanu.ai/api/pub/v1/files/uploads ``` Starts a presigned multipart upload. The response names the part size and one `PUT` URL per part; upload each part directly to its URL, keep the `ETag` header each `PUT` returns, then [complete the upload](/developer/reference/complete-an-upload). The session expires after `expiresInSeconds`; an expired session must be started again. ## Request ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/files/uploads \ -H "Authorization: Bearer $DHANU_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "filename": "recording.mp4", "mimeType": "video/mp4", "sizeBytes": 734003200 }' ``` ```ts const session = await dhanu.files.createUpload({ filename: "recording.mp4", mimeType: "video/mp4", sizeBytes: 734_003_200, }); for (const part of session.parts) { // PUT the matching byte range to part.url and keep the ETag header. } ``` ```python session = dhanu.files.create_upload(filename="recording.mp4", mime_type="video/mp4", size_bytes=734_003_200) for part in session.parts: ... # PUT the matching byte range to part.url and keep the ETag header ``` ## Response `201 Created` ```json { "upload": { "sessionId": "…", "partSize": 8388608, "partCount": 88, "parts": [{ "partNumber": 1, "url": "https://…" }], "expiresInSeconds": 3600 } } ``` ## Errors --- # Complete an upload Source: https://docs.dhanu.ai/developer/reference/complete-an-upload POST /api/pub/v1/files/uploads/{uploadId}/complete, finish a presigned upload and get the file. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` POST https://api.dhanu.ai/api/pub/v1/files/uploads/{uploadId}/complete ``` Finishes a presigned upload once every part has been `PUT`. Send each part's number with the `ETag` the storage returned for it; the parts are stitched together and the file appears in the drive. ## Request ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/files/uploads/$SESSION_ID/complete \ -H "Authorization: Bearer $DHANU_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "parts": [{ "partNumber": 1, "etag": "\"9b2cf…\"" }] }' ``` ```ts const file = await dhanu.files.completeUpload(session.sessionId, [ { partNumber: 1, etag: '"9b2cf…"' }, ]); ``` ```python file = dhanu.files.complete_upload(session.session_id, [{"partNumber": 1, "etag": '"9b2cf…"'}]) ``` ## Response `201 Created` ```json { "file": { "id": "…", "filename": "recording.mp4", "mimeType": "video/mp4", "sizeBytes": 734003200, "kind": "video", "createdAt": "…" } } ``` ## Errors --- # Read a file Source: https://docs.dhanu.ai/developer/reference/read-a-file GET /api/pub/v1/files/{fileId}, a file's metadata. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/files/{fileId} ``` Reads a file's metadata. `kind` tells you what it is: an uploaded `file`, `image`, `audio` or `video`, or an artifact the assistant made, a `canvas`, `page`, `sheet`, `deck` or `app`. Uploaded kinds can be [downloaded](/developer/reference/download-a-file); artifacts are [exported](/developer/reference/export-a-file) instead. ## Request ```sh curl https://api.dhanu.ai/api/pub/v1/files/$FILE_ID \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts const file = await dhanu.files.get(fileId); ``` ```python file = dhanu.files.get(file_id) ``` ## Response `200 OK` ```json { "file": { "id": "…", "filename": "Q3 plan.sheet", "mimeType": "application/vnd.dhanu.sheet", "sizeBytes": 0, "kind": "sheet", "createdAt": "…" } } ``` ## Errors --- # Download a file Source: https://docs.dhanu.ai/developer/reference/download-a-file GET /api/pub/v1/files/{fileId}/download, the stored bytes of an uploaded file. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/files/{fileId}/download ``` Streams the stored bytes of an uploaded file with its original `Content-Type` and a `Content-Disposition` carrying the filename. Only uploaded kinds have bytes; an artifact answers `unsupported_media_type`, [export it](/developer/reference/export-a-file). ## Request ```sh curl -OJ https://api.dhanu.ai/api/pub/v1/files/$FILE_ID/download \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts const { bytes, mimeType, filename } = await dhanu.files.download(fileId); await fs.writeFile(filename ?? "download.bin", bytes); ``` ```python downloaded = dhanu.files.download(file_id) with open(downloaded.filename or "download.bin", "wb") as fh: fh.write(downloaded.bytes) ``` ## Response `200 OK` The file body, with `Content-Type`, `Content-Length` and `Content-Disposition: attachment; filename="…"`. ## Errors --- # Export a file Source: https://docs.dhanu.ai/developer/reference/export-a-file GET /api/pub/v1/files/{fileId}/export?format=, a page as HTML, a sheet as CSV or XLSX, a deck as PDF or PPTX. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/files/{fileId}/export ``` Exports an artifact in a portable format. Which formats fit depends on the kind: | Kind | Formats | Notes | | ------- | ---------------- | --------------------------------------------------------------------- | | `page` | `html` | The single self-contained HTML document, as it is served in the app. | | `sheet` | `csv`, `xlsx` | `csv` exports the first tab; `xlsx` keeps every tab and its formulas. | | `deck` | `pptx`, `pdf` | `pdf` needs a render first, see below. | A deck's PDF is rendered by a background job, so the first `pdf` export of a deck answers `export_not_rendered`: call [render a file](/developer/reference/render-a-file), wait for the job, then export. Every other pairing is produced on the spot. A format the kind does not support answers `unsupported_export_format`. ## Request ```sh curl -OJ "https://api.dhanu.ai/api/pub/v1/files/$FILE_ID/export?format=xlsx" \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts const workbook = await dhanu.files.export(fileId, "xlsx"); await fs.writeFile(workbook.filename ?? "export.xlsx", workbook.bytes); ``` ```python workbook = dhanu.files.export(file_id, "xlsx") with open(workbook.filename or "export.xlsx", "wb") as fh: fh.write(workbook.bytes) ``` ## Response `200 OK` The exported document, with the format's `Content-Type` and a `Content-Disposition` filename. ## Errors --- # Render a file Source: https://docs.dhanu.ai/developer/reference/render-a-file POST /api/pub/v1/files/{fileId}/render, render a deck to PDF in the background. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` POST https://api.dhanu.ai/api/pub/v1/files/{fileId}/render ``` Queues a headless render of a deck to PDF and returns the [job](/developer/reference/read-a-job). Wait on the job, or subscribe to `job.succeeded`, then [export the deck as `pdf`](/developer/reference/export-a-file). Rendering a deck that already has a current PDF is a no-op that still returns a job. Other kinds do not need a render step and answer `unsupported_export_format`. ## Request ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/files/$FILE_ID/render \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts const job = await dhanu.files.render(deckId); await dhanu.jobs.wait(job.id); const pdf = await dhanu.files.export(deckId, "pdf"); ``` ```python job = dhanu.files.render(deck_id) dhanu.jobs.wait(job.id) pdf = dhanu.files.export(deck_id, "pdf") ``` ## Response `202 Accepted` ```json { "job": { "id": "…", "type": "deck.export", "status": "queued", "result": null, "error": null, "createdAt": "…", "completedAt": null } } ``` ## Errors --- # Search the brain Source: https://docs.dhanu.ai/developer/reference/search-the-brain POST /api/pub/v1/brain/search, semantic search over the workspace brain. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` POST https://api.dhanu.ai/api/pub/v1/brain/search ``` Runs a semantic search over the workspace **brain**, the knowledge pages Dhanu synthesises from what the workspace works on, and returns the best-matching pages, ranked, each with a short snippet. Results are limited to what the key's creator is allowed to see. The query itself is not billed. ## Request ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/brain/search \ -H "Authorization: Bearer $DHANU_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "refund policy for annual plans" }' ``` ```ts const results = await dhanu.brain.search("refund policy for annual plans"); for (const hit of results) console.log(hit.title, hit.score); ``` ```python for hit in dhanu.brain.search("refund policy for annual plans"): print(hit.title, hit.score) ``` ## Response `200 OK` ```json { "results": [{ "pageId": "…", "slug": "refunds", "title": "Refunds", "score": 0.83, "snippet": "Annual plans are refundable within 14 days…" }] } ``` ## Errors --- # Generate a video Source: https://docs.dhanu.ai/developer/reference/generate-a-video POST /api/pub/v1/videos, start rendering a short clip on a task. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` POST https://api.dhanu.ai/api/pub/v1/videos ``` Starts rendering a short video clip from a prompt on an existing task, so it appears on that task's media board like a clip the assistant made. Rendering takes minutes, so the call returns at once with the video in `generating` and the job to wait on. The bytes exist once the video is `ready`: poll [read a video](/developer/reference/read-a-video), [wait on the job](/developer/reference/read-a-job), or subscribe to the `video.ready` [webhook](/developer/webhooks/overview). Test keys answer with a ready fixture and render nothing. ## Request ```sh curl -X POST https://api.dhanu.ai/api/pub/v1/videos \ -H "Authorization: Bearer $DHANU_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "taskId": "…", "prompt": "A slow dolly over Kathmandu at dawn", "aspectRatio": "16:9" }' ``` ```ts const { video, jobId } = await dhanu.videos.generate({ taskId, prompt: "A slow dolly over Kathmandu at dawn", aspectRatio: "16:9", }); await dhanu.jobs.wait(jobId); const clip = await dhanu.videos.download(video.id); ``` ```python started = dhanu.videos.generate(task_id=task_id, prompt="A slow dolly over Kathmandu at dawn", aspect_ratio="16:9") dhanu.jobs.wait(started.job_id) clip = dhanu.videos.download(started.video.id) ``` ## Response `202 Accepted` ```json { "video": { "id": "…", "taskId": "…", "title": "A slow dolly over Kathmandu at dawn", "status": "generating", "error": null, "aspectRatio": "16:9", "resolution": "720p", "durationSeconds": null, "createdAt": "…", "updatedAt": "…" }, "jobId": "…" } ``` ## Errors --- # Read a video Source: https://docs.dhanu.ai/developer/reference/read-a-video GET /api/pub/v1/videos/{videoId}, a video's status and metadata. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/videos/{videoId} ``` Reads a video's status and metadata. `status` moves `generating` → `ready` or `failed`; a failed video carries the reason in `error`. Once ready, fetch the bytes with [download a video](/developer/reference/download-a-video). ## Request ```sh curl https://api.dhanu.ai/api/pub/v1/videos/$VIDEO_ID \ -H "Authorization: Bearer $DHANU_API_KEY" ``` ```ts const video = await dhanu.videos.get(videoId); ``` ```python video = dhanu.videos.get(video_id) ``` ## Response `200 OK` ```json { "video": { "id": "…", "taskId": "…", "title": "…", "status": "ready", "error": null, "aspectRatio": "16:9", "resolution": "720p", "durationSeconds": 8, "createdAt": "…", "updatedAt": "…" } } ``` ## Errors --- # Download a video Source: https://docs.dhanu.ai/developer/reference/download-a-video GET /api/pub/v1/videos/{videoId}/file, the rendered clip, with range requests. import CodeGroup from "@/components/code-group.astro"; import EndpointReference from "@/components/endpoint-reference.astro"; ``` GET https://api.dhanu.ai/api/pub/v1/videos/{videoId}/file ``` Streams the rendered clip as `video/mp4`. The route honours `Range` headers, so a `