Skip to main content

API Quickstart: Create an Agent and Send a Message

This walkthrough uses Bash, curl, and jq. Select your licensed Organization and create an Administrator API key in API keys (/app/data/api-keys) for agent creation and setup. The Console defaults new keys to Operator, which can message existing agents but cannot create them. You can use narrower keys after setup; Organization keys also require their issuer's current Organization-wide agent authority as described in Authentication.

With the default COMPANY_LICENSE_ENFORCEMENT=true, agent execution requires a licensed Organization; new personal agent creation and personal execution are unavailable. Authorized historical reads remain subject to existing ownership and scope checks. Sending external communication also depends on verified email, contact permissions, security restrictions, and deployment configuration. Customer-run deployments need configured models, a working email endpoint, Redis, and workers to process queued work. ping verifies authentication and web connectivity; it does not test license, worker or model readiness.

Set the base URL to your actual deployment and enter an existing key without embedding it in a script:

export API_BASE="https://your-deployment.example/api/v1"
read -r -s -p "API key: " API_KEY
export API_KEY
curl --fail-with-body -H "X-Api-Key: $API_KEY" "$API_BASE/ping/"

For a remote deployment, use its HTTPS origin followed by /api/v1. Keep the trailing slash on endpoint paths.

Create an agent without a primary schedule​

CREATE_KEY="$(uuidgen)"
AGENT_JSON=$(curl --fail-with-body -X POST "$API_BASE/agents/" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $CREATE_KEY" \
-d '{"name":"API Quickstart","charter":"Answer incoming questions briefly.","schedule":null}')
AGENT_ID=$(jq -er '.id' <<<"$AGENT_JSON")

Creation returns 201 and the agent representation. A duplicate name in the same workspace returns a validation error; use a new name for a separate demo. Preserve CREATE_KEY and the exact body if you need to retry the same creation. This active agent can process its charter immediately and receive a separate first-check-in timer even with schedule: null; see creation and setup effects for preparing a paused agent.

Discover the assigned email address:

curl --fail-with-body -H "X-Api-Key: $API_KEY" \
"$API_BASE/agents/$AGENT_ID/contact-endpoints/"

Submit authenticated application input​

On a deployment that has enabled API_APPLICATION_REQUESTS_ENABLED, use the Application Requests workflow. Admission defaults off. Service clients use an existing granted agent; agent creation above remains a member-key operation.

REQUEST_KEY="$(uuidgen)"
REQUEST_JSON=$(curl --fail-with-body -X POST "$API_BASE/agents/$AGENT_ID/requests/" \
-H "X-Api-Key: $API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $REQUEST_KEY" -d '{"body":"What is 17 plus 25?"}')
REQUEST_ID=$(jq -er '.request_id' <<<"$REQUEST_JSON")
curl --fail-with-body -H "X-Api-Key: $API_KEY" \
"$API_BASE/agents/$AGENT_ID/requests/$REQUEST_ID/responses/"

202 means durable acceptance. Retry the identical body/key after an ambiguous response, then use request detail, its revision wait, and its own response collection. Those reads distinguish publication, execution, holds and reply state without matching another request's timeline reply. Shared agents retain shared model context. If admission is unavailable, the existing member-key channel workflow below remains supported.

Send a message from an allowed sender​

Use an address allowed by the agent's contact policy. For a personal agent with default policy, use the owner's account email. For a service address, add it through the contact API first. The earlier example's default policy does not authorize an arbitrary external sender.

read -r -p "Allowed sender email: " SENDER_EMAIL
MESSAGE_KEY="$(uuidgen)"
MESSAGE_BODY=$(jq -n --arg sender "$SENDER_EMAIL" \
'{channel:"email",sender:$sender,body:"Reply with a short greeting."}')
MESSAGE_JSON=$(curl --fail-with-body -X POST "$API_BASE/agents/$AGENT_ID/messages/" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $MESSAGE_KEY" \
-d "$MESSAGE_BODY")
CURSOR=$(jq -er '.event.cursor' <<<"$MESSAGE_JSON")

Omitting recipient selects the agent's primary email endpoint. The 201 response wraps the inbound timeline event in event; it is not the agent's reply. Capture its cursor before waiting so a fast response cannot be skipped by taking a later timeline snapshot.

Wait for an outbound reply​

for attempt in 1 2 3 4; do
WAIT_BODY=$(jq -n --arg cursor "$CURSOR" \
'{after_cursor:$cursor,timeout_seconds:30,event_types:["message"],filters:{from_actor_type:"agent",channel:"email"}}')
WAIT_JSON=$(curl --fail-with-body --max-time 40 -X POST \
"$API_BASE/agents/$AGENT_ID/events/wait/" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" -d "$WAIT_BODY") || break
CURSOR=$(jq -er '.latest_cursor' <<<"$WAIT_JSON") || break
if jq -e '.matched' <<<"$WAIT_JSON" > /dev/null; then
jq '.events' <<<"$WAIT_JSON"
break
fi
done

Timeouts return 200 with matched: false and timed_out: true; they do not cancel agent work. A reply might need a contact approval, outbox review, restriction resolution, or worker recovery. Inspect pending actions, /processing-status/, and the timeline when no reply arrives. For concurrent requests, inspect message.inReplyToMessageId when present and apply your own correlation; an agent/channel filter alone matches any qualifying outbound message.

Archive the demo agent after use:

curl --fail-with-body -X DELETE -H "X-Api-Key: $API_KEY" \
"$API_BASE/agents/$AGENT_ID/"

For authenticated client messages with explicit request/reply lineage, use gated Application Requests. Remote MCP retains its existing internal-message/file contract. The legacy REST messages route models inbound email and applies its channel allowlist.