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.
Service accounts and keys
Section titled “Service accounts and keys”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.
| Setting | What it defines |
|---|---|
| Name | To recognize the integration (e.g. “ERP — invoice upload”). |
| Roles | The tenant’s roles, including custom ones. Owner and Admin cannot be assigned. |
| Allowed environments | The only environments where the key can read or operate. |
| Expiration | Optional. After that date the account stops working on its own. |
| Allowed IPs | Optional. IP addresses or CIDR ranges, IPv4 or IPv6 (e.g. 203.0.113.10 or 2001:db8::/48). Any other IP gets 403. |
The key is shown only once
Section titled “The key is shown only once”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.
Rotate and revoke without downtime
Section titled “Rotate and revoke without downtime”- 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.
Authentication
Section titled “Authentication”Every request carries the key in the Authorization header:
GET /v1/environments HTTP/1.1Host: api.cloud.zoansoftware.comAuthorization: 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 responds400 API_KEY_IN_URLand 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.
What a key cannot do
Section titled “What a key cannot do”An API key is for integrating, not for administering. By design:
- It only works on
/v1. Any other route responds403 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.
Rate limits
Section titled “Rate limits”The API is available on every plan, Community included. Each plan sets how much you can use it:
| Plan | Requests per minute | Service accounts | Outgoing webhooks |
|---|---|---|---|
| Community | 60 | 1 | Not included |
| Core | 300 | 5 | 3 |
| Professional | 1,200 | 20 | 20 |
| Enterprise | Unlimited (or as agreed in your contract) | Unlimited | Unlimited |
- 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-RemainingandX-RateLimit-Reset(Unix seconds, UTC). Past the quota, the API responds429 RATE_LIMITEDwithRetry-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_LIMITEDwith theRetry-Afterheader (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.
Pagination and filters
Section titled “Pagination and filters”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>. Whennext_cursorisnull, there is nothing more. - Order: newest to oldest. An execution’s logs and a case’s events come in chronological order.
from(inclusive) andto(exclusive): ISO 8601 date or time (2026-10-01or2026-10-01T05:00:00Z). Withtoexclusive, 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.
Errors
Section titled “Errors”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.
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 401 | API_KEY_INVALID | The key is missing or invalid. | Check the Authorization: Bearer zk_… header. |
| 401 | API_KEY_REVOKED | The key was revoked (or its rotation grace ended). | Use the account’s current key. |
| 401 | API_KEY_EXPIRED | The key expired. | Create or rotate a key. |
| 401 | SERVICE_ACCOUNT_DISABLED | The service account is disabled. | Ask an administrator to enable it. |
| 401 | SERVICE_ACCOUNT_EXPIRED | The service account expired. | Extend or remove the expiration in the portal. |
| 403 | API_KEY_IP_NOT_ALLOWED | The request came from an IP outside the allowed ones. | Add the IP or range to the account. |
| 403 | API_KEY_NOT_ALLOWED | Route outside /v1: not available with a key. | Use only the routes in this reference. |
| 400 | API_KEY_IN_URL | The key travelled in the URL. | Send it in the header and rotate the key. |
| 403 | FORBIDDEN (or a permission code) | The account’s roles lack the route’s permission. | Add the permission to the integration’s role. |
| 404 | NOT_FOUND | It does not exist or is in an environment the key does not have. | Check the id and the allowed environments. |
| 400 | INVALID_CURSOR / INVALID_QUERY | Invalid cursor or parameters. | Repeat the original query with the next_cursor you received. |
| 422 | VALIDATION_ERROR | The body does not match the schema. | See errors for the field and the reason. |
| 409 | CONFLICT | The resource’s state does not allow the operation (e.g. the task was already decided). | Read the resource again before retrying. |
| 429 | RATE_LIMITED | The request limit was reached. | Wait as long as Retry-After says. |
Endpoint reference
Section titled “Endpoint reference”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.
Identity
Section titled “Identity”| Method | Route | Permission | Description |
|---|---|---|---|
| GET | /me | — | Service account, roles and effective permissions of the key. |
| GET | /environments | — | Active environments the key can access. |
| Method | Route | Permission | Description |
|---|---|---|---|
| GET | /environments/{envId}/flows | orchestration:list | Flows in the environment. |
| GET | /environments/{envId}/cases | orchestration:list | Flow 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:read | A case’s detail with its steps. Variables and step data only with orchestration:read_data; sensitive data arrives masked. |
| GET | /environments/{envId}/cases/{caseId}/events | orchestration:read | A case’s events, in chronological order. |
| POST | /environments/{envId}/cases/{caseId}/signals | orchestration: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. |
Executions
Section titled “Executions”| Method | Route | Permission | Description |
|---|---|---|---|
| GET | /environments/{envId}/executions | execution:list | Robot executions; filters status (queued, running, paused, completed, failed, cancelled, stopped), process_id, from/to by creation date. |
| GET | /environments/{envId}/executions/{executionId} | execution:read | Detail with inputs and outputs; sensitive inputs arrive masked. |
| GET | /environments/{envId}/executions/{executionId}/logs | execution:logs | The execution’s log in chronological order; filter level (trace, debug, info, warn, warning, error, fatal). |
Queues
Section titled “Queues”| Method | Route | Permission | Description |
|---|---|---|---|
| GET | /environments/{envId}/queues | queue:list | Queues in the environment. |
| GET | /environments/{envId}/queues/{queueId}/items | queue-item:list | Queue 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}/items | queue-item:create | Adds 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). |
Human tasks
Section titled “Human tasks”| Method | Route | Permission | Description |
|---|---|---|---|
| GET | /environments/{envId}/human-tasks | human_task:list | Tasks 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}/decision | human_task:complete | Decides 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. |
Documents (IDP)
Section titled “Documents (IDP)”| Method | Route | Permission | Description |
|---|---|---|---|
| GET | /environments/{envId}/documents | document:list | Documents processed with IDP; filters status (pending, extracted, in_review, validated), project_id, reference, from/to. |
| GET | /environments/{envId}/documents/{documentId} | document:read | Document detail with its fields and each one’s confidence. |
| GET | /environments/{envId}/documents/{documentId}/values | document:read | Validated values. While the document is not validated, values is null. |
AI usage
Section titled “AI usage”| Method | Route | Permission | Description |
|---|---|---|---|
| GET | /environments/{envId}/ai-usage | audit:read | Totals 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. |
| Method | Route | Permission | Description |
|---|---|---|---|
| GET | /audit-events | audit:read | The 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. |
Outgoing webhooks
Section titled “Outgoing webhooks”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.
| Method | Route | Permission | Description |
|---|---|---|---|
| GET | /environments/{envId}/webhooks | webhook:list | The environment’s webhooks (without the secret: only its last 4 characters). |
| POST | /environments/{envId}/webhooks | webhook:manage and the permissions for each event | Creates 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:list | One webhook. |
| PATCH | /environments/{envId}/webhooks/{webhookId} | webhook:manage | Changes name, url, events or enabled (re-enabling resets the failure count). |
| DELETE | /environments/{envId}/webhooks/{webhookId} | webhook:manage | Deletes the webhook and its log. |
| POST | /environments/{envId}/webhooks/{webhookId}/rotate-secret | webhook:manage | New secret (shown once); the previous one keeps signing for 24 hours. |
| POST | /environments/{envId}/webhooks/{webhookId}/test | webhook:manage | Sends a webhook.ping test notice. |
| GET | /environments/{envId}/webhooks/{webhookId}/deliveries | webhook:list | Deliveries from the last 30 days; status filter (pending, delivering, succeeded, failed, skipped). |
| POST | /environments/{envId}/webhooks/{webhookId}/deliveries/{deliveryId}/retry | webhook:manage | Resends a failed or skipped delivery. |
Every field is detailed in the OpenAPI specification; the notice bodies, in its webhooks section.
Examples
Section titled “Examples”The examples use fictitious data. Keep the key in an environment variable so you do not type it on the command line:
export ZOAN_API_KEY="zk_0123456789ab_EJEMPLOxxxxxxxxxxxxxxxxxxxxxxxx"export ZOAN_API="https://api.cloud.zoansoftware.com/v1"List environments
Section titled “List environments”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" } ]}Cases with an incident in October
Section titled “Cases with an incident in October”ENV=7c1e4a52-3b9d-4f0e-9a61-2d8f5b0c9e11curl -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"}Walk through every page
Section titled “Walk through every page”Repeat the same query with next_cursor until it comes back null (requires jq):
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" ] && breakdoneAdd an item to a queue
Section titled “Add an item to a queue”With reference, retrying the same request is safe: if the item already exists, it is not duplicated.
QUEUE=3e9a7c51-6d2b-4f84-b0a3-8c1e5d7f9b26curl -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}Decide a human task
Section titled “Decide a human task”First list the pending ones to learn their decision_options and form_fields:
curl -s "$ZOAN_API/environments/$ENV/human-tasks?status=pending" \ -H "Authorization: Bearer $ZOAN_API_KEY"Then send the decision:
TASK=9d4b6e20-1f7a-4c58-8b39-e5a0c2d7f164curl -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.
Send a signal to a case
Section titled “Send a signal to a case”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:
CASE=3b7f2c10-8e4d-4a91-b6c5-0d2e9f7a1c38curl -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}| Field | What it is |
|---|---|
name | Required. The name of the signal the step is waiting for (1 to 100 characters). |
data | Optional. An object with the signal’s data; the step stores the fields it is set up for in variables, with their type checked. |
idempotency_key | Optional (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.
| HTTP | Code | Meaning |
|---|---|---|
| 404 | NOT_FOUND | The case does not exist or is in an environment the key does not have. |
| 409 | SIGNAL_NOT_EXPECTED | The case is not waiting for that signal. |
| 409 | CASE_ENDED | The case has already ended. |
| 409 | IDEMPOTENCY_CONFLICT | The same idempotency key was used with another name or other data. |
| 409 | INSTANCE_BUSY | The case is busy right now; retry in a few seconds. |
| 422 | SIGNAL_DATA_INVALID | The 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.
Read a document’s validated values
Section titled “Read a document’s validated values”DOC=4f2c8a91-7b3e-4d60-a5c2-1e9b7d0f3a85curl -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 }}AI usage this month
Section titled “AI usage this month”Without from and to, it returns the current month (UTC):
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 } ]}Stability of v1
Section titled “Stability of v1”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.
Next steps
Section titled “Next steps”- 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.