Skip to content

Zoan Cloud API

The Zoan Cloud API v1 lets you connect your own systems with Zoan Automation over HTTP and JSON, with the same permissions you already manage in the portal and without touching the database:

  • Integrate your ERP or CRM: your system adds work to a queue when an invoice or an order is created, and later checks how it ended.
  • Feed BI and reports: bring flow cases, executions, documents processed with IDP and AI usage into Power BI, Looker or a spreadsheet, filtered by date and status.
  • Operate from another system: decide human tasks from your internal application or review an execution’s log without opening the portal.
  • Find out without asking: with outgoing webhooks Zoan Cloud notifies your system when an execution or a case finishes, when a task is created or decided and when a document is ready to validate.

The API is stable: in v1 fields and routes are only added, never removed or retyped (see Stability).

Download the OpenAPI 3.1 specification (YAML) · Download as JSON

Import it into Postman or Insomnia to get every route ready to try, or use it to generate a typed client in your language. The same specification is served at GET https://api.cloud.zoansoftware.com/v1/openapi.json. Descriptions in the specification are in Spanish.

An integration does not use a person’s user: it uses a service account, a machine identity with its own roles and its own key. If someone leaves the company or changes their password, the integration keeps working, and each connected system is identified separately in the audit log.

Accounts are created in the portal under Administration › API keys. This requires the api_key:manage permission, which the Owner and Admin roles have by default.

SettingWhat it defines
NameTo recognize the integration (e.g. “ERP — invoice upload”).
RolesThe tenant’s roles, including custom ones. Owner and Admin cannot be assigned.
Allowed environmentsThe only environments where the key can read or operate.
ExpirationOptional. After that date the account stops working on its own.
Allowed IPsOptional. IP addresses or CIDR ranges, IPv4 or IPv6 (e.g. 203.0.113.10 or 2001:db8::/48). Any other IP gets 403.

When you create a key, the portal shows it in full only once (it starts with zk_). Copy it right then into your secrets manager: Zoan Cloud does not store it in clear text, and afterwards only its last 4 characters are visible so you can recognize it.

The account’s list shows, per key, when it was created, whether it expires and its last use with the IP it came from, so you can spot forgotten keys or unexpected use.

  • Each account allows at most 2 active keys, exactly what you need to switch keys without interrupting the integration.
  • Rotate creates a new key and retires the previous one. You can give a grace period of 0 to 48 hours during which both work: you deploy the new key to your system calmly and the old one turns off by itself when the period ends.
  • Revoke turns a key off immediately. Use it if a key leaked.

Every request carries the key in the Authorization header:

GET /v1/environments HTTP/1.1
Host: api.cloud.zoansoftware.com
Authorization: Bearer zk_0123456789ab_EJEMPLOxxxxxxxxxxxxxxxxxxxxxxxx
  • Base URL: https://api.cloud.zoansoftware.com/v1
  • Never put the key in the URL (?api_key=…, ?token=…): URLs end up in histories, proxies and logs. The API responds 400 API_KEY_IN_URL and you must rotate that key, because it must be considered exposed.
  • No two-factor authentication: it is a machine identity, so people’s two-factor does not apply. Allowed IPs and expiration play that role.
  • The key acts with its roles’ permissions in its environments: it sees exactly what a person with those roles would see, including masking of sensitive data (sensitive form fields and inputs arrive masked just as in the portal).

To check what a key can do, call GET /me: it returns the service account, its roles and its effective permissions.

An API key is for integrating, not for administering. By design:

  • It only works on /v1. Any other route responds 403 API_KEY_NOT_ALLOWED.
  • It does not sign in to the portal or the Designer.
  • It does not manage users, roles, API keys, SSO, billing or organization settings, even if its role has those permissions.
  • It does not enter environments it is not assigned to: for the key, those resources do not exist (404).
  • It cannot have the Owner or Admin role.

The API is available on every plan, Community included. Each plan sets how much you can use it:

PlanRequests per minuteService accountsOutgoing webhooks
Community601Not included
Core30053
Professional1,2002020
EnterpriseUnlimited (or as agreed in your contract)UnlimitedUnlimited
  • Requests per minute apply to the whole organization: all your service accounts share the same quota. Every response to a key request carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds, UTC). Past the quota, the API responds 429 RATE_LIMITED with Retry-After: wait that long before retrying.
  • Service accounts and outgoing webhooks count across the organization. At the cap you cannot create more (403 PLAN_LIMIT_REACHED, the message names the plan that raises it); what you already have keeps working after a downgrade.

