Quickstart

1. Configure and deploy an agent

Create an agent, attach a rule and a vault, then deploy it.

Five short calls take you from a blank organization to a running agent that knows your brand voice and has a secret it can use.

The agent we'll build is a "Support agent" with:

  • A rule describing how the agent should talk to customers (its brand voice).
  • A vault with one secret (STRIPE_API_KEY) the agent can use when calling Stripe.

Setup

Pick a language and copy the imports/setup once at the top of your file. Every step below uses these helpers.

BASE=https://api.nairi.ai/api/public/v1
AUTH="Authorization: Bearer $NAIRI_API_KEY"
CT="Content-Type: application/json"
const BASE = "https://api.nairi.ai/api/public/v1";
const HEADERS = {
  Authorization: `Bearer ${process.env.NAIRI_API_KEY}`,
  "Content-Type": "application/json",
};
require "net/http"
require "json"
require "uri"

BASE = "https://api.nairi.ai/api/public/v1"
HEADERS = {
  "Authorization" => "Bearer #{ENV['NAIRI_API_KEY']}",
  "Content-Type" => "application/json",
}

def post(path, body)
  uri = URI("#{BASE}#{path}")
  req = Net::HTTP::Post.new(uri, HEADERS)
  req.body = body.to_json
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
  JSON.parse(res.body)
end
import os
import requests

BASE = "https://api.nairi.ai/api/public/v1"
HEADERS = {
    "Authorization": f"Bearer {os.environ['NAIRI_API_KEY']}",
    "Content-Type": "application/json",
}
package main

import (
	"bytes"
	"encoding/json"
	"io"
	"net/http"
	"os"
)

const base = "https://api.nairi.ai/api/public/v1"

func post(path string, body any) (map[string]any, error) {
	buf, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequest("POST", base+path, bytes.NewReader(buf))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("NAIRI_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	raw, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}
	var out map[string]any
	if len(raw) == 0 {
		return nil, nil
	}
	return out, json.Unmarshal(raw, &out)
}

Step 1: Create the agent

The agent's slug (agent_id) is the human-readable identifier you'll pass to every other endpoint. It's derived from name.

AGENT_ID=$(curl -s -X POST $BASE/agents \
  -H "$AUTH" -H "$CT" \
  -d '{"name": "Support agent", "instances_count": 1}' \
  | jq -r .agent_id)
const agentRes = await fetch(`${BASE}/agents`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({ name: "Support agent", instances_count: 1 }),
});
const agent = (await agentRes.json()) as { agent_id: string };
const agentId = agent.agent_id;
agent = post("/agents", { name: "Support agent", instances_count: 1 })
agent_id = agent["agent_id"]
agent = requests.post(
    f"{BASE}/agents",
    headers=HEADERS,
    json={"name": "Support agent", "instances_count": 1},
).json()
agent_id = agent["agent_id"]
agent, err := post("/agents", map[string]any{
	"name":            "Support agent",
	"instances_count": 1,
})
if err != nil {
	panic(err)
}
agentID := agent["agent_id"].(string)

Step 2: Create a rule

Rules are text fragments that get injected into the agent's system prompt. Brand voice, policies, runbooks — anything you'd otherwise paste into every prompt.

RULE_ID=$(curl -s -X POST $BASE/artifacts/rules \
  -H "$AUTH" -H "$CT" \
  -d '{
    "title": "Brand voice",
    "description": "How the agent talks to customers",
    "content": "Always respond in plain English. No bullet lists unless asked."
  }' | jq -r .id)
const ruleRes = await fetch(`${BASE}/artifacts/rules`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({
    title: "Brand voice",
    description: "How the agent talks to customers",
    content: "Always respond in plain English. No bullet lists unless asked.",
  }),
});
const rule = (await ruleRes.json()) as { id: string };
const ruleId = rule.id;
rule = post("/artifacts/rules", {
  title: "Brand voice",
  description: "How the agent talks to customers",
  content: "Always respond in plain English. No bullet lists unless asked.",
})
rule_id = rule["id"]
rule = requests.post(
    f"{BASE}/artifacts/rules",
    headers=HEADERS,
    json={
        "title": "Brand voice",
        "description": "How the agent talks to customers",
        "content": "Always respond in plain English. No bullet lists unless asked.",
    },
).json()
rule_id = rule["id"]
rule, err := post("/artifacts/rules", map[string]any{
	"title":       "Brand voice",
	"description": "How the agent talks to customers",
	"content":     "Always respond in plain English. No bullet lists unless asked.",
})
if err != nil {
	panic(err)
}
ruleID := rule["id"].(string)

Step 3: Create a vault and add a secret

Vaults are logical containers for secrets. Secrets are encrypted at rest and scoped to specific outbound domains — the agent can only use a secret when it's calling one of the allowed_domains.

