XY Logo
Developer hub

Platform guide

Connect the API pieces before writing code.

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

There are three ways to deliver the automation.

These paths share an organization-scoped API key, but their resources and ownership boundaries are different. Choose the delivery contract first; then use its supporting tools.

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.

2. Build a workflow self-service

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.

3. Ask XY to build an agent

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

Definition, installation, and run are different objects.

Keeping these identities separate prevents the most common integration mistakes. IDs are opaque and organization-scoped.
ObjectWhat it meansWhat comes next
AgentA published, versioned business capability.Create an installation with its setup schema.
InstallationOne exact agent version plus sources, configuration, and required connection state.Wait for ready/degraded, then create executions.
ExecutionOne asynchronous run of an installation.Poll status/result and handle any review items.
Planner agent buildA durable authoring conversation with tested steps and explicit decisions.Promote the accepted build to a production definition.
Workflow definitionThe promoted, reusable production workflow.Run it once on demand or create a recurring schedule.
Workflow runOne execution of a promoted workflow definition.Read safe phase projections and governed analytics.

Authentication

Create a key for a job, not for everything.

Every XY API key is bound to one organization and environment. Choose whether it authorizes as a specific human or as the organization’s managed service identity; XY always derives tenant identity from the key.
Verify a production key
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"

Organization settings → API Keys

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.

JobTypical minimum scopesAdd only when needed
Run a published agentagents:read, installations:*, executions:*review_items:*, metrics:read
Build a workflowplanner:read, planner:writeplanner:view, planner:control, planner:promote
Design an existing workflow queuequeue_ui:read, queue_ui:writequeue_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 stepbrowser:read, browser:write, planner:writebrowser:control, browser:debug
Run a promoted workflow onceexecutions:write, executions:readNo schedule scope is required.
Operate recurring productionschedules:read, schedules:writeexecutions:read, dashboards:read, usage:read
Create a missing API integrationintegrations:read, integrations:generate, integrations:promote, credentials:writeAdd integrations:connect when managing service connections outside factory validation.
Propose an analytics dashboard (release preview)dashboards:proposedashboards:read is separate analytics-query access. Human publication remains in XY.
Ask XY to build an agentagent_requests:read, agent_requests:writeNo 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

Go from a key to a useful response.

The connection check proves the key works. This flow produces an actual evidence-backed result: install the catalog Knowledge Base agent against a small public documentation site, wait for indexing, ask one question, and inspect the cited sources.
  1. Install and wait for a Knowledge Base
    # 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}"
  2. Ask and poll one question
    # 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

Build it yourself, or give XY a precise specification.

A Planner agent build creates a workflow definition isolated within your XY organization. A custom-agent request returns a secure form URL for the partner to complete and submit, starting a separate XY-owned delivery lifecycle.

Build it self-service when

  • The Planner agent can compose browser steps, pure transforms, OCR, control flow, and available integrations.
  • A missing HTTP API can be described by public or private documentation and created through the Integration Factory.
  • The required portal or API credential can be referenced through the credential API.
  • Your team can review tests, accept each step, and own the promoted definition.

Request XY to build it when

  • A bespoke webhook, inbox, typed client, executable adapter, or specialized persistence service is required.
  • The flow needs bespoke operational hardening, reconciliation, monitoring, or human-review behavior.
  • You want XY to review, publish, version, and support the result as an entitled agent.
  1. Create the hosted request form
    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"]
      }'
  2. Hand off, poll, and clarify
    # 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 stateWhat the partner does
draftOpen form_url, sign in as a member of the API key's organization, complete the specification, and submit it.
submitted / in_progressPoll the request URL; XY is triaging, building, or reviewing.
needs_informationRead the XY message and reply through the messages route; the reply returns the request to in_progress.
ready_for_reviewDelivery is built, but publication and organization entitlement are still separate XY review actions.
availableRead agent.id and agent.version, then use the normal catalog, installation, and execution APIs.
rejected / failedRead decision and messages. These states are terminal.

Operating contract

Build retries and supportability in from the start.

Treat headers and asynchronous resource state as part of the API contract. They tell your client whether to retry, poll, refresh, or ask a person to act.
SignalClient behavior
X-Request-IDRecord this opaque response header and include it when contacting XY support. Never substitute a business or organization identifier.
Idempotency-KeyReuse 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-AfterWait at least the returned number of seconds after rate limits or when polling an accepted asynchronous resource.
ETag / If-MatchFor 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 cursorSave 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.codeBranch on the stable code and HTTP status. Show error.message to an operator; validation responses may also include structured details.