# 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 `