On top of the plan quota:

  • 3000 requests per key every 15 minutes. Beyond that, the API responds 429 RATE_LIMITED with the Retry-After header (seconds to wait).
  • The RateLimit-* headers on every response show the quota and what remains, so your integration can pace itself before reaching the cap.
  • There is also a cap on rejected keys per network: 100 attempts with an invalid key every 15 minutes from the same network. If a misconfigured integration keeps sending an old key, the network is paused for a while; fix the key before retrying.

Lists return a page and a cursor:

{ "data": [ ... ], "next_cursor": "eyJ0IjoiMjAyNi0xMC0wNV..." }
  • limit: items per page, from 1 to 100 (default 50).
  • For the next page, repeat the same query with cursor=<next_cursor>. When next_cursor is null, there is nothing more.
  • Order: newest to oldest. An execution’s logs and a case’s events come in chronological order.
  • from (inclusive) and to (exclusive): ISO 8601 date or time (2026-10-01 or 2026-10-01T05:00:00Z). With to exclusive, months chain without overlapping: from=2026-10-01&to=2026-11-01.
  • status: one or more, comma-separated (status=running,incident).

An invalid cursor, or one from another query, responds 400 INVALID_CURSOR; a malformed filter, 400 INVALID_QUERY.

Every error response has the same shape:

{ "error": { "code": "FORBIDDEN", "message": "No tienes permiso para esta acción." } }

Validation errors add errors with the field and the reason; rate-limit errors add retryAfter. Messages are in Spanish; rely on code.

HTTPCodeMeaningWhat to do
401API_KEY_INVALIDThe key is missing or invalid.Check the Authorization: Bearer zk_… header.
401API_KEY_REVOKEDThe key was revoked (or its rotation grace ended).Use the account’s current key.
401API_KEY_EXPIREDThe key expired.Create or rotate a key.
401SERVICE_ACCOUNT_DISABLEDThe service account is disabled.Ask an administrator to enable it.
401SERVICE_ACCOUNT_EXPIREDThe service account expired.Extend or remove the expiration in the portal.
403API_KEY_IP_NOT_ALLOWEDThe request came from an IP outside the allowed ones.Add the IP or range to the account.
403API_KEY_NOT_ALLOWEDRoute outside /v1: not available with a key.Use only the routes in this reference.
400API_KEY_IN_URLThe key travelled in the URL.Send it in the header and rotate the key.
403FORBIDDEN (or a permission code)The account’s roles lack the route’s permission.Add the permission to the integration’s role.
404NOT_FOUNDIt does not exist or is in an environment the key does not have.Check the id and the allowed environments.
400INVALID_CURSOR / INVALID_QUERYInvalid cursor or parameters.Repeat the original query with the next_cursor you received.
422VALIDATION_ERRORThe body does not match the schema.See errors for the field and the reason.
409CONFLICTThe resource’s state does not allow the operation (e.g. the task was already decided).Read the resource again before retrying.
429RATE_LIMITEDThe request limit was reached.Wait as long as Retry-After says.

All routes are relative to https://api.cloud.zoansoftware.com/v1. {envId} is an environment id (see GET /environments). The Permission column is the one some role of the account must have.

