1. Use a catalog agent
Discover an Invoice Processing agent, or configure your own Knowledge Base agent with your organization’s sources.
Discover → install and configure → execute → poll the result or review queue.
The AI native company for Healthcare
Platform guide
Choose one of three paths: discover an Invoice Processing agent or configure your own Knowledge Base agent, build a workflow isolated within your XY organization using the full self-service toolset, or follow a guided specification process that gives XY what it needs to build and deliver a custom agent.
Choose a surface
Discover an Invoice Processing agent, or configure your own Knowledge Base agent with your organization’s sources.
Discover → install and configure → execute → poll the result or review queue.
You want to build and own a workflow isolated within your XY organization using the Planner agent, Browser Studio, credentials and MFA, schedules, and observability.
Plan → test → control browser steps → accept → promote → schedule → observe.
The catalog does not fit the job, a required component is not self-service, or you want XY to own delivery.
Create request → redirect to form_url → partner answers and submits → XY builds and delivers the custom agent.
Resource model
| Object | What it means | What comes next |
|---|---|---|
| Agent | A published, versioned business capability. | Create an installation with its setup schema. |
| Installation | One exact agent version plus sources, configuration, and required connection state. | Wait for ready/degraded, then create executions. |
| Execution | One asynchronous run of an installation. | Poll status/result and handle any review items. |
| Planner agent build | A durable authoring conversation with tested steps and explicit decisions. | Promote the accepted build to a production definition. |
| Workflow definition | The promoted, reusable production workflow. | Run it once on demand or create a recurring schedule. |
| Workflow run | One execution of a promoted workflow definition. | Read safe phase projections and governed analytics. |
Authentication
export XY_API_BASE='https://api.xy.ai/api/v1'
export XY_API_KEY='xy_prod_…'
curl --fail-with-body \
--header "Authorization: Bearer ${XY_API_KEY}" \
"${XY_API_BASE}/auth/check"An authorized organization user chooses Personal or Service, then creates, tests, edits, rotates, or revokes the key. The complete value is shown only to the user who created or rotated it and only until the page is left or reloaded. Other administrators see metadata and the public prefix—not each other's plaintext keys. A personal key can be rotated only by its bound user.
Personal key: use for work performed by one person. XY binds every request to that user, reapplies their current organization membership and workflow permissions, and rejects the key when that access is removed.
Service key: use for an unattended backend or integration. Calls identify the service credential and organization, not whichever person happens to operate the service.
| Job | Typical minimum scopes | Add only when needed |
|---|---|---|
| Run a published agent | agents:read, installations:*, executions:* | review_items:*, metrics:read |
| Build a workflow | planner:read, planner:write | planner:view, planner:control, planner:promote |
| Design an existing workflow queue | queue_ui:read, queue_ui:write | queue_ui:publish after exact human or system approval. Personal callers need explicit workflow access/configure permission; system keys use authorized organization access. API approval additionally requires system-only queue_ui:approve. |
| Author a browser step | browser:read, browser:write, planner:write | browser:control, browser:debug |
| Run a promoted workflow once | executions:write, executions:read | No schedule scope is required. |
| Operate recurring production | schedules:read, schedules:write | executions:read, dashboards:read, usage:read |
| Create a missing API integration | integrations:read, integrations:generate, integrations:promote, credentials:write | Add integrations:connect when managing service connections outside factory validation. |
| Propose an analytics dashboard (release preview) | dashboards:propose | dashboards:read is separate analytics-query access. Human publication remains in XY. |
| Ask XY to build an agent | agent_requests:read, agent_requests:write | No Planner agent or browser scopes are required. |
An asterisk above means the separate read and write scopes. The API-key editor shows the exact current catalog. Analytics access returns aggregate metrics and approved tile data that a partner can render in its own UI. Dashboard proposal authoring is a separate release preview, not an extension of read-only analytics access. For presentation changes to an eligible app-managed Planner entity queue, use Queue UI Designer.
First result
# Use a small public documentation site that you control.
: "${FIRST_RESULT_ATTEMPT_ID:?Set a stable unique ID for this quickstart}"
retry_after_seconds() {
local header_file="$1" fallback="$2" value
value="$(awk 'tolower($1) == "retry-after:" {gsub("\r", "", $2); print $2; exit}' "${header_file}")"
case "${value}" in
""|*[!0-9]*) value="${fallback}" ;;
esac
printf '%s\n' "${value}"
}
INSTALLATION_HEADERS="$(mktemp)"
INSTALLATION_RESPONSE=""
if ! INSTALLATION_RESPONSE="$(curl --fail-with-body --request POST \
--dump-header "${INSTALLATION_HEADERS}" \
--header "Authorization: Bearer ${XY_API_KEY}" \
--header "Idempotency-Key: quickstart-kb-${FIRST_RESULT_ATTEMPT_ID}" \
--header "Content-Type: application/json" \
"${XY_API_BASE}/agents/knowledge-base-answering/installations" \
--data '{
"name": "Developer quickstart",
"description": "A disposable knowledge base for the first API result.",
"system_prompt": "Answer only from indexed sources and identify the supporting source.",
"initial_sources": [{
"type": "url",
"url": "https://docs.example.com/",
"crawl_limit": 10
}]
}')"; then
rm -f "${INSTALLATION_HEADERS}"
printf '%s\n' "${INSTALLATION_RESPONSE}" >&2
exit 1
fi
printf '%s\n' "${INSTALLATION_RESPONSE}" | jq
if ! INSTALLATION_ID="$(printf '%s' "${INSTALLATION_RESPONSE}" | jq -er '.id | select(type == "string" and length > 0)')"; then
rm -f "${INSTALLATION_HEADERS}"
printf 'Installation response did not include a usable id.\n' >&2
exit 1
fi
# Both ready and degraded can answer from successfully indexed sources.
# Stop immediately on failed; never poll forever.
POLL_SECONDS="$(retry_after_seconds "${INSTALLATION_HEADERS}" 5)"
DEADLINE=$(( $(date +%s) + 300 ))
while :; do
REMAINING=$(( DEADLINE - $(date +%s) ))
if ((REMAINING <= 0)); then
rm -f "${INSTALLATION_HEADERS}"
printf 'Installation did not become ready within 5 minutes.\n' >&2
exit 1
fi
WAIT_SECONDS="${POLL_SECONDS}"
if ((WAIT_SECONDS > REMAINING)); then WAIT_SECONDS="${REMAINING}"; fi
sleep "${WAIT_SECONDS}"
INSTALLATION_RESPONSE=""
if ! INSTALLATION_RESPONSE="$(curl --fail-with-body \
--dump-header "${INSTALLATION_HEADERS}" \
--header "Authorization: Bearer ${XY_API_KEY}" \
"${XY_API_BASE}/installations/${INSTALLATION_ID}")"; then
rm -f "${INSTALLATION_HEADERS}"
printf '%s\n' "${INSTALLATION_RESPONSE}" >&2
exit 1
fi
if ! INSTALLATION_STATUS="$(printf '%s' "${INSTALLATION_RESPONSE}" | jq -er '.status | select(type == "string")')"; then
rm -f "${INSTALLATION_HEADERS}"
printf 'Installation response did not include a usable status.\n' >&2
exit 1
fi
POLL_SECONDS="$(retry_after_seconds "${INSTALLATION_HEADERS}" 5)"
case "${INSTALLATION_STATUS}" in
ready|degraded)
printf '%s\n' "${INSTALLATION_RESPONSE}" | jq
break
;;
failed)
rm -f "${INSTALLATION_HEADERS}"
printf '%s\n' "${INSTALLATION_RESPONSE}" | jq >&2
exit 1
;;
esac
done
rm -f "${INSTALLATION_HEADERS}"# Reuse the attempt ID only when retrying this identical question payload.
# Choose a new attempt ID after changing the question or other request fields.
: "${FIRST_RESULT_ATTEMPT_ID:?Set the stable ID for this question payload}"
: "${INSTALLATION_ID:?Set the ready or degraded installation ID}"
retry_after_seconds() {
local header_file="$1" fallback="$2" value
value="$(awk 'tolower($1) == "retry-after:" {gsub("\r", "", $2); print $2; exit}' "${header_file}")"
case "${value}" in
""|*[!0-9]*) value="${fallback}" ;;
esac
printf '%s\n' "${value}"
}
EXECUTION_HEADERS="$(mktemp)"
EXECUTION_RESPONSE=""
if ! EXECUTION_RESPONSE="$(curl --fail-with-body --request POST \
--dump-header "${EXECUTION_HEADERS}" \
--header "Authorization: Bearer ${XY_API_KEY}" \
--header "Idempotency-Key: quickstart-question-${FIRST_RESULT_ATTEMPT_ID}" \
--header "Content-Type: application/json" \
"${XY_API_BASE}/installations/${INSTALLATION_ID}/executions" \
--data '{
"input": {
"question": "What does this documentation say the product is for?",
"max_chunks": 5
},
"metadata": {"partner_reference": "developer-quickstart"}
}')"; then
rm -f "${EXECUTION_HEADERS}"
printf '%s\n' "${EXECUTION_RESPONSE}" >&2
exit 1
fi
printf '%s\n' "${EXECUTION_RESPONSE}" | jq
if ! EXECUTION_ID="$(printf '%s' "${EXECUTION_RESPONSE}" | jq -er '.id | select(type == "string" and length > 0)')"; then
rm -f "${EXECUTION_HEADERS}"
printf 'Execution response did not include a usable id.\n' >&2
exit 1
fi
# Poll until succeeded, then inspect result.outcome, result.answer, and result.sources.
POLL_SECONDS="$(retry_after_seconds "${EXECUTION_HEADERS}" 5)"
DEADLINE=$(( $(date +%s) + 300 ))
while :; do
REMAINING=$(( DEADLINE - $(date +%s) ))
if ((REMAINING <= 0)); then
rm -f "${EXECUTION_HEADERS}"
printf 'Execution did not finish within 5 minutes.\n' >&2
exit 1
fi
WAIT_SECONDS="${POLL_SECONDS}"
if ((WAIT_SECONDS > REMAINING)); then WAIT_SECONDS="${REMAINING}"; fi
sleep "${WAIT_SECONDS}"
EXECUTION_RESPONSE=""
if ! EXECUTION_RESPONSE="$(curl --fail-with-body \
--dump-header "${EXECUTION_HEADERS}" \
--header "Authorization: Bearer ${XY_API_KEY}" \
"${XY_API_BASE}/executions/${EXECUTION_ID}")"; then
rm -f "${EXECUTION_HEADERS}"
printf '%s\n' "${EXECUTION_RESPONSE}" >&2
exit 1
fi
if ! EXECUTION_STATUS="$(printf '%s' "${EXECUTION_RESPONSE}" | jq -er '.status | select(type == "string")')"; then
rm -f "${EXECUTION_HEADERS}"
printf 'Execution response did not include a usable status.\n' >&2
exit 1
fi
POLL_SECONDS="$(retry_after_seconds "${EXECUTION_HEADERS}" 5)"
case "${EXECUTION_STATUS}" in
succeeded)
printf '%s\n' "${EXECUTION_RESPONSE}" | jq
break
;;
failed|cancelled)
rm -f "${EXECUTION_HEADERS}"
printf '%s\n' "${EXECUTION_RESPONSE}" | jq >&2
exit 1
;;
esac
done
rm -f "${EXECUTION_HEADERS}"Build or request
curl --fail-with-body --request POST \
--header "Authorization: Bearer ${XY_API_KEY}" \
--header "Idempotency-Key: daily-portal-reconciliation-v1" \
--header "Content-Type: application/json" \
"${XY_API_BASE}/agent-requests" \
--data '{
"name": "Daily portal reconciliation",
"systems": ["Source portal", "Destination system"]
}'# POST returns 202 Accepted with the hosted form in the JSON body:
# {
# "id": "areq_…",
# "state": "draft",
# "form_url": "https://app.xy.ai/workflow-requests/…"
# }
# It also returns Location for polling and Link: <form_url>; rel="form".
# Send form_url to the partner organization member who will define the agent.
# After sign-in, that person completes and submits the hosted form.
# Poll the request resource. Submission moves state from draft to submitted.
curl --fail-with-body \
--header "Authorization: Bearer ${XY_API_KEY}" \
"${XY_API_BASE}/agent-requests/${REQUEST_ID}"
# When state is needs_information, answer the XY message.
curl --fail-with-body --request POST \
--header "Authorization: Bearer ${XY_API_KEY}" \
--header "Idempotency-Key: ${REQUEST_ID}-clarification-1" \
--header "Content-Type: application/json" \
"${XY_API_BASE}/agent-requests/${REQUEST_ID}/messages" \
--data '{"message":"The portal account is customer-owned and uses TOTP."}'| Request state | What the partner does |
|---|---|
draft | Open form_url, sign in as a member of the API key's organization, complete the specification, and submit it. |
submitted / in_progress | Poll the request URL; XY is triaging, building, or reviewing. |
needs_information | Read the XY message and reply through the messages route; the reply returns the request to in_progress. |
ready_for_review | Delivery is built, but publication and organization entitlement are still separate XY review actions. |
available | Read agent.id and agent.version, then use the normal catalog, installation, and execution APIs. |
rejected / failed | Read decision and messages. These states are terminal. |
Operating contract
| Signal | Client behavior |
|---|---|
X-Request-ID | Record this opaque response header and include it when contacting XY support. Never substitute a business or organization identifier. |
Idempotency-Key | Reuse the same key only for a retry of the same logical request and identical payload. A different payload with the same key returns a conflict. |
Retry-After | Wait at least the returned number of seconds after rate limits or when polling an accepted asynchronous resource. |
ETag / If-Match | For Planner agent promotion and Integration Factory mutations, copy the latest quoted ETag into If-Match. Planner step actions instead carry expected_state_version in the body. Refresh after a conflict. |
Event cursor | Save each cursor with its exact stream/run identity after applying ordered events. Reconnect with the documented after parameter or Last-Event-ID; discard duplicates. Snapshot, replay, then tail rebuilds progress without starting another run. MCP notifications are optional; bounded polling remains available. |
error.code | Branch on the stable code and HTTP status. Show error.message to an operator; validation responses may also include structured details. |