Skip to main content
The Duvo Public API lets you drive Duvo from your own code, scripts, and pipelines. This page is the orientation hub: what you can do, which endpoints to reach for, and how to put them together into real integrations. For request and response schemas, follow the links into the API Reference.

What you can do with the API

GoalEntities involved
Start a Job from your application or pipelineJobs
Create and update Assignments programmaticallyAssignments, Revisions
Sync SOPs from a Git repository into DuvoSkills
Push work items into a queue from an external systemQueues, Cases
Schedule recurring JobsSchedules
Connect and provision Connections without the UIConnections
Upload files for an Assignment to processSandboxes

Authentication

All requests use a team-scoped API key in the Authorization header:
curl https://api.duvo.ai/v2/teams/$TEAM_ID/agents \
  -H "Authorization: Bearer $DUVO_API_KEY"
Generate a key at Team Settings → API Keys in the Duvo dashboard. Keys are team-scoped and inherit the permissions of the user who created them — only Admins and Owners can create them. Keys use the format dv_<random> and are shown once at creation, so store them immediately.
  • Base URL: https://api.duvo.ai/v2
  • Rate limit: 300 requests per minute per API key. Responses include x-ratelimit-limit, x-ratelimit-remaining, and x-ratelimit-reset headers. On 429, honor the retry-after header.
  • Error model: errors return { "error": "...", "message": "..." } with a standard HTTP status code (400, 401, 403, 404, 413, 429, 5xx).
For the full auth, rate-limit, and error reference, see Running Assignments via API.
Collection endpoints are team-scoped (/teams/\{teamId\}/...); endpoints that act on a specific resource are addressed directly by ID (/agents/\{agent_id\}, /runs/\{run_id\}).

Assignments and Revisions

Assignments are the units of work in Duvo. A Revision is a versioned snapshot of an Assignment’s Setup — SOP, model settings, and attached skills. Jobs always run against an Assignment’s latest Revision. What you can do:
  • List, create, and update Assignments
  • Create Revisions to deploy SOP changes programmatically
  • Organize Assignments into folders
ActionMethodPath
List AssignmentsGET/teams/{teamId}/agents
Create an AssignmentPOST/teams/{teamId}/agents
Get an AssignmentGET/agents/{agent_id}
Update an AssignmentPATCH/agents/{agent_id}
List RevisionsGET/agents/{agent_id}/revisions
Create a RevisionPOST/agents/{agent_id}/revisions
See the API Reference for request and response schemas, and Creating Assignments via API for worked examples including folders and the full create flow. You can create an Assignment and its first Revision in a single request — pass a build object in the body:
curl -X POST https://api.duvo.ai/v2/teams/$TEAM_ID/agents \
  -H "Authorization: Bearer $DUVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Invoice Processor",
    "build": {
      "name": "v1",
      "config": {
        "version": "v2",
        "data": {
          "input": "Process invoices from uploaded files, extract line items, and post a summary to Slack."
        }
      }
    }
  }'
Organize Assignments into folders:
ActionMethodPath
List foldersGET/teams/{teamId}/agent-folders
Create a folderPOST/teams/{teamId}/agent-folders
Update a folderPATCH/agent-folders/{folder_id}
Delete a folderDELETE/agent-folders/{folder_id}
Move Assignments to folderPOST/teams/{teamId}/agent-folders/move-agents

Jobs

A Job is a single execution of an Assignment. Jobs are asynchronous — the API returns immediately and you poll or use a webhook to track completion. What you can do:
  • Start a Job with optional file input, an initial message, and a webhook for event notifications
  • Poll for status (pending, running, waiting, completed, failed, stopped)
  • Read the conversation — everything the Assignment did, step by step
  • Respond to human-in-the-loop requests programmatically
  • Stop a running Job
