API & SDK
Run AI agents from your code
The Lark API starts agents, follows them live, talks to them while they work, and gets their results, from any language. Each agent works on its own cloud computer, with a terminal, a browser and your tools, for minutes or days. The official SDKs for TypeScript and Python wrap it, with no dependencies.
The base URL is your Lark address: https://dashboard.larkbot.app. All requests and responses are JSON, and timestamps are ISO 8601 in UTC.
Quick start
- Create an API key. In the dashboard, go to Settings → API keys (admins only), name it, and copy it. It's shown once. Store it as
LARK_API_KEY. - Have an agent. Create one in the dashboard (or ask the assistant), or with the API. You refer to it by name.
- Start a run and wait for the result:
curl -X POST https://dashboard.larkbot.app/api/agents/researcher/runs \
-H "Authorization: Bearer $LARK_API_KEY" \
-H "content-type: application/json" \
-d '{"prompt": "Research Acme Inc. and write a one-page brief with sources."}'
# → {"id": "run_8kQ2…", "status": "queued", …}
curl https://dashboard.larkbot.app/api/runs/run_8kQ2… -H "Authorization: Bearer $LARK_API_KEY"
import { Lark } from "lark-agents";
const lark = new Lark({ apiKey: process.env.LARK_API_KEY, baseUrl: "https://dashboard.larkbot.app" });
const run = await lark.runs.create({
agent: "researcher",
prompt: "Research Acme Inc. and write a one-page brief with sources.",
});
const done = await lark.runs.wait(run.id);
console.log(done.status, done.result);
from lark_agents import Lark
lark = Lark(base_url="https://dashboard.larkbot.app") # reads LARK_API_KEY
run = lark.runs.create(agent="researcher",
prompt="Research Acme Inc. and write a one-page brief with sources.")
done = lark.runs.wait(run["id"])
print(done["status"], done["result"])
That's it: the agent got its own machine, did the research in a browser, and returned a report. The rest of this page covers following it live, talking to it, automating it, and what it costs.
Authentication
Send your key on every request as a bearer token: Authorization: Bearer lark_…. A key belongs to one organization and acts with the role you gave it:
| Role | Can |
|---|---|
member | Run agents, read runs and events, manage agents, triggers, skills and secrets. |
admin | Everything a member can, plus organization settings, notifications, members, API keys and the webhook signing secret. |
- Keys never expire. Revoke one in Settings → API keys and it stops working immediately. Rotate by creating the new key first, deploying it, then revoking the old one.
- Keep keys on a server, never in a browser or mobile app, and never in source control.
- Keys can't approve runs the assistant proposed, drive an agent's browser, or change billing. Those need a signed-in person.
Costs & limits
Runs started with an API key are billed to the key's organization exactly like any other run. A run costs its AI model tokens plus its machine time. It's costed as it works (every 30 seconds) and shows on the run and in Usage & billing, broken down by agent and by API key.
When a run can't start, the API answers 402 with a code that says why (see Errors). A run that hits a limit while it's working ends as failed, with the reason in error.
SDKs
Both SDKs have the same shape, no dependencies, and typed errors. Set LARK_API_KEY and LARK_URL, or pass them in.
# Node 18+, Deno, Bun or workers
npm install lark-agents
# Python 3.9+
pip install lark-agents
| Area | Methods |
|---|---|
| Runs | create get list events stream wait message cancel |
| Agents | list get create update delete |
| Triggers | list create update delete fire |
| Skills | list get create update delete draft |
| Secrets | list set delete |
| Other | usage(), request() for any endpoint, and verifyWebhook / verify_webhook |
Runs
A run is one task for one agent. It moves through queued → running → succeeded, failed or cancelled. A run lasts up to the agent's limit: 24 hours by default, up to 7 days.
/api/agents/{agent}/runsStart a run/api/runs/{id}Status, result, error, cost/api/runs?agentId=&status=&limit=&before=List, newest first/api/runs/{id}/events?after=The activity log/api/runs/{id}/stream?after=The same, live (Server-Sent Events)/api/runs/{id}/messagesMessage it while it works/api/runs/{id}/cancelStop itThe body for starting a run:
| Field | Type | |
|---|---|---|
prompt | string, required | The task, in plain words. Be specific about what "done" looks like. |
repo | string | A git URL to clone before it starts. Private repos need a GITHUB_TOKEN secret on the agent. |
ref | string | Branch, tag or commit to check out. |
webhookUrl | URL | Where to POST the run when it finishes (see Run webhooks). |
const run = await lark.runs.create({
agent: "coder",
prompt: "Fix the failing tests and open a pull request.",
repo: "https://github.com/acme/web.git",
webhookUrl: "https://acme.dev/hooks/lark",
});
// Later, or somewhere else:
const latest = await lark.runs.get(run.id);
if (latest.status === "failed") console.error(latest.error);
console.log(latest.cost); // { billableUsd, modelUsd, vmSeconds, … }
run = lark.runs.create(
agent="coder",
prompt="Fix the failing tests and open a pull request.",
repo="https://github.com/acme/web.git",
webhook_url="https://acme.dev/hooks/lark",
)
latest = lark.runs.get(run["id"])
if latest["status"] == "failed":
print(latest["error"])
print(latest["cost"])
curl -X POST https://dashboard.larkbot.app/api/agents/coder/runs \
-H "Authorization: Bearer $LARK_API_KEY" -H "content-type: application/json" \
-d '{"prompt":"Fix the failing tests and open a pull request.","repo":"https://github.com/acme/web.git"}'
Streaming a run
Follow an agent as it works: every command, page and message arrives as an event. The stream is Server-Sent Events. Each event's text is a readable line, and an end event closes it when the run finishes. Reconnect with after=<last seq> (or Last-Event-ID) and you won't miss or repeat anything. The SDKs do this for you.
for await (const event of lark.runs.stream(run.id)) {
if (event.text) console.log(event.text); // "$ npm test", "⌁ browser goto …"
if (event.type === "question") console.log("It's asking:", event.data.question);
}
for event in lark.runs.stream(run["id"]):
if event.get("text"):
print(event["text"])
if event["type"] == "question":
print("It's asking:", event["data"]["question"])
curl -N https://dashboard.larkbot.app/api/runs/run_8kQ2…/stream \
-H "Authorization: Bearer $LARK_API_KEY" -H "Accept: text/event-stream"
| Event | When |
|---|---|
status | The run's status changed (data.status, and data.error on failure). |
assistant | The agent said something (data.text). |
tool.call / tool.result / tool.output | A command, a file edit, a browser action, and what came back. |
screen | Its browser has a new screenshot (data.url, data.title). |
question / answer | It asked you something and is waiting; it got the answer. |
user.message | Someone sent it a message. |
cost | The final cost, when it ends. |
result | The final report was recorded. |
Talking to a running agent
Add to the task while it works, or answer a question it asked. The agent reads your message before its next step. When an agent is stuck (it needs a login, or a decision only you can make), it emits a question event, notifies the people who follow it, and waits for a message.
await lark.runs.message(run.id, "Also include their pricing page.");
for await (const e of lark.runs.stream(run.id)) {
if (e.type === "question") await lark.runs.message(run.id, "Use the staging account.");
}
lark.runs.message(run["id"], "Also include their pricing page.")
for e in lark.runs.stream(run["id"]):
if e["type"] == "question":
lark.runs.message(run["id"], "Use the staging account.")
curl -X POST https://dashboard.larkbot.app/api/runs/run_8kQ2…/messages \
-H "Authorization: Bearer $LARK_API_KEY" -H "content-type: application/json" \
-d '{"text":"Also include their pricing page."}'
Agents
An agent is a model, a harness (Lark's built-in agent, Claude Code, Codex or Aider), a machine, and its instructions, secrets, skills and tools. Refer to agents by name or id.
/api/agentsList/api/agentsCreate/api/agents/{agent}Get/api/agents/{agent}Change some fields/api/agents/{agent}Deleteawait lark.agents.create({
name: "researcher",
description: "Researches companies on the web",
model: { provider: "lark", model: "accounts/fireworks/models/glm-5p3" },
browser: true, // a real browser in its machine
sandbox: { type: "fly", persistent: true }, // keeps files, logins and notes between tasks
systemPrompt: "Cite every source. Prefer primary sources.",
secrets: ["CRM_TOKEN"],
skills: ["company-research-brief"],
});
lark.agents.create(
name="researcher",
description="Researches companies on the web",
model={"provider": "lark", "model": "accounts/fireworks/models/glm-5p3"},
browser=True,
sandbox={"type": "fly", "persistent": True},
systemPrompt="Cite every source. Prefer primary sources.",
secrets=["CRM_TOKEN"],
skills=["company-research-brief"],
)
List the models and harnesses this server offers with GET /api/providers and GET /api/harnesses.
Triggers & inbound webhooks
A trigger starts an agent on its own: on a schedule (cron, in your time zone) or when another service calls its webhook URL. What a webhook receives becomes part of the task: {{payload}} is the whole body, and {{payload.field}} is one value from it. Triggered runs are billed like any run, and stop at the trigger's daily cap and your budget.
const hook = await lark.triggers.create("researcher", {
name: "new-lead",
kind: "webhook",
prompt: "New lead: {{payload.name}} at {{payload.company}}. Research the company and write a brief.",
});
console.log(hook.url, hook.secret); // give these to the sending service; the secret is shown once
const nightly = await lark.triggers.create("coder", {
name: "nightly-tests", kind: "schedule", schedule: "0 2 * * *", timezone: "America/New_York",
prompt: "Run the test suite and fix anything that's red.",
});
hook = lark.triggers.create("researcher", name="new-lead", kind="webhook",
prompt="New lead: {{payload.name}} at {{payload.company}}. Research the company and write a brief.")
print(hook["url"], hook["secret"])
lark.triggers.create("coder", name="nightly-tests", kind="schedule",
schedule="0 2 * * *", timezone="America/New_York",
prompt="Run the test suite and fix anything that's red.")
# The sending service signs its body with the trigger's secret:
BODY='{"name":"Ada Lovelace","company":"Analytical Engines"}'
SIG="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$TRIGGER_SECRET" -hex | sed 's/.* //')"
curl -X POST "$TRIGGER_URL" -H "content-type: application/json" -H "X-Lark-Signature: $SIG" -d "$BODY"
# → 202 {"runId": "run_…", "status": "queued"}
If the sender can't sign, call <url>/<secret> instead. Lark answers 202 with the run id, 401 for a bad signature, 429 past the daily cap, and 402 past your budget.
Run webhooks
Pass webhookUrl when starting a run, and Lark POSTs the run there when it finishes, retrying with backoff if your server fails. The body is {"event": "run.succeeded" | "run.failed" | "run.cancelled", "run": {…}}. Every delivery is signed with your organization's webhook signing secret, which admins see in Settings → API keys: X-Lark-Signature: sha256=<hex HMAC-SHA256 of the raw body>.
import { verifyWebhook } from "lark-agents";
app.post("/hooks/lark", express.raw({ type: "application/json" }), async (req, res) => {
const ok = await verifyWebhook(req.body, req.get("x-lark-signature"), process.env.LARK_WEBHOOK_SECRET!);
if (!ok) return res.sendStatus(401);
const { event, run } = JSON.parse(req.body.toString());
if (event === "run.succeeded") await saveReport(run.id, run.result);
res.sendStatus(200);
});
from lark_agents import verify_webhook
@app.post("/hooks/lark")
def lark_hook():
raw = request.get_data()
if not verify_webhook(raw, request.headers.get("X-Lark-Signature"), os.environ["LARK_WEBHOOK_SECRET"]):
abort(401)
payload = json.loads(raw)
if payload["event"] == "run.succeeded":
save_report(payload["run"]["id"], payload["run"]["result"])
return "", 200
Always verify against the raw body, before parsing it. Answer with a 2xx quickly; do slow work afterwards.
Skills
A skill is reusable know-how (how you deploy, a research method, a checklist) that agents load when a task calls for it. Draft one with AI from a description or from a run that went well, review it, then save it and attach it to agents.
const { draft } = await lark.skills.draft({ runId: run.id }); // learn from a run that went well
await lark.skills.create(draft);
await lark.agents.update("researcher", { skills: ["company-research-brief", draft.name] });
draft = lark.skills.draft(run_id=run["id"])["draft"]
lark.skills.create(**draft)
lark.agents.update("researcher", skills=["company-research-brief", draft["name"]])
Secrets
API keys and tokens your agents need (a GitHub token, a CRM key) live encrypted in your organization and are only put into the machines of the agents you attach them to, as environment variables. Values are write-only: you can list names and replace values, never read them back. They're also removed from logs.
await lark.secrets.set("GITHUB_TOKEN", process.env.GITHUB_TOKEN!);
await lark.agents.update("coder", { secrets: ["GITHUB_TOKEN"] });
lark.secrets.set("GITHUB_TOKEN", os.environ["GITHUB_TOKEN"])
lark.agents.update("coder", secrets=["GITHUB_TOKEN"])
Usage & billing
GET /api/org/billing (or lark.usage()) returns the month's spend, by agent, by day and by API key, plus what's running now, the plan, its limits and the budget. Use it to watch an integration's spend from your own code.
Errors
Errors are JSON: {"error": "what went wrong", "code": "…"}. The SDKs raise them as LarkError, with status and code.
| Status | Code | Meaning |
|---|---|---|
| 400 | The request isn't valid. details says which field. | |
| 401 | Missing, wrong or revoked key. | |
| 402 | budget_reached | The organization's monthly budget is used up. |
| 402 | key_cap | This key's own monthly cap is used up. |
| 402 | plan_limit | The plan doesn't include this, or its included usage is used up (Free). |
| 402 | payment_failed | The last payment failed; update the card in Usage & billing. |
| 402 | suspended | The organization is suspended. Contact support. |
| 403 | The key's role can't do this, or only a signed-in person can. | |
| 404 | No such agent, run or resource in this organization. | |
| 409 | Not now: for example, messaging a run that already finished. | |
| 429 | rate_limited | Too many requests. Wait and retry with backoff. |
Rate limits
- Starting runs: 30 per minute per API key, by default.
- Inbound trigger webhooks: 120 requests per minute per trigger, plus each trigger's daily run cap.
- Agents running at once: set by your plan. Extra runs queue and start as others finish.
A 429 means slow down. Retry after a short pause, with exponential backoff.
Objects
Run
id | string | run_… |
status | string | queued, running, succeeded, failed, cancelled |
agent | object | The agent as it was when the run started. |
prompt, repo, ref | string | What it was asked to do. |
result | string | null | The final report (Markdown). |
error | string | null | Why it failed. |
cost | object | null | billableUsd, modelUsd, computeUsd, vmSeconds, updated while it runs. |
usage | object | inputTokens, outputTokens, steps. |
createdBy | string | null | Who started it: an email, key:<prefix> for an API key, or a trigger. |
triggerId | string | null | The trigger that started it. |
createdAt, startedAt, finishedAt | string | ISO 8601, UTC. |
Need something these docs don't cover? Every endpoint the dashboard uses is in the full API reference.