MethodRoutePermissionDescription
GET/me—Service account, roles and effective permissions of the key.
GET/environments—Active environments the key can access.
MethodRoutePermissionDescription
GET/environments/{envId}/flowsorchestration:listFlows in the environment.
GET/environments/{envId}/casesorchestration:listFlow cases; filters flow_id, status (running, waiting, incident, completed, cancelled), from/to by start date. Without orchestration:read_data, title and business_key come as null.
GET/environments/{envId}/cases/{caseId}orchestration:readA case’s detail with its steps. Variables and step data only with orchestration:read_data; sensitive data arrives masked.
GET/environments/{envId}/cases/{caseId}/eventsorchestration:readA case’s events, in chronological order.
POST/environments/{envId}/cases/{caseId}/signalsorchestration:signal (or orchestration:operate)Sends a signal to a case waiting for it in an Esperar evento (wait for event) step. name is the signal, data its data (optional) and idempotency_key (or the Idempotency-Key header) keeps it from being applied twice. See Send a signal to a case.
MethodRoutePermissionDescription
GET/environments/{envId}/executionsexecution:listRobot executions; filters status (queued, running, paused, completed, failed, cancelled, stopped), process_id, from/to by creation date.
GET/environments/{envId}/executions/{executionId}execution:readDetail with inputs and outputs; sensitive inputs arrive masked.
GET/environments/{envId}/executions/{executionId}/logsexecution:logsThe execution’s log in chronological order; filter level (trace, debug, info, warn, warning, error, fatal).
MethodRoutePermissionDescription
GET/environments/{envId}/queuesqueue:listQueues in the environment.
GET/environments/{envId}/queues/{queueId}/itemsqueue-item:listQueue items; filters status (new, review, waiting_decision, in_progress, successful, failed, abandoned, retried, deleted), reference, from/to. Without status, deleted items are excluded.
POST/environments/{envId}/queues/{queueId}/itemsqueue-item:createAdds an item (reference, priority, specific_content, defer_until). With reference it is idempotent: if an item with that reference already exists in the queue, that one is returned (200, deduplicated: true) instead of creating another (201).
MethodRoutePermissionDescription
GET/environments/{envId}/human-taskshuman_task:listTasks directed to one of the key’s roles (or all with human_task:view_all); filters status (pending, claimed, completed, cancelled, expired), from/to. Form data and answers are not included.
POST/environments/{envId}/human-tasks/{taskId}/decisionhuman_task:completeDecides an open task directed to one of the key’s roles (or any with human_task:manage). decision is one of decision_options; fields carries the form fields and comment is optional.
MethodRoutePermissionDescription
GET/environments/{envId}/documentsdocument:listDocuments processed with IDP; filters status (pending, extracted, in_review, validated), project_id, reference, from/to.
GET/environments/{envId}/documents/{documentId}document:readDocument detail with its fields and each one’s confidence.
GET/environments/{envId}/documents/{documentId}/valuesdocument:readValidated values. While the document is not validated, values is null.
MethodRoutePermissionDescription
GET/environments/{envId}/ai-usageaudit:readTotals and breakdowns by day, source and model (calls, tokens and cost in USD, separating what is included in the plan from what is charged to your own provider account). Defaults to the current month (UTC); maximum range 366 days. Same data as the AI Usage panel.
MethodRoutePermissionDescription
GET/audit-eventsaudit:readThe organization’s audit log events, newest first, with from, to, event_type, entity_type and cursor. Whoever does not administer the organization sees organization-wide events and those of their environments. To pull the log continuously into your system, query with from set to the date of the last event you already have. Same log as Governance › Audit.

Environment subscriptions to events that Zoan Cloud delivers with a signed JSON POST. How they work, the events and how to verify the signature: Outgoing webhooks.

MethodRoutePermissionDescription
GET/environments/{envId}/webhookswebhook:listThe environment’s webhooks (without the secret: only its last 4 characters).
POST/environments/{envId}/webhookswebhook:manage and the permissions for each eventCreates a webhook (name, public https url, events). Returns the signing secret only once. Up to 20 per environment and, across the organization, as many as the plan allows (limits).
GET/environments/{envId}/webhooks/{webhookId}webhook:listOne webhook.
PATCH/environments/{envId}/webhooks/{webhookId}webhook:manageChanges name, url, events or enabled (re-enabling resets the failure count).
DELETE/environments/{envId}/webhooks/{webhookId}webhook:manageDeletes the webhook and its log.
POST/environments/{envId}/webhooks/{webhookId}/rotate-secretwebhook:manageNew secret (shown once); the previous one keeps signing for 24 hours.
POST/environments/{envId}/webhooks/{webhookId}/testwebhook:manageSends a webhook.ping test notice.
GET/environments/{envId}/webhooks/{webhookId}/deliverieswebhook:listDeliveries from the last 30 days; status filter (pending, delivering, succeeded, failed, skipped).
POST/environments/{envId}/webhooks/{webhookId}/deliveries/{deliveryId}/retrywebhook:manageResends a failed or skipped delivery.

Every field is detailed in the OpenAPI specification; the notice bodies, in its webhooks section.

The examples use fictitious data. Keep the key in an environment variable so you do not type it on the command line:

Ventana de terminal
export ZOAN_API_KEY="zk_0123456789ab_EJEMPLOxxxxxxxxxxxxxxxxxxxxxxxx"
export ZOAN_API="https://api.cloud.zoansoftware.com/v1"
Ventana de terminal
curl -s "$ZOAN_API/environments" \
-H "Authorization: Bearer $ZOAN_API_KEY"
{
"data": [
{ "id": "7c1e4a52-3b9d-4f0e-9a61-2d8f5b0c9e11", "name": "Production", "code": "prod" },
{ "id": "0f6b2d84-91ae-4c37-b5d2-6e3a8c1f7d20", "name": "Testing", "code": "qa" }
]
}
Ventana de terminal
ENV=7c1e4a52-3b9d-4f0e-9a61-2d8f5b0c9e11
curl -s "$ZOAN_API/environments/$ENV/cases?status=incident&from=2026-10-01&to=2026-11-01&limit=100" \
-H "Authorization: Bearer $ZOAN_API_KEY"
{
"data": [
{
"id": "a3d9f1c0-5e27-4b8a-9c44-71f2e6b0d583",
"flow_id": "5b8e0d17-2c6f-4a93-8e1b-c4a7f9d2e604",
"flow_name": "Invoice posting",
"status": "incident",
"title": "Invoice INV-10482",
"business_key": "INV-10482",
"start_source": { "kind": "queue", "queue_item_id": "e2c47a19-8f3b-4d06-a5e1-9b0d6c3f2a78" },
"step_count": 4,
"ended_reason": null,
"started_at": "2026-10-03T14:12:08Z",
"ended_at": null,
"last_activity_at": "2026-10-03T14:13:51Z"
}
],
"next_cursor": "eyJ0IjoiMjAyNi0xMC0wM1QxNDoxMjowOFoiLCJpZCI6ImEzZDlmMWMwIn0"
}

Repeat the same query with next_cursor until it comes back null (requires jq):

Ventana de terminal
URL="$ZOAN_API/environments/$ENV/executions?from=2026-10-01&to=2026-11-01&limit=100"
CURSOR=""
while : ; do
PAGE=$(curl -s "$URL${CURSOR:+&cursor=$CURSOR}" -H "Authorization: Bearer $ZOAN_API_KEY")
echo "$PAGE" | jq -c '.data[]' >> executions-october.jsonl
CURSOR=$(echo "$PAGE" | jq -r '.next_cursor // empty | @uri')
[ -z "$CURSOR" ] && break
done

With reference, retrying the same request is safe: if the item already exists, it is not duplicated.

Ventana de terminal
QUEUE=3e9a7c51-6d2b-4f84-b0a3-8c1e5d7f9b26
curl -s -X POST "$ZOAN_API/environments/$ENV/queues/$QUEUE/items" \
-H "Authorization: Bearer $ZOAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reference": "INV-10483",
"priority": "high",
"specific_content": { "tax_id": "900123456", "supplier": "Andean Supplies Ltd.", "amount": 1850000 }
}'

Response 201 Created (or 200 with "deduplicated": true if it already existed):

{
"item": {
"id": "c81f2b6d-4a07-49e3-9d15-6a2e0f8b3c47",
"queue_id": "3e9a7c51-6d2b-4f84-b0a3-8c1e5d7f9b26",
"reference": "INV-10483",
"priority": "high",
"status": "new",
"specific_content": { "tax_id": "900123456", "supplier": "Andean Supplies Ltd.", "amount": 1850000 },
"attempts": 0,
"created_at": "2026-10-06T15:02:44Z",
"updated_at": "2026-10-06T15:02:44Z"
},
"deduplicated": false
}

First list the pending ones to learn their decision_options and form_fields:

Ventana de terminal
curl -s "$ZOAN_API/environments/$ENV/human-tasks?status=pending" \
-H "Authorization: Bearer $ZOAN_API_KEY"

Then send the decision:

Ventana de terminal
TASK=9d4b6e20-1f7a-4c58-8b39-e5a0c2d7f164
curl -s -X POST "$ZOAN_API/environments/$ENV/human-tasks/$TASK/decision" \
-H "Authorization: Bearer $ZOAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "decision": "Approve", "fields": { "cost_center": "CC-204" }, "comment": "Approved from the ERP" }'
{
"id": "9d4b6e20-1f7a-4c58-8b39-e5a0c2d7f164",
"title": "Approve payment INV-10482",
"status": "completed",
"decision": "Approve",
"completed_at": "2026-10-06T15:10:02Z",
"completed_by_name": "ERP — approvals"
}

If the task was already decided or cancelled, the response is 409 CONFLICT.

When a case is waiting for an event in an Esperar evento (wait for event) step, your system notifies it with a signal. For example, the payment gateway confirms an order’s payment:

Ventana de terminal
CASE=3b7f2c10-8e4d-4a91-b6c5-0d2e9f7a1c38
curl -s -X POST "$ZOAN_API/environments/$ENV/cases/$CASE/signals" -H "Authorization: Bearer $ZOAN_API_KEY" -H "Content-Type: application/json" -H "Idempotency-Key: payment-PAY-88213" -d '{ "name": "payment-confirmed", "data": { "reference": "PAY-88213", "total": 1850000 } }'
{
"case_id": "3b7f2c10-8e4d-4a91-b6c5-0d2e9f7a1c38",
"signal": "payment-confirmed",
"delivered_to": [ { "step_id": "a1d5e7c2-4b90-4f38-9e61-7c2b0d8f5a14", "element_id": "wait_payment" } ],
"status": "running",
"received_at": "2026-10-19T15:04:22Z",
"duplicate": false
}
FieldWhat it is
nameRequired. The name of the signal the step is waiting for (1 to 100 characters).
dataOptional. An object with the signal’s data; the step stores the fields it is set up for in variables, with their type checked.
idempotency_keyOptional (1 to 200 characters). The Idempotency-Key header is accepted too. With the same key and the same content, the API returns the same response with "duplicate": true without applying the signal again: your system can retry safely.

In the response, delivered_to lists the steps that received the signal (several, if parallel branches are waiting for the same one) and status is the case’s status after receiving it.

HTTPCodeMeaning
404NOT_FOUNDThe case does not exist or is in an environment the key does not have.
409SIGNAL_NOT_EXPECTEDThe case is not waiting for that signal.
409CASE_ENDEDThe case has already ended.
409IDEMPOTENCY_CONFLICTThe same idempotency key was used with another name or other data.
409INSTANCE_BUSYThe case is busy right now; retry in a few seconds.
422SIGNAL_DATA_INVALIDThe data does not fit the step’s variables (a required value is missing or the type does not match). The case keeps waiting; fix the data and send it again.

Each call counts toward the key’s rate limits, like any other request.

Ventana de terminal
DOC=4f2c8a91-7b3e-4d60-a5c2-1e9b7d0f3a85
curl -s "$ZOAN_API/environments/$ENV/documents/$DOC/values" \
-H "Authorization: Bearer $ZOAN_API_KEY"
{
"document_id": "4f2c8a91-7b3e-4d60-a5c2-1e9b7d0f3a85",
"validated": true,
"validated_at": "2026-10-05T19:44:10Z",
"values": {
"invoice_number": "INV-10482",
"issuer_tax_id": "900123456",
"date": "2026-10-02",
"total": 1850000
}
}

Without from and to, it returns the current month (UTC):

Ventana de terminal
curl -s "$ZOAN_API/environments/$ENV/ai-usage" \
-H "Authorization: Bearer $ZOAN_API_KEY"
{
"environment_id": "7c1e4a52-3b9d-4f0e-9a61-2d8f5b0c9e11",
"from": "2026-10-01T00:00:00Z",
"to": "2026-11-01T00:00:00Z",
"currency": "USD",
"totals": { "calls": 1240, "input_tokens": 3815200, "output_tokens": 402950, "cost_usd": 9.87, "cost_included_usd": 9.87, "cost_own_usd": 0 },
"by_day": [
{ "date": "2026-10-01", "calls": 210, "input_tokens": 640100, "output_tokens": 68020, "cost_usd": 1.66, "cost_included_usd": 1.66, "cost_own_usd": 0 }
],
"by_source": [
{ "source": "idp", "calls": 820, "input_tokens": 2950000, "output_tokens": 301400, "cost_usd": 7.42, "cost_included_usd": 7.42, "cost_own_usd": 0 }
],
"by_model": [
{ "model": "gpt-4.1-mini", "kind": "chat", "calls": 1240, "input_tokens": 3815200, "output_tokens": 402950, "cost_usd": 9.87, "cost_included_usd": 9.87, "cost_own_usd": 0 }
]
}

You can build on v1 with confidence:

  • In v1 fields and routes are only added; none is removed or retyped.
  • Ignore fields you do not know: that way your integration keeps working when new data appears.
  • An incompatible change, if ever needed, will arrive as a new version (/v2) living alongside v1.
  • Outgoing webhooks — have Zoan Cloud notify your system when something finishes, with a verifiable signature.
  • Users and roles — create a dedicated role with the minimum for your integration.
  • Work queues — how robots process the items you add through the API.
  • Tenant settings — organization settings.