ActionMethodPath
Start a JobPOST/teams/{teamId}/runs
List JobsGET/teams/{teamId}/runs
Get Job statusGET/runs/{run_id}
Read messagesGET/runs/{run_id}/messages
Send a messagePOST/runs/{run_id}/messages
Respond to HITL requestPOST/runs/{run_id}/human-requests/{request_id}/respond
Stop a JobPOST/runs/{run_id}/stop
See the API Reference for schemas, and Running Assignments via API for the full flow including file uploads and HITL webhooks. Starting a Job and polling for completion:
#!/bin/bash
# Start a Job
RUN=$(curl -s -X POST https://api.duvo.ai/v2/teams/$TEAM_ID/runs \
  -H "Authorization: Bearer $DUVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"agent_id\": \"$AGENT_ID\"}")
RUN_ID=$(echo $RUN | jq -r '.run.id')

# Poll until the Job finishes
while true; do
  STATUS=$(curl -s https://api.duvo.ai/v2/runs/$RUN_ID \
    -H "Authorization: Bearer $DUVO_API_KEY" | jq -r '.run.status')
  [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] || [ "$STATUS" = "stopped" ] && break
  sleep 5
done

# Read what the Assignment did
curl -s "https://api.duvo.ai/v2/runs/$RUN_ID/messages?limit=100" \
  -H "Authorization: Bearer $DUVO_API_KEY" | jq '.messages[]'

Skills

Skills package reusable knowledge — SOPs, rule books, taxonomies — into a zip that any Assignment can use. Managing skills via the API lets you keep your SOPs in a Git repository and sync them to Duvo on every push, without anyone clicking through the UI. What you can do:
  • List team skills and system skills
  • Create a skill from text content
  • Upload a multi-file skill as a zip
  • Download a skill as a zip
  • Update individual skill files
  • Delete a skill
ActionMethodPath
List team skillsGET/teams/{teamId}/skills
List system skillsGET/skills/system
Create a skill (text)POST/teams/{teamId}/skills
Upload a skill (zip)POST/teams/{teamId}/skills/upload
Get skill filesGET/skills/{skill_id}/files
Get a skill fileGET/skills/{skill_id}/files/{path}
Update a skill filePUT/skills/{skill_id}/files/{path}
Download skill as zipGET/skills/{skill_id}/download
Delete a skillDELETE/skills/{skill_id}
See the API Reference for schemas and Creating Custom Skills for how to structure your SKILL.md files. Creating a skill from text:
curl -X POST https://api.duvo.ai/v2/teams/$TEAM_ID/skills \
  -H "Authorization: Bearer $DUVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "refund-policy",
    "description": "Guidelines for processing customer refund requests. Use when evaluating refund eligibility or drafting refund responses.",
    "content": "---\nname: refund-policy\ndescription: Guidelines for processing customer refund requests.\n---\n\n# Refund Policy\n\nRefunds are accepted within 30 days of purchase..."
  }'
Uploading a multi-file skill as a zip: A zip must contain a SKILL.md at the root with name and description frontmatter. Supporting documents, templates, and examples can live in subfolders.
# Zip your skill folder first
cd my-skill && zip -r ../my-skill.zip . && cd ..

curl -X POST https://api.duvo.ai/v2/teams/$TEAM_ID/skills/upload \
  -H "Authorization: Bearer $DUVO_API_KEY" \
  -F "file=@my-skill.zip"
Syncing skills from a Git repository (CI example): This script pushes a directory of skill folders to Duvo on every deploy. It deletes stale skills by name and re-creates them from the latest source.
#!/bin/bash
# skills-sync.sh — run on every push to your skills repo
DUVO_API_KEY="${DUVO_API_KEY}"
TEAM_ID="${DUVO_TEAM_ID}"
BASE_URL="https://api.duvo.ai/v2"
SKILLS_DIR="./skills"  # directory of skill folders

# Fetch existing team skills
EXISTING=$(curl -s "$BASE_URL/teams/$TEAM_ID/skills" \
  -H "Authorization: Bearer $DUVO_API_KEY")