VAULT_ID=$(curl -s -X POST $BASE/vaults \
  -H "$AUTH" -H "$CT" \
  -d '{"name": "Production secrets"}' | jq -r .id)

curl -s -X POST $BASE/vaults/$VAULT_ID/secrets \
  -H "$AUTH" -H "$CT" \
  -d '{
    "env_key": "STRIPE_API_KEY",
    "value": "sk_live_...",
    "allowed_domains": ["*.stripe.com"]
  }'
const vaultRes = await fetch(`${BASE}/vaults`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({ name: "Production secrets" }),
});
const vault = (await vaultRes.json()) as { id: string };
const vaultId = vault.id;

await fetch(`${BASE}/vaults/${vaultId}/secrets`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({
    env_key: "STRIPE_API_KEY",
    value: "sk_live_...",
    allowed_domains: ["*.stripe.com"],
  }),
});
vault = post("/vaults", { name: "Production secrets" })
vault_id = vault["id"]

post("/vaults/#{vault_id}/secrets", {
  env_key: "STRIPE_API_KEY",
  value: "sk_live_...",
  allowed_domains: ["*.stripe.com"],
})
vault = requests.post(
    f"{BASE}/vaults",
    headers=HEADERS,
    json={"name": "Production secrets"},
).json()
vault_id = vault["id"]

requests.post(
    f"{BASE}/vaults/{vault_id}/secrets",
    headers=HEADERS,
    json={
        "env_key": "STRIPE_API_KEY",
        "value": "sk_live_...",
        "allowed_domains": ["*.stripe.com"],
    },
)
vault, err := post("/vaults", map[string]any{"name": "Production secrets"})
if err != nil {
	panic(err)
}
vaultID := vault["id"].(string)

if _, err := post("/vaults/"+vaultID+"/secrets", map[string]any{
	"env_key":         "STRIPE_API_KEY",
	"value":           "sk_live_...",
	"allowed_domains": []string{"*.stripe.com"},
}); err != nil {
	panic(err)
}

Step 4: Attach the rule and the vault to the agent

Resources live independently of agents. To make the agent actually see the rule and the secret, attach them.

curl -s -X POST $BASE/agents/$AGENT_ID/resources \
  -H "$AUTH" -H "$CT" \
  -d "{\"entity_type\":\"agent_artifact_rule\",\"entity_id\":\"$RULE_ID\"}"

curl -s -X POST $BASE/agents/$AGENT_ID/resources \
  -H "$AUTH" -H "$CT" \
  -d "{\"entity_type\":\"vault\",\"entity_id\":\"$VAULT_ID\"}"
for (const [entityType, entityId] of [
  ["agent_artifact_rule", ruleId],
  ["vault", vaultId],
] as const) {
  await fetch(`${BASE}/agents/${agentId}/resources`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({ entity_type: entityType, entity_id: entityId }),
  });
}
[
  ["agent_artifact_rule", rule_id],
  ["vault", vault_id],
].each do |entity_type, entity_id|
  post("/agents/#{agent_id}/resources", {
    entity_type: entity_type,
    entity_id: entity_id,
  })
end
for entity_type, entity_id in [
    ("agent_artifact_rule", rule_id),
    ("vault", vault_id),
]:
    requests.post(
        f"{BASE}/agents/{agent_id}/resources",
        headers=HEADERS,
        json={"entity_type": entity_type, "entity_id": entity_id},
    )
attachments := []struct {
	entityType string
	entityID   string
}{
	{"agent_artifact_rule", ruleID},
	{"vault", vaultID},
}
for _, a := range attachments {
	if _, err := post("/agents/"+agentID+"/resources", map[string]any{
		"entity_type": a.entityType,
		"entity_id":   a.entityID,
	}); err != nil {
		panic(err)
	}
}

Step 5: Deploy

POST /agents/{agent_id}/deploy is asynchronous — it returns 202 Accepted immediately while the container comes up in the background. Set update_config_only: true to roll config without restarting the container.

curl -s -X POST $BASE/agents/$AGENT_ID/deploy \
  -H "$AUTH" -H "$CT" \
  -d '{"update_config_only": false}'
await fetch(`${BASE}/agents/${agentId}/deploy`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({ update_config_only: false }),
});
post("/agents/#{agent_id}/deploy", { update_config_only: false })
requests.post(
    f"{BASE}/agents/{agent_id}/deploy",
    headers=HEADERS,
    json={"update_config_only": False},
)
if _, err := post("/agents/"+agentID+"/deploy", map[string]any{
	"update_config_only": false,
}); err != nil {
	panic(err)
}

The agent is now deploying. Head to Drive a conversation to send it its first prompt.

On this page