for SKILL_DIR in "$SKILLS_DIR"/*/; do
  SKILL_NAME=$(basename "$SKILL_DIR")

  # Delete the old version if it exists
  OLD_ID=$(echo $EXISTING | jq -r ".skills[] | select(.name == \"$SKILL_NAME\") | .id")
  if [ -n "$OLD_ID" ] && [ "$OLD_ID" != "null" ]; then
    curl -s -X DELETE "$BASE_URL/skills/$OLD_ID" \
      -H "Authorization: Bearer $DUVO_API_KEY"
    echo "Deleted old version of $SKILL_NAME"
  fi

  # Zip and upload the updated skill
  cd "$SKILL_DIR" && zip -r "/tmp/${SKILL_NAME}.zip" . && cd -
  RESULT=$(curl -s -X POST "$BASE_URL/teams/$TEAM_ID/skills/upload" \
    -H "Authorization: Bearer $DUVO_API_KEY" \
    -F "file=@/tmp/${SKILL_NAME}.zip")
  echo "Uploaded $SKILL_NAME: $(echo $RESULT | jq -r '.skill.id')"
done

echo "Skill sync complete."

Queues and Cases

Queues hold individual work items — called Cases — that Assignments process one at a time. You can push Cases into a queue from any external system, which makes it the standard pattern for high-volume integrations where events arrive faster than a single Assignment can handle them. What you can do:
  • List and inspect queues
  • Push Cases into a queue (one at a time or in batch)
  • List and search Cases in a queue
  • Bulk-update, delegate, retry, or delete Cases
  • Track Case status through the processing lifecycle
ActionMethodPath
List queuesGET/teams/{teamId}/queues
Get a queueGET/queues/{queue_id}
List Cases in a queueGET/queues/{queue_id}/cases
Search CasesPOST/queues/{queue_id}/cases/search
Create CasesPOST/queues/{queue_id}/cases
Bulk-update Case statusPOST/queues/{queue_id}/cases/bulk-update-status
Bulk-delegate CasesPOST/queues/{queue_id}/cases/bulk-delegate
Bulk-retry CasesPOST/queues/{queue_id}/cases/bulk-retry
Delete a CaseDELETE/cases/{case_id}
See the API Reference for schemas. Case statuses:
StatusMeaning
pendingWaiting to be picked up
claimedAn Assignment is actively working on it
completedProcessing finished successfully
failedProcessing failed
When listing Cases you can also filter by needs_input (Cases with an open human request) and postponed (Cases scheduled to retry later). These are derived from Case fields (pending_human_request_id, postponed_to) rather than stored status values. Pushing a Case from an external system. Wrap a single Case in a case object. The data field is free-form text or a JSON string — Assignments receive it when they claim the Case:
curl -X POST "https://api.duvo.ai/v2/queues/$QUEUE_ID/cases" \
  -H "Authorization: Bearer $DUVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "case": {
      "title": "Order #ORD-20240115-4821",
      "data": "{\"order_id\":\"ORD-20240115-4821\",\"customer_email\":\"buyer@example.com\",\"total\":89.97,\"notes\":\"Gift wrap requested\"}"
    }
  }'
Batch-pushing Cases for high-volume intake. Use a cases array instead:
curl -X POST "https://api.duvo.ai/v2/queues/$QUEUE_ID/cases" \
  -H "Authorization: Bearer $DUVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cases": [
      { "title": "Invoice #INV-001", "data": "{\"supplier\":\"ACME\",\"amount\":1200.00}" },
      { "title": "Invoice #INV-002", "data": "{\"supplier\":\"GlobalParts\",\"amount\":580.50}" },
      { "title": "Invoice #INV-003", "data": "{\"supplier\":\"ShipFast\",\"amount\":345.00}" }
    ]
  }'
The response returns the created Cases under added_cases, each with its id and status. Once Cases are in the queue, an Assignment with a Case Trigger picks them up automatically — see Case Queue for how to configure the trigger. Validate case-queue wiring before you start work. Check that a Revision’s case-queue integration slots are linked to real queues. The response reports, per producer/consumer slot, how many queues are linked. A slot with linked_queue_count of 0 is attached but points at no queue and will fail at runtime — link a queue before starting Jobs.
ActionMethodPath
Validate a Revision’s case queueGET/agents/{agent_id}/revisions/{build_id}/case-queue-validation

Schedules

You can create, list, update, and delete an Assignment’s schedules via the API to run recurring Jobs.
ActionMethodPath
List schedulesGET/agents/{agent_id}/schedules
Create a schedulePOST/agents/{agent_id}/schedules
Update a schedulePATCH/agents/{agent_id}/schedules/{schedule_id}
Delete a scheduleDELETE/agents/{agent_id}/schedules/{schedule_id}
See the API Reference for schedule fields and limits.

Connections

Connections link your Assignments to external services. The API lets you list and inspect connections, create user-provided connections (custom MCP servers with an API key), and initiate OAuth flows.
ActionMethodPath
List connectionsGET/teams/{teamId}/connections
Get a connectionGET/connections/{connection_id}
Create a connection (user-provided)POST/teams/{teamId}/connections
Update a connectionPATCH/connections/{connection_id}
Delete a connectionDELETE/connections/{connection_id}
Start native OAuth (Gmail, Sheets, Outlook…)POST/teams/{teamId}/connections/oauth/native/{provider}/start
Start Composio OAuth (Slack, HubSpot…)POST/teams/{teamId}/connections/composio/start
Finalize Composio OAuthPOST/teams/{teamId}/connections/composio/finalize
Probe a custom MCP serverPOST/teams/{teamId}/connections/mcp/probe
See the API Reference for schemas and Creating Assignments via API for the full OAuth flows.

Sandboxes and file uploads

When your Assignment needs to process files — CSVs, PDFs, images — upload them to a Sandbox before starting the Job, then pass the sandbox_id when you start the run.
ActionMethodPath
Create a SandboxPOST/sandboxes
Get upload URLsPOST/sandboxes/{sandbox_id}/upload-urls
Upload a filePOST/sandboxes/{sandbox_id}/files
List filesGET/sandboxes/{sandbox_id}/files
See the API Reference for schemas and Running Assignments via API for the complete file-upload pattern.

End-to-end example: order management integration

This example shows an order management system (OMS) pushing new orders into Duvo for processing whenever they arrive. Duvo validates each order, checks inventory, and posts the result back to the OMS. Architecture:
OMS → POST /queues/{queue_id}/cases (one case per order)
Duvo Assignment (Case Trigger) → picks up case, processes it
Assignment → calls OMS webhook with result
Step 1: Configure Duvo. Create an Assignment with a Case Queue trigger pointed at your order-processing queue. The Assignment’s SOP instructs it to read Case data, validate the order, check inventory via your ERP connection, and post the result to your OMS webhook. Step 2: OMS integration code.
const DUVO_API_KEY = process.env.DUVO_API_KEY;
const DUVO_QUEUE_ID = process.env.DUVO_ORDER_QUEUE_ID;
const BASE_URL = "https://api.duvo.ai/v2";

interface Order {
  orderId: string;
  customerEmail: string;
  items: Array<{ sku: string; quantity: number; unitPrice: number }>;
  total: number;
}

async function submitOrderToDuvo(order: Order): Promise<string> {
  const response = await fetch(`${BASE_URL}/queues/${DUVO_QUEUE_ID}/cases`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${DUVO_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      case: {
        title: `Order ${order.orderId}`,
        data: JSON.stringify(order),
      },
    }),
  });

  if (!response.ok) {
    const err = await response.json();
    throw new Error(`Duvo API error: ${err.error}${err.message}`);
  }

  const { added_cases } = await response.json();
  return added_cases[0].id;
}

// Called when a new order arrives in the OMS
async function onNewOrder(order: Order) {
  const caseId = await submitOrderToDuvo(order);
  console.info(`Order ${order.orderId} submitted as Case ${caseId}`);
}
Step 3: Track results. List Cases to see their status and retrieve Assignment output:
# Check failed orders
curl "https://api.duvo.ai/v2/queues/$QUEUE_ID/cases?status=failed" \
  -H "Authorization: Bearer $DUVO_API_KEY" | jq '.cases[] | {id, title, status}'

Next steps