MENU navbar-image

Introduction

API for the revenexx Integration Studio — connects Revenue Cloud to external systems (ERP, PIM, CRM) via Temporal workflows.

The Integrations Service API provides endpoints for managing connections to external systems,
workflow definitions, and integration events.

All endpoints require a valid Zitadel JWT token. The active tenant is taken
from the `X-Tenant-Id` request header (a `tenant_id` JWT claim is accepted as
a legacy fallback for development tokens). Tenant isolation is enforced on
every request.

**Base URL:** `/v1`
**Versioning:** URL path prefix per [ADR-0036](https://atlas.revenexx.dev/adr/adr-0036-api-versioning-strategy).

Authenticating requests

To authenticate requests, include an Authorization header with the value "Bearer {ZITADEL_JWT}".

All authenticated endpoints are marked with a requires authentication badge in the documentation below.

Authenticate via a Zitadel-issued OIDC JWT. Tenant context is taken from the X-Tenant-Id request header; the tenant_id JWT claim is accepted as a legacy fallback for development tokens.

Audit Log

Read access to the append-only audit trail (credential / secret / workflow / trigger create-edit-delete, plus workflow run start/finished/failed). Two planes: the public feed is auto-scoped to the caller's org + tenant; the admin feed (registered under v1/admin) filters all three identity fields optionally. Results are newest-first and cursor-paginated.

List audit entries (public)

requires authentication

Returns the audit entries for the caller's org + tenant, newest first. Optionally narrowed by sub (actor), resource_type and action.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/audit-logs?sub=289347298347&resource_type=workflow&action=create&limit=50&cursor=eyJpZCI6NDJ9" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/audit-logs"
);

const params = {
    "sub": "289347298347",
    "resource_type": "workflow",
    "action": "create",
    "limit": "50",
    "cursor": "eyJpZCI6NDJ9",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        {
            "id": 42,
            "org_id": "acme-org",
            "tenant_id": "acme-production",
            "actor_sub": "289347298347",
            "via_system": false,
            "channel": "api",
            "action": "create",
            "resource_type": "workflow",
            "resource_id": "1",
            "metadata": {
                "name": "Sync orders"
            },
            "created_at": "2026-06-18T10:00:00Z"
        }
    ],
    "meta": {
        "per_page": 50,
        "next_cursor": null,
        "prev_cursor": null
    }
}
 

Request      

GET api/v1/audit-logs

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Query Parameters

sub   string  optional    

optional Filter by acting user subject. Example: 289347298347

resource_type   string  optional    

optional Filter by resource type (credential, secret, workflow, trigger, workflow_run). Example: workflow

action   string  optional    

optional Filter by action (create, update, delete, run.started, run.finished, run.failed). Example: create

limit   integer  optional    

optional Page size (1–100, default 50). Example: 50

cursor   string  optional    

optional Opaque pagination cursor from a previous response. Example: eyJpZCI6NDJ9

Response

Response Fields

data   object     
actor_sub   string     

The acting user subject (sub); null for system-initiated entries (via_system = true).

metadata   object     

Action-specific detail recorded with the entry; null when none was captured.

Auth

Endpoints for verifying authentication and tenant context.

Get authenticated user info

requires authentication

Returns full user profile, resolved tenant context, and raw token claims. Useful for debugging authentication and understanding which data Zitadel provides.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/me" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/me"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "user": {
        "id": 1,
        "zitadel_id": "289347298347",
        "email": "user@acme.com",
        "name": "Jane Doe"
    },
    "context": {
        "tenant_id": "acme-production",
        "active_plane": "public",
        "roles": [
            "admin",
            "user"
        ]
    },
    "claims": {
        "iss": "https://id.revenexx.com",
        "sub": "289347298347",
        "aud": [
            "project-id"
        ]
    }
}
 

Example response (401, Missing or invalid token):


{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/me

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Credential Types

Read-only catalogue of the credential types registered via node packages. Drives the credential form (fields + validation) and the credentials-ref type picker in the editor. Globally registered, not tenant-scoped.

List credential types

requires authentication

Returns the latest registered version of each credential type.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/credential-types" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/credential-types"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        {
            "slug": "revenexx:smtp",
            "version": "1.0.0",
            "name": "SMTP",
            "description": "Send mail over SMTP",
            "icon": null,
            "images": [
                {
                    "src": "https://api.example.com/v1/node-packages/images/2b1c0000-0000-4000-8000-000000000000?expires=1750000000&signature=abcdef",
                    "alt": {
                        "en": "SMTP logo"
                    },
                    "title": {
                        "en": "SMTP"
                    },
                    "category": "logo"
                }
            ],
            "auth_kind": "secret",
            "fields": []
        }
    ]
}
 

Request      

GET api/v1/credential-types

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Show a credential type

requires authentication

Returns the latest registered version of the credential type identified by its namespaced slug.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/credential-types/revenexx:smtp" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/credential-types/revenexx:smtp"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": {
        "slug": "revenexx:smtp",
        "version": "1.0.0",
        "name": "SMTP",
        "description": "Send mail over SMTP",
        "icon": null,
        "images": [
            {
                "src": "https://api.example.com/v1/node-packages/images/2b1c0000-0000-4000-8000-000000000000?expires=1750000000&signature=abcdef",
                "alt": {
                    "en": "SMTP logo"
                },
                "title": {
                    "en": "SMTP"
                },
                "category": "logo"
            }
        ],
        "auth_kind": "secret",
        "fields": []
    }
}
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Request      

GET api/v1/credential-types/{slug}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

slug   string     

Namespaced credential type slug. Example: revenexx:smtp

Credentials

Tenant-scoped credential instances of a registered {@see CredentialType}. The encrypted config / durable_creds blobs are never returned. Runtime resolution happens in the broker, not here — this controller only stores and proxies the on-demand connection test.

OAuth redirect callback (public)

The provider redirects the user's browser here with code + state. Unauthenticated by necessity — the single-use state (looked up in the cache) is the CSRF protection and carries the tenant + PKCE verifier.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/credentials/oauth/callback" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/credentials/oauth/callback"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Connected):


{
    "status": "connected",
    "credential_id": 1
}
 

Example response (400, Missing or invalid state):


{
    "message": "Invalid or expired OAuth state."
}
 

Request      

GET api/v1/credentials/oauth/callback

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

List credentials

requires authentication

Returns the current tenant's credential instances, optionally filtered to a single credential type via ?type={slug}. Secrets are never exposed.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/credentials?type=revenexx%3Asmtp" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/credentials"
);

const params = {
    "type": "revenexx:smtp",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        {
            "id": 1,
            "tenant_id": "acme-production",
            "credential_type_slug": "revenexx:smtp",
            "name": "Production SMTP",
            "status": "active",
            "public_config": {},
            "created_at": "2026-05-06T10:00:00Z",
            "updated_at": "2026-05-06T10:00:00Z"
        }
    ]
}
 

Example response (401, Missing or invalid token):


{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/credentials

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Query Parameters

type   string  optional    

Filter by credential type slug. Example: revenexx:smtp

Create a credential

requires authentication

Validates the submitted config against the credential type's declared fields, then stores it encrypted. Returns 201 with the masked instance.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/credentials" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"credential_type_slug\": \"revenexx:smtp\",
    \"name\": \"Mailpit\",
    \"config\": {
        \"host\": \"smtp.example.com\",
        \"port\": 587,
        \"username\": \"mailer\",
        \"password\": \"sekret\"
    }
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/credentials"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "credential_type_slug": "revenexx:smtp",
    "name": "Mailpit",
    "config": {
        "host": "smtp.example.com",
        "port": 587,
        "username": "mailer",
        "password": "sekret"
    }
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201, Created):


{
    "id": 1,
    "tenant_id": "acme-production",
    "credential_type_slug": "revenexx:smtp",
    "name": "Production SMTP",
    "status": "active",
    "public_config": {},
    "created_at": "2026-05-06T10:00:00Z",
    "updated_at": "2026-05-06T10:00:00Z"
}
 

Example response (422, Validation failed):


{
    "message": "The given data was invalid.",
    "errors": {
        "config": [
            "..."
        ]
    }
}
 

Request      

POST api/v1/credentials

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Body Parameters

credential_type_slug   string     

Slug of a registered credential type the instance is created for. The slug of an existing record in the credential_types table. Example: revenexx:smtp

name   string     

Human-readable label, unique per tenant. Must not be greater than 191 characters. Example: Mailpit

config   object     

Connection data for the credential type. The accepted fields are defined by the type manifest; stored encrypted and never returned.

Test an unsaved credential

requires authentication

Runs the broker connection test against an inline type + config, before the instance is persisted. Non-blocking.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/credentials/test" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"credential_type_slug\": \"revenexx:smtp\",
    \"config\": {
        \"host\": \"smtp.example.com\",
        \"port\": 587,
        \"username\": \"mailer\",
        \"password\": \"sekret\"
    }
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/credentials/test"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "credential_type_slug": "revenexx:smtp",
    "config": {
        "host": "smtp.example.com",
        "port": 587,
        "username": "mailer",
        "password": "sekret"
    }
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200, Tested):


{
    "ok": true,
    "message": null
}
 

Example response (200, Failed):


{
    "ok": false,
    "message": "Invalid auth key"
}
 

Request      

POST api/v1/credentials/test

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Body Parameters

credential_type_slug   string     

Slug of the credential type to test the inline config against. The slug of an existing record in the credential_types table. Example: revenexx:smtp

config   object     

Inline connection data to exercise the type's connection test, before any instance is persisted.

Response

Response Fields

ok   boolean     

Whether the values authenticate.

message   string|null     

Why not, where the credential type said why — bounded to 500 characters, the same bound the stored reason on a saved credential is held to. Always present; null when there is nothing to say.

Show a credential

requires authentication

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/credentials/9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/credentials/9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "id": 1,
    "tenant_id": "acme-production",
    "credential_type_slug": "revenexx:smtp",
    "name": "Production SMTP",
    "status": "active",
    "public_config": {},
    "created_at": "2026-05-06T10:00:00Z",
    "updated_at": "2026-05-06T10:00:00Z"
}
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Request      

GET api/v1/credentials/{id}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

id   string     

The ID of the credential. Example: 9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d

Update a credential

requires authentication

Updates the name and/or config. When config is supplied it is re-validated against the credential type's declared fields.

Example request:
curl --request PATCH \
    "https://integrations.revenexx.com/api/v1/credentials/9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"name\": \"Mailpit (staging)\",
    \"config\": {
        \"host\": \"smtp.example.com\",
        \"port\": 587,
        \"username\": \"mailer\",
        \"password\": \"sekret\"
    }
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/credentials/9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "name": "Mailpit (staging)",
    "config": {
        "host": "smtp.example.com",
        "port": 587,
        "username": "mailer",
        "password": "sekret"
    }
};

fetch(url, {
    method: "PATCH",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200, Updated):


{
    "id": 1,
    "tenant_id": "acme-production",
    "credential_type_slug": "revenexx:smtp",
    "name": "Production SMTP",
    "status": "active",
    "public_config": {},
    "created_at": "2026-05-06T10:00:00Z",
    "updated_at": "2026-05-06T11:00:00Z"
}
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Example response (422, Validation failed):


{
    "message": "The given data was invalid.",
    "errors": {
        "config": [
            "..."
        ]
    }
}
 

Request      

PATCH api/v1/credentials/{id}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

id   string     

The ID of the credential. Example: 9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d

Body Parameters

name   string  optional    

New human-readable label, unique per tenant. Optional. Must not be greater than 191 characters. Example: Mailpit (staging)

config   object  optional    

Replacement connection data, re-validated against the credential type manifest. Optional; stored encrypted and never returned.

Delete a credential

requires authentication

Example request:
curl --request DELETE \
    "https://integrations.revenexx.com/api/v1/credentials/9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/credentials/9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, Deleted):

Empty response
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Request      

DELETE api/v1/credentials/{id}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

id   string     

The ID of the credential. Example: 9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d

Test a saved credential

requires authentication

Runs the credential type's connection test in the broker against the stored config and records the outcome on the instance (last_test_at, last_test_ok, last_test_message), so the answer to "do these credentials actually work?" survives a reload — status cannot answer that, it only tracks the OAuth refresh lifecycle. The stored config is never modified.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/credentials/9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d/test" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/credentials/9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d/test"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200, Tested):


{
    "ok": true,
    "message": null,
    "last_test_at": "2026-07-16T21:00:00+00:00",
    "last_test_ok": true
}
 

Request      

POST api/v1/credentials/{id}/test

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

id   string     

The ID of the credential. Example: 9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d

Begin 3-legged OAuth setup

requires authentication

Returns the provider authorize URL for the credential. The caller (the cockpit) opens it; the provider redirects back to the public callback.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/credentials/9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d/oauth/authorize-url" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/credentials/9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d/oauth/authorize-url"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200, Authorize URL):


{
    "authorize_url": "https://login.example.com/authorize?client_id=...&state=..."
}
 

Example response (422, Not a 3-legged OAuth type):


{
    "message": "Credential type does not use 3-legged OAuth."
}
 

Request      

POST api/v1/credentials/{id}/oauth/authorize-url

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

id   string     

The ID of the credential. Example: 9c1b4f2a-7d3e-4a6b-8c5d-1e2f3a4b5c6d

Node Packages

Stream a node-package image.

requires authentication

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/node-packages/images/architecto?expires=16&signature=architecto" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/node-packages/images/architecto"
);

const params = {
    "expires": "16",
    "signature": "architecto",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Valid signature):


"<binary image stream>"
 

Example response (403, Missing, tampered, or expired signature):


{
    "message": "Invalid signature."
}
 

Example response (404, Unknown image, or stored file missing):


{
    "message": "Not Found"
}
 

Request      

GET api/v1/node-packages/images/{id}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

id   string     

The ID of the image. Example: architecto

image   string     

The image UUID. Example: 9b2e6b1e-6d0a-5f3a-8c1b-2b7f6a1c9d4e

Query Parameters

expires   integer     

Signed-URL expiry (added by the signer). Example: 16

signature   string     

URL signature (added by the signer). Example: architecto

Nodes

Globally registered node definitions, addressed publicly by <slug>/<version>. Read endpoints additionally accept the alias latest in place of an explicit version. The DELETE endpoint refuses the alias and requires an exact version, so the caller always knows which row they're touching.

Nodes are created and updated exclusively through the node-package upload (POST /v1/admin/orgs/{org_id}/node-packages), which extracts the manifests from the published tarball — there is no standalone create/update endpoint, since a node always ships with its runtime code.

Authentication is required for every endpoint, but the resource itself is not tenant-scoped — per-tenant entitlement (which tenant may use which node) is the responsibility of a future app-registration flow.

List nodes

requires authentication

Returns the registered nodes ordered by slug ascending, then semver descending. Optionally filters to a single namespace via the namespace query parameter.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/nodes?namespace=revenexx" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/nodes"
);

const params = {
    "namespace": "revenexx",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        {
            "name": "HTTP request",
            "namespace": "revenexx",
            "slug": "revenexx:http-request",
            "version": "1.0.0",
            "package": {
                "name": "@revenexx/integrations-nodes-core",
                "version": "0.2.0",
                "label": "Core"
            },
            "manifest_version": "v0-draft",
            "manifest": {},
            "created_at": "2026-05-06T10:00:00Z",
            "updated_at": "2026-05-06T10:00:00Z"
        }
    ]
}
 

Example response (401, Missing or invalid token):


{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/nodes

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Query Parameters

namespace   string  optional    

Filter by the slug-derived namespace (e.g. revenexx). Example: revenexx

List versions for a node

requires authentication

Returns the registered versions for a given slug, semver-descending. The slug must match at least one stored row, otherwise a 404 is returned — there's no representation for "an empty list of versions". The response intentionally only contains version strings; callers that need the full row should follow up with GET /nodes/{slug}/{version}.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/nodes/revenexx:http-request/versions" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/nodes/revenexx:http-request/versions"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        "2.5.0",
        "1.10.0",
        "1.0.0"
    ]
}
 

Example response (404, Unknown slug):


{
    "message": "Not found."
}
 

Request      

GET api/v1/nodes/{slug}/versions

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

slug   string     

Full node slug. Example: revenexx:http-request

Show a node

requires authentication

Returns a single node by (slug, version). The version may be the literal string latest, in which case the highest semver registered for the slug is returned.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/nodes/revenexx:http-request/1.0.0" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/nodes/revenexx:http-request/1.0.0"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "name": "HTTP request",
    "namespace": "revenexx",
    "slug": "revenexx:http-request",
    "version": "1.0.0",
    "package": {
        "name": "@revenexx/integrations-nodes-core",
        "version": "0.2.0",
        "label": "Core"
    },
    "manifest_version": "v0-draft",
    "manifest": {},
    "created_at": "2026-05-06T10:00:00Z",
    "updated_at": "2026-05-06T10:00:00Z"
}
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Request      

GET api/v1/nodes/{slug}/{version}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

slug   string     

Full node slug. Example: revenexx:http-request

version   string     

Semver, or the alias latest. Example: 1.0.0

Resolve dynamic node config

requires authentication

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/nodes/revenexx:api/1.0.0/config:resolve" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"target\": \"*\",
    \"config\": {
        \"customerId\": \"42\"
    },
    \"locale\": \"de\",
    \"search\": \"acme\"
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/nodes/revenexx:api/1.0.0/config:resolve"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "target": "*",
    "config": {
        "customerId": "42"
    },
    "locale": "de",
    "search": "acme"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200, Resolved):


{
    "fields": [
        {
            "key": "customerId",
            "type": "string"
        }
    ],
    "outputs": [
        {
            "name": "success"
        }
    ]
}
 

Example response (404, Unknown node):


{
    "message": "Not found."
}
 

Example response (409, Node version has no built code):


{
    "message": "This node version has no built code, so it cannot be executed. Its code is built when its package is registered."
}
 

Example response (502, Node runtime unavailable or failed):


{
    "message": "Node runtime resolve failed: ..."
}
 

Request      

POST api/v1/nodes/{slug}/{version}/config:resolve

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

slug   string     

Full node slug. Example: revenexx:api

version   string     

Semver, or the alias latest. Example: 1.0.0

Body Parameters

target   string     

"outputs", "" (all), or a config field key. Must not be greater than 128 characters. Example: ``

config   object  optional    

The user's current partial config values.

locale   string  optional    

Preferred locale for resolved labels. Must not be greater than 16 characters. Example: de

search   string  optional    

The operator's type-to-search term for the option list (config-field targets only). Must not be greater than 200 characters. Example: acme

Test-execute a node

requires authentication

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/nodes/revenexx:api/1.0.0/execute:test" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"config\": {
        \"customerId\": \"42\"
    },
    \"inputs\": {
        \"payload\": {
            \"email\": \"test@acme.com\"
        }
    },
    \"timeout_ms\": 30000,
    \"workflow_id\": 12
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/nodes/revenexx:api/1.0.0/execute:test"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "config": {
        "customerId": "42"
    },
    "inputs": {
        "payload": {
            "email": "test@acme.com"
        }
    },
    "timeout_ms": 30000,
    "workflow_id": 12
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200, Executed):


{
    "outputs": {
        "customer": {
            "id": "42"
        }
    },
    "branch": "success",
    "logs": [
        {
            "level": "info",
            "message": "fetched customer 42"
        }
    ]
}
 

Example response (404, Unknown node):


{
    "message": "Not found."
}
 

Example response (409, Node version has no built code):


{
    "message": "This node version has no built code, so it cannot be executed. Its code is built when its package is registered."
}
 

Example response (422, Node threw):


{
    "error": {
        "message": "rate limited",
        "code": "RATE_LIMITED"
    },
    "logs": []
}
 

Example response (502, Node runtime unavailable):


{
    "message": "Node runtime execute failed: ..."
}
 

Request      

POST api/v1/nodes/{slug}/{version}/execute:test

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

slug   string     

Full node slug. Example: revenexx:api

version   string     

Semver, or the alias latest. Example: 1.0.0

Body Parameters

config   object  optional    

The node's config values to run with.

inputs   object  optional    

Simulated input payloads, merged over the config.

timeout_ms   integer  optional    

Wall-clock limit in milliseconds; clamped by the runtime. Must be at least 1000. Example: 30000

workflow_id   integer  optional    

The workflow being edited. Grants the test run read-only access to that workflow's declared state namespaces; writes stay run-only. Example: 12

Delete a node

requires authentication

Removes the node version identified by (slug, version). The latest alias is rejected to prevent ambiguous deletes.

Example request:
curl --request DELETE \
    "https://integrations.revenexx.com/api/v1/nodes/revenexx:http-request/1.0.0" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/nodes/revenexx:http-request/1.0.0"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, Deleted):

Empty response
 

Example response (400, Latest alias rejected):


{
    "message": "The `latest` alias is not allowed for mutating requests."
}
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Request      

DELETE api/v1/nodes/{slug}/{version}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

slug   string     

Full node slug. Example: revenexx:http-request

version   string     

Exact semver of the node version to delete; the alias latest is rejected. Example: 1.0.0

Run Dead Letters

A trigger firing that could not start a run — and left no failed run row to retry — is captured here so it can be replayed once the cause is resolved (typically the workflow bundle finishing its build) or explicitly discarded.

List dead-lettered firings for a workflow

requires authentication

Newest first. Only firings for the addressed workflow, scoped to the authenticated tenant.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/1/dead-letters?page=1&per_page=25" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/dead-letters"
);

const params = {
    "page": "1",
    "per_page": "25",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [],
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "total": 0,
        "last_page": 1
    }
}
 

Request      

GET api/v1/workflows/{workflowId}/dead-letters

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

Query Parameters

page   integer  optional    

Page number to retrieve (1-based). Defaults to 1. Must be at least 1. Example: 1

per_page   integer  optional    

Number of dead letters per page (1–100). Defaults to 25. Must be at least 1. Must not be greater than 100. Example: 25

List the tenant's missed firings

requires authentication

Every trigger firing across the tenant that never became a run, newest first. Optionally narrowed by handling state, by workflow and by a time window.

The sibling of GET /v1/runs: a firing that produced a run belongs there, one that produced none belongs here. Each row names the workflow it belongs to, which the per-workflow list can leave to the request path and this one cannot.

No total is reported — GET /v1/dead-letters/summary answers "how many", including the count of those still waiting.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/dead-letters?status[]=pending&workflow_id=1&since=2026-08-01T00%3A00%3A00Z&until=2026-08-21T00%3A00%3A00Z&page=1&per_page=25" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/dead-letters"
);

const params = {
    "status[0]": "pending",
    "workflow_id": "1",
    "since": "2026-08-01T00:00:00Z",
    "until": "2026-08-21T00:00:00Z",
    "page": "1",
    "per_page": "25",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (0, Success):


{
    "data": [
        {
            "id": 7,
            "workflow_id": 1,
            "workflow_name": "Sync orders",
            "trigger_id": 3,
            "source": "schedule",
            "trigger_handle": "9f1c8e2a-0b4d-4f7e-9a3c-1d2e3f4a5b6c",
            "reason": "Workflow bundle not ready (build_",
            "status": "pending",
            "created_at": "2026-08-21T02:00:00Z",
            "updated_at": "2026-08-21T02:00:00Z"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "has_more": false
    }
}
 

Example response (401, Missing or invalid token):


{
    "message": "Unauthenticated."
}
 

Example response (403, No tenant context):


{
    "message": "Tenant context is required."
}
 

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-request-id: fc60e865-6f17-444f-b9c3-6dc8b49bae51
access-control-allow-origin: *
 

{
    "message": "Server Error"
}
 

Request      

GET api/v1/dead-letters

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Query Parameters

status   string[]  optional    

Restrict to firings in these handling states. One value, or several comma-separated (pending, replayed, discarded).

Must be one of:
  • pending
  • replayed
  • discarded
workflow_id   integer  optional    

Restrict to firings of one workflow. A workflow of another tenant is rejected. Must be at least 1. Example: 1

since   string  optional    

Only firings captured at or after this instant. Must be a valid date. Example: 2026-08-01T00:00:00Z

until   string  optional    

Only firings captured at or before this instant. Must not precede since. Must be a valid date. Must be a date after or equal to since. Example: 2026-08-21T00:00:00Z

page   integer  optional    

Page number to retrieve (1-based). Defaults to 1. Must be at least 1. Example: 1

per_page   integer  optional    

Firings per page. Clamped to the configured maximum (100) rather than rejected. Example: 25

search   string  optional    

Response

Response Fields

data   object     
workflow_name   string     

The name of the workflow whose firing was missed.

reason   string     

Why the firing never became a run, in the platform's words. Empty when nothing was recorded.

meta   object     
has_more   boolean     

Whether a further page exists. This list reports no total — see GET /v1/dead-letters/summary.

Count the tenant's missed firings

requires authentication

How many firings never became runs, broken down by handling state, under exactly the narrowing GET /v1/dead-letters accepts.

by_status.pending is the count the studio shows on its Missed runs tab and on the dashboard: the firings still waiting to be sent through or put aside. Every state appears, zeros included, so "none waiting" cannot read as "not loaded yet".

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/dead-letters/summary?status[]=pending&workflow_id=1&since=2026-08-01T00%3A00%3A00Z&until=2026-08-21T00%3A00%3A00Z&page=1&per_page=25" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/dead-letters/summary"
);

const params = {
    "status[0]": "pending",
    "workflow_id": "1",
    "since": "2026-08-01T00:00:00Z",
    "until": "2026-08-21T00:00:00Z",
    "page": "1",
    "per_page": "25",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": {
        "total": 7,
        "by_status": {
            "pending": 4,
            "replayed": 2,
            "discarded": 1
        }
    }
}
 

Example response (401, Missing or invalid token):


{
    "message": "Unauthenticated."
}
 

Example response (403, No tenant context):


{
    "message": "Tenant context is required."
}
 

Request      

GET api/v1/dead-letters/summary

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Query Parameters

status   string[]  optional    

Restrict to firings in these handling states. One value, or several comma-separated (pending, replayed, discarded).

Must be one of:
  • pending
  • replayed
  • discarded
workflow_id   integer  optional    

Restrict to firings of one workflow. A workflow of another tenant is rejected. Must be at least 1. Example: 1

since   string  optional    

Only firings captured at or after this instant. Must be a valid date. Example: 2026-08-01T00:00:00Z

until   string  optional    

Only firings captured at or before this instant. Must not precede since. Must be a valid date. Must be a date after or equal to since. Example: 2026-08-21T00:00:00Z

page   integer  optional    

Page number to retrieve (1-based). Defaults to 1. Must be at least 1. Example: 1

per_page   integer  optional    

Firings per page. Clamped to the configured maximum (100) rather than rejected. Example: 25

search   string  optional    

Response

Response Fields

data   object     
total   integer     

How many missed firings match the narrowing.

by_status   object     

A count per handling state. Every state appears, including the ones at zero.

Replay a dead-lettered firing

requires authentication

Starts a fresh run from the captured trigger payload + handle. Requires the workflow to be runnable again; marks the dead letter replayed.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/dead-letters/1/replay" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/dead-letters/1/replay"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (0, Workflow not runnable):


{
    "message": "Workflow bundle is not ready (build_"
}
 

Example response (202, Accepted):


{
    "id": 5,
    "workflow_id": 1,
    "status": "running"
}
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Example response (409, Already handled):


{
    "message": "Dead letter has already been replayed or discarded."
}
 

Example response (422, Trigger no longer resolvable):


{
    "message": "Workflow has no active manual trigger; pass trigger_handle to start a specific trigger."
}
 

Example response (503, Worker unavailable):


{
    "message": "Workflow worker is unavailable."
}
 

Request      

POST api/v1/dead-letters/{id}/replay

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

id   integer     

The dead letter id. Example: 1

Discard a dead-lettered firing

requires authentication

Marks the dead letter discarded without starting a run.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/dead-letters/1/discard" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/dead-letters/1/discard"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200, Discarded):


{
    "id": 1,
    "status": "discarded"
}
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Example response (409, Already handled):


{
    "message": "Dead letter has already been replayed or discarded."
}
 

Request      

POST api/v1/dead-letters/{id}/discard

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

id   integer     

The dead letter id. Example: 1

Runs

Every run of every workflow the tenant owns, in one list the platform orders and pages itself. The studio's Runs page reads this; a workflow's own Runs tab reads the per-workflow subresource (GET /workflows/{workflowId}/runs), whose contract is unchanged.

Its own controller rather than more methods on {@see WorkflowRunController}: that class orchestrates starting, cancelling, resuming and inspecting a run and carries the services to do it. This is a read, and it needs none of them.

List the tenant's runs

requires authentication

Newest first, across every workflow the tenant owns, and optionally in another order — by when a run started, or by how long it took. Optionally narrowed by outcome, by the kind of trigger that started the run, by workflow, and by a time window; the filters narrow together, and the order applies to what they left.

The rows are leaner than the per-workflow list's: a run's result and captured trigger payload are not included, because they can run to megabytes each. Read them from GET /workflows/{workflowId}/runs/{runId}.

No total is reported. GET /v1/runs/summary answers "how many", and answers it under the same narrowing — a page cannot, and pretending otherwise is what made clients read every page to count.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/runs?status[]=failed&workflow_id=1&since=2026-08-01T00%3A00%3A00Z&until=2026-08-21T00%3A00%3A00Z&page=1&per_page=25&trigger_type=schedule&search=orders&sort=duration&direction=desc" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/runs"
);

const params = {
    "status[0]": "failed",
    "workflow_id": "1",
    "since": "2026-08-01T00:00:00Z",
    "until": "2026-08-21T00:00:00Z",
    "page": "1",
    "per_page": "25",
    "trigger_type": "schedule",
    "search": "orders",
    "sort": "duration",
    "direction": "desc",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        {
            "id": 42,
            "workflow_id": 1,
            "workflow_name": "Sync orders",
            "status": "failed",
            "error": "Node revenexx:http-request failed: 500",
            "trigger_handle": "9f1c8e2a-0b4d-4f7e-9a3c-1d2e3f4a5b6c",
            "trigger_type": "schedule",
            "temporal_workflow_id": "acme-production:1:42",
            "temporal_run_id": "018f...",
            "started_at": "2026-08-21T02:00:00Z",
            "completed_at": "2026-08-21T02:00:07Z",
            "created_at": "2026-08-21T02:00:00Z",
            "updated_at": "2026-08-21T02:00:07Z"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "has_more": false
    }
}
 

Example response (401, Missing or invalid token):


{
    "message": "Unauthenticated."
}
 

Example response (403, No tenant context):


{
    "message": "Tenant context is required."
}
 

Example response (422, Unknown status):


{
    "message": "The given data was invalid.",
    "errors": {
        "status.0": [
            "The selected status.0 is invalid."
        ]
    }
}
 

Request      

GET api/v1/runs

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Query Parameters

status   string[]  optional    

Restrict to runs in these statuses. One value, or several comma-separated (pending, running, completed, failed, cancelled, terminated).

Must be one of:
  • pending
  • running
  • completed
  • failed
  • cancelled
  • terminated
workflow_id   integer  optional    

Restrict to runs of one workflow. A workflow of another tenant is rejected. Must be at least 1. Example: 1

since   string  optional    

Only runs created at or after this instant. Must be a valid date. Example: 2026-08-01T00:00:00Z

until   string  optional    

Only runs created at or before this instant. Must not precede since. Must be a valid date. Must be a date after or equal to since. Example: 2026-08-21T00:00:00Z

page   integer  optional    

Page number to retrieve (1-based). Defaults to 1. Must be at least 1. Example: 1

per_page   integer  optional    

Runs per page. Clamped to the configured maximum (100) rather than rejected. Example: 25

trigger_type   string  optional    

Restrict to runs started by this kind of trigger (manual, schedule, webhook, event). Example: schedule

Must be one of:
  • manual
  • schedule
  • webhook
  • event
search   string  optional    

optional Narrow to runs of workflows whose name contains this text (case-insensitive). Example: orders

sort   string  optional    

Which order to read the list in (created_at, started_at, duration). Defaults to created_at. duration is the time between a run starting and finishing. A run the chosen column cannot describe — one that never started, or one still running — is listed last whichever direction is asked for. Ordering is always broken by the run id, so paging never repeats or loses a row. Example: duration

Must be one of:
  • created_at
  • started_at
  • duration
direction   string  optional    

Which way to read the order: desc (the default) or asc. Example: desc

Must be one of:
  • asc
  • desc

Response

Response Fields

data   object     
workflow_name   string     

The name of the workflow the run belongs to. Always present — a run's workflow cannot be missing.

trigger_type   string     

What kind of trigger started the run (manual, schedule, webhook, event). Null when that trigger has since been deleted, or when the run predates the platform recording it.

error   string     

Why the run failed, in the platform's words; null for a run that did not fail.

meta   object     
has_more   boolean     

Whether a further page exists. This list reports no total — see GET /v1/runs/summary.

Count the tenant's runs

requires authentication

How many runs the tenant has, broken down by outcome, under exactly the narrowing GET /v1/runs accepts — so a figure and the list beside it can never disagree.

Separate from the list on purpose. Coupling "how many" to "which page" is what made clients read every page to count, and a tenant-wide COUNT on a list that refreshes itself every few seconds is the most expensive query this surface could ask for. Asked on its own, it can be asked less often.

Every status is present, zeros included. An absent key would leave "none of those" and "not loaded" reading alike, which is the confusion these figures exist to remove.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/runs/summary?status[]=failed&workflow_id=1&since=2026-08-01T00%3A00%3A00Z&until=2026-08-21T00%3A00%3A00Z&page=1&per_page=25&trigger_type=schedule&search=orders&sort=duration&direction=desc" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/runs/summary"
);

const params = {
    "status[0]": "failed",
    "workflow_id": "1",
    "since": "2026-08-01T00:00:00Z",
    "until": "2026-08-21T00:00:00Z",
    "page": "1",
    "per_page": "25",
    "trigger_type": "schedule",
    "search": "orders",
    "sort": "duration",
    "direction": "desc",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": {
        "total": 124,
        "in_flight": 3,
        "by_status": {
            "pending": 1,
            "running": 2,
            "completed": 100,
            "failed": 19,
            "cancelled": 1,
            "terminated": 1
        }
    }
}
 

Example response (401, Missing or invalid token):


{
    "message": "Unauthenticated."
}
 

Example response (403, No tenant context):


{
    "message": "Tenant context is required."
}
 

Request      

GET api/v1/runs/summary

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Query Parameters

status   string[]  optional    

Restrict to runs in these statuses. One value, or several comma-separated (pending, running, completed, failed, cancelled, terminated).

Must be one of:
  • pending
  • running
  • completed
  • failed
  • cancelled
  • terminated
workflow_id   integer  optional    

Restrict to runs of one workflow. A workflow of another tenant is rejected. Must be at least 1. Example: 1

since   string  optional    

Only runs created at or after this instant. Must be a valid date. Example: 2026-08-01T00:00:00Z

until   string  optional    

Only runs created at or before this instant. Must not precede since. Must be a valid date. Must be a date after or equal to since. Example: 2026-08-21T00:00:00Z

page   integer  optional    

Page number to retrieve (1-based). Defaults to 1. Must be at least 1. Example: 1

per_page   integer  optional    

Runs per page. Clamped to the configured maximum (100) rather than rejected. Example: 25

trigger_type   string  optional    

Restrict to runs started by this kind of trigger (manual, schedule, webhook, event). Example: schedule

Must be one of:
  • manual
  • schedule
  • webhook
  • event
search   string  optional    

Restrict to runs whose workflow's name contains this text (case-insensitive). Must not be greater than 200 characters. Example: orders

sort   string  optional    

Which order to read the list in (created_at, started_at, duration). Defaults to created_at. duration is the time between a run starting and finishing. A run the chosen column cannot describe — one that never started, or one still running — is listed last whichever direction is asked for. Ordering is always broken by the run id, so paging never repeats or loses a row. Example: duration

Must be one of:
  • created_at
  • started_at
  • duration
direction   string  optional    

Which way to read the order: desc (the default) or asc. Example: desc

Must be one of:
  • asc
  • desc

Response

Response Fields

data   object     
total   integer     

How many runs match the narrowing.

in_flight   integer     

How many are still expected to finish (pending + running), derived here so every surface counts it the same way.

by_status   object     

A count per run status. Every status appears, including the ones at zero.

The latest run of each workflow

requires authentication

One row per workflow that has ever run — its most recent run — newest workflow first. Workflows that have never run are absent rather than present-and-empty.

Its own endpoint because it is a different question from the feed, not a page of it. specs/workflows.md AC-4 promises a row says how its workflow last ran, and that a workflow which never ran claims no outcome at all — so reading it off the first page of the tenant's runs would make a workflow whose last run happens to be older than a hundred others claim it had never run, which is the mirror of the lie that criterion forbids.

Unpaged: there is at most one row per workflow, the same bound GET /v1/workflows already lives with.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/runs/latest-per-workflow" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/runs/latest-per-workflow"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        {
            "id": 42,
            "workflow_id": 1,
            "workflow_name": "Sync orders",
            "status": "failed",
            "error": "Node revenexx:http-request failed: 500",
            "trigger_handle": "9f1c8e2a-0b4d-4f7e-9a3c-1d2e3f4a5b6c",
            "trigger_type": "schedule",
            "temporal_workflow_id": "acme-production:1:42",
            "temporal_run_id": "018f...",
            "started_at": "2026-08-21T02:00:00Z",
            "completed_at": "2026-08-21T02:00:07Z",
            "created_at": "2026-08-21T02:00:00Z",
            "updated_at": "2026-08-21T02:00:07Z"
        }
    ]
}
 

Example response (401, Missing or invalid token):


{
    "message": "Unauthenticated."
}
 

Example response (403, No tenant context):


{
    "message": "Tenant context is required."
}
 

Request      

GET api/v1/runs/latest-per-workflow

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Schedules

Every scheduled trigger the tenant owns, on one list, each saying when it fires next and when it last really ran.

The per-workflow trigger list cannot answer this surface's questions: how many schedules are live across the tenant, and which timezones its schedules are actually in, are counts over the tenant rather than over one workflow — so a client assembling them from a fan-out could only ever report what it had managed to load.

List the tenant's schedules

requires authentication

Every schedule trigger across every workflow, and nothing else — a webhook or a manual trigger is not a schedule and is not listed.

Served whole rather than paged, like the tenant's workflows and credentials: there is one row per schedule, the page that shows them narrows and sorts them in the browser, and a count of what is live is only true if it covers all of them. meta therefore describes the whole tenant, not a page.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/schedules?active=1&timezone=Europe%2FBerlin&next_fire_count=1" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/schedules"
);

const params = {
    "active": "1",
    "timezone": "Europe/Berlin",
    "next_fire_count": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        {
            "id": 13,
            "workflow_id": 1,
            "workflow_name": "Sync orders",
            "handle": "9f1c8e2a-0b4d-4f7e-9a3c-1d2e3f4a5b6c",
            "name": null,
            "active": true,
            "workflow_active": true,
            "schedule": {
                "cron": "0 3 * * *",
                "timezone": "Europe/Berlin",
                "timezone_source": "schedule",
                "holds_firings": true,
                "not_firing_reason": null,
                "next_fire_at": "2026-08-22T01:00:00Z",
                "next_fire_times": [
                    {
                        "at": "2026-08-22T01:00:00Z",
                        "local": "2026-08-22T03:00:00+02:00"
                    }
                ],
                "last_dispatch_at": "2026-08-21T01:00:00Z",
                "last_run": {
                    "id": 4711,
                    "status": "completed",
                    "error": null,
                    "started_at": "2026-08-21T01:00:01Z",
                    "completed_at": "2026-08-21T01:00:44Z"
                }
            },
            "created_at": "2026-06-15T10:00:00Z",
            "updated_at": "2026-06-15T10:00:00Z"
        }
    ],
    "meta": {
        "total": 1,
        "live_count": 1,
        "timezones": [
            "Europe/Berlin"
        ]
    }
}
 

Example response (401, Missing or invalid token):


{
    "message": "Unauthenticated."
}
 

Example response (403, No tenant context):


{
    "message": "Tenant context is required."
}
 

Request      

GET api/v1/schedules

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Query Parameters

active   boolean  optional    

Restrict to live (true) or paused (false) schedules. Example: true

timezone   string  optional    

Restrict to schedules that run in this timezone. Matches the effective zone, so the platform default also matches schedules that carry none. The zones actually in use are reported as meta.timezones. Example: Europe/Berlin

next_fire_count   integer  optional    

How many upcoming firings each schedule reports (1–10). Defaults to 1. Must be at least 1. Must not be greater than 10. Example: 1

Response

Response Fields

data   object     
workflow_name   string     

The workflow the schedule belongs to.

active   boolean     

Whether the schedule itself is live. Distinct from workflow_active: a live schedule on a deactivated workflow does not fire, and schedule.not_firing_reason says which of the two is the cause.

workflow_active   boolean     

Whether the workflow the schedule belongs to is active.

schedule   object     

The firings the platform holds and the last run it performed — the same block the per-workflow trigger list returns.

meta   object     
live_count   integer     

How many of the tenant's schedules are live, counted over the tenant rather than the response.

timezones   string[]     

The effective timezones the tenant's schedules actually run in, sorted. A schedule carrying none contributes the platform default.

Schemas

Public, read-only access to the JSON-Schema definitions used to validate versioned payloads (currently the workflow blob and the node manifest). Schemas are global — they describe the shape of payloads regardless of tenant — so these endpoints are intentionally unauthenticated.

List schema versions

Returns the identifiers of every registered schema version for the given domain, in registration order (which is also the order in which they are accepted by the validation layer).

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/schemas/workflow" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/schemas/workflow"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "domain": "workflow",
    "versions": [
        "v0-draft"
    ]
}
 

Example response (404, Unknown domain):


{
    "message": "Unknown schema domain."
}
 

Request      

GET api/v1/schemas/{domain}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

domain   string     

The schema domain. Supported domains: workflow, node. Example: workflow

Show a schema version

Returns the serialized JSON-Schema definition for one specific (domain, version) pair. The payload uses the standard JSON Schema vocabulary (type, properties, required, …) and is suitable for client-side validation or form generation.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/schemas/workflow/v0-draft" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/schemas/workflow/v0-draft"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "domain": "workflow",
    "version": "v0-draft",
    "schema": {
        "title": "Workflow Definition (v0-draft)",
        "type": "object"
    }
}
 

Example response (404, Unknown version):


{
    "message": "Unknown schema version."
}
 

Request      

GET api/v1/schemas/{domain}/{version}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

domain   string     

The schema domain. Example: workflow

version   string     

The schema version identifier. Example: v0-draft

State Store

The operator's window into the tenant state store (PO-374): which namespaces exist, what they hold, and the ability to correct a single entry when a sync went wrong.

This exists because the support question is never abstract — it is "why was article X created twice?", and the answer is one lookup away if somebody can see the correlation and the run that wrote it. Without this view the store would be a black box that only the workflow author can reason about.

Deliberately read-and-correct, not read-and-write: there is no bulk import. That is the door through which "let's just load our master data in here" walks in, and the store is not a database.

List state namespaces

requires authentication

Every namespace in the current tenant, with its entry count.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/state/namespaces" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/state/namespaces"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        {
            "id": 1,
            "name": "article",
            "role": "mapping",
            "visibility": "shared",
            "owner_workflow_id": null,
            "entries": 2,
            "ttl_seconds": null,
            "updated_at": "2026-08-26T10:00:00Z"
        }
    ]
}
 

Request      

GET api/v1/state/namespaces

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

List the entries of a namespace

requires authentication

Paginated, narrowable by key with ?q=.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/state/namespaces/1/entries?q=pim%3A12345" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"q\": \"b\",
    \"per_page\": 22
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/state/namespaces/1/entries"
);

const params = {
    "q": "pim:12345",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "q": "b",
    "per_page": 22
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        {
            "id": 7,
            "key": "pim:12345",
            "value": "erp:A-8891",
            "last_run_id": 42,
            "updated_at": "2026-08-26T10:00:00Z"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "total": 1,
        "last_page": 1
    }
}
 

Request      

GET api/v1/state/namespaces/{id}/entries

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

id   integer     

The namespace id. Example: 1

Query Parameters

q   string  optional    

Narrow to entries whose key contains this. Example: pim:12345

Body Parameters

q   string  optional    

Must not be greater than 255 characters. Example: b

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 22

Delete one entry

requires authentication

The correction path for a wrong correlation or a stuck cursor. Audited, because an operator editing what a run wrote is exactly the kind of change somebody will want to trace back later.

Example request:
curl --request DELETE \
    "https://integrations.revenexx.com/api/v1/state/namespaces/1/entries/7" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/state/namespaces/1/entries/7"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, Deleted):

Empty response
 

Request      

DELETE api/v1/state/namespaces/{id}/entries/{entryId}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

id   integer     

The namespace id. Example: 1

entryId   integer     

The entry id. Example: 7

Delete a namespace

requires authentication

Removes the namespace and every entry in it. The explicit act a shared namespace requires: it outlives the workflows that declared it, so nothing else ever removes one.

Example request:
curl --request DELETE \
    "https://integrations.revenexx.com/api/v1/state/namespaces/1" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/state/namespaces/1"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, Deleted):

Empty response
 

Request      

DELETE api/v1/state/namespaces/{id}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

id   integer     

The namespace id. Example: 1

Templates

Catalogue of the workflow templates registered via node packages, plus the "install" actions a UI uses to turn one into a real workflow. Listing/showing is global; instantiation is scoped to the current tenant.

List templates

requires authentication

Returns the latest registered version of each template, without the (heavy) embedded workflow definition blob — that ships only on show.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/templates" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/templates"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        {
            "slug": "revenexx:bc-sales-order-digest",
            "version": "1.0.0",
            "category": "sales",
            "level": "beginner",
            "name": "Daily Sales-Order Digest",
            "shortDescription": "Email a daily summary of Business Central sales orders.",
            "description": null,
            "icon": "mdi:email-newsletter",
            "industries": [
                "any"
            ],
            "vendors": [
                "microsoft",
                "business-central"
            ],
            "images": [
                {
                    "src": "https://api.example.com/v1/node-packages/images/2b1c0000-0000-4000-8000-000000000000?expires=1750000000&signature=abcdef",
                    "alt": {
                        "en": "Business Central banner"
                    },
                    "title": {
                        "en": "Business Central"
                    },
                    "category": "banner"
                }
            ],
            "triggerTypes": [
                "schedule"
            ]
        }
    ]
}
 

Example response (401, Missing or invalid token):


{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/templates

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Show a template

requires authentication

Returns the latest registered version of the template identified by its namespaced slug, including the embedded workflow definition used to instantiate a new workflow.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/templates/revenexx:slack-to-crm" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/templates/revenexx:slack-to-crm"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": {
        "slug": "revenexx:bc-sales-order-digest",
        "version": "1.0.0",
        "category": "sales",
        "level": "beginner",
        "name": "Daily Sales-Order Digest",
        "shortDescription": "Email a daily summary of Business Central sales orders.",
        "description": null,
        "icon": "mdi:email-newsletter",
        "industries": [
            "any"
        ],
        "vendors": [
            "microsoft",
            "business-central"
        ],
        "images": [
            {
                "src": "https://api.example.com/v1/node-packages/images/2b1c0000-0000-4000-8000-000000000000?expires=1750000000&signature=abcdef",
                "alt": {
                    "en": "Business Central banner"
                },
                "title": {
                    "en": "Business Central"
                },
                "category": "banner"
            }
        ],
        "triggerTypes": [
            "schedule"
        ],
        "blob_version": "v0-draft",
        "definition": {
            "nodeManifestVersion": "v0-draft",
            "name": "Daily Sales-Order Digest",
            "nodes": [],
            "edges": []
        },
        "triggers": [
            {
                "handle": "a1c0ffee-0001-4001-8001-000000000001",
                "type": "schedule",
                "config": {
                    "cron": "0 7 * * *"
                }
            }
        ]
    }
}
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Request      

GET api/v1/templates/{slug}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

slug   string     

Namespaced template slug. Example: revenexx:slack-to-crm

Template installation requirements

requires authentication

Lists the credential types and secret keys the template's workflow references, cross-referenced against the current tenant (how many credential instances already exist, whether each secret key is stored) so an install UI can have the user provision what is missing first.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/templates/revenexx:slack-to-crm/requirements" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/templates/revenexx:slack-to-crm/requirements"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": {
        "credentialTypes": [
            {
                "slug": "revenexx:business-central",
                "registered": true,
                "existingInstances": 1,
                "usedBy": [
                    {
                        "nodeId": "list_orders",
                        "configKey": "credentials"
                    }
                ],
                "version": "1.0.0",
                "name": "Business Central",
                "authKind": "oauth2-client-credentials",
                "fields": []
            }
        ],
        "secrets": []
    }
}
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Request      

GET api/v1/templates/{slug}/requirements

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

slug   string     

Namespaced template slug. Example: revenexx:slack-to-crm

Instantiate a template

requires authentication

Creates a new workflow (and its triggers) for the current tenant from the template's blueprint. Trigger handles are regenerated so repeated installs never collide; the workflow and all triggers start inactive. Returns the created workflow.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/templates/revenexx:slack-to-crm/instantiate" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"name\": \"My Slack flow\",
    \"credential_bindings\": [
        [
            \"6ff8f7f6-1eb3-3525-be4a-3932c805afed\"
        ]
    ]
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/templates/revenexx:slack-to-crm/instantiate"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "name": "My Slack flow",
    "credential_bindings": [
        [
            "6ff8f7f6-1eb3-3525-be4a-3932c805afed"
        ]
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201, Created):


{
    "id": 1,
    "name": "Daily Sales-Order Digest",
    "description": null,
    "blob_definition_version": "v0-draft",
    "blob": {},
    "active": false,
    "revision": 1,
    "created_at": "2026-05-06T10:00:00Z",
    "updated_at": "2026-05-06T10:00:00Z"
}
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Example response (422, Validation failed):


{
    "message": "The given data was invalid.",
    "errors": {
        "credential_bindings": [
            "The credential_bindings field must be an array."
        ]
    }
}
 

Request      

POST api/v1/templates/{slug}/instantiate

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

slug   string     

Namespaced template slug. Example: revenexx:slack-to-crm

Body Parameters

name   string  optional    

Optional workflow name override; defaults to the template name. Must not be greater than 255 characters. Example: My Slack flow

credential_bindings   string[][]  optional    

Must be a valid UUID.

Tenant Secrets

Per-tenant encrypted key/value store. Values are AES-256-GCM encrypted at rest using a dedicated SECRETS_ENCRYPTION_KEY (separate from APP_KEY) and are never returned by the API.

List secrets

requires authentication

Returns the alphabetically sorted secrets of the current tenant in data — key plus timestamps, never the value. updated_at is when the value was last written, which is what "when was this last rotated" asks for; overwriting a secret with the same plaintext still counts as a write.

Replaces the earlier {"keys": [...]} envelope. Every consumer of this endpoint is first-party and reads data.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/secrets" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/secrets"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        {
            "key": "SLACK_WEBHOOK_URL",
            "created_at": "2026-05-04T08:50:00.000000Z",
            "updated_at": "2026-05-04T08:50:00.000000Z"
        },
        {
            "key": "STRIPE_API_KEY",
            "created_at": "2026-05-05T08:50:00.000000Z",
            "updated_at": "2026-07-01T11:02:00.000000Z"
        }
    ]
}
 

Example response (401, Missing or invalid token):


{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/secrets

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Create or update a secret

requires authentication

Upserts a secret value for the given key within the current tenant. Returns 201 on creation, 200 on update. The value is never echoed back.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/secrets" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"key\": \"STRIPE_API_KEY\",
    \"value\": \"sk_live_abc123\"
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/secrets"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "key": "STRIPE_API_KEY",
    "value": "sk_live_abc123"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200, Updated):


{
    "key": "STRIPE_API_KEY",
    "created_at": "2026-05-04T08:50:00Z",
    "updated_at": "2026-05-05T08:50:00Z"
}
 

Example response (201, Created):


{
    "key": "STRIPE_API_KEY",
    "created_at": "2026-05-05T08:50:00Z",
    "updated_at": "2026-05-05T08:50:00Z"
}
 

Example response (422, Validation failed):


{
    "message": "The key field format is invalid.",
    "errors": {
        "key": [
            "..."
        ]
    }
}
 

Request      

POST api/v1/secrets

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Body Parameters

key   string     

Env-style key, must match ^[A-Z0-9_]+$. Example: STRIPE_API_KEY

value   string     

The secret value. Example: sk_live_abc123

Update a secret value

requires authentication

Replaces the value of an existing secret. The key is immutable — create a new secret and delete the old one if you need to rename a key. Returns 404 if the key does not exist for the current tenant.

Example request:
curl --request PATCH \
    "https://integrations.revenexx.com/api/v1/secrets/bc-client-secret" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"value\": \"sk_live_new_value\"
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/secrets/bc-client-secret"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "value": "sk_live_new_value"
};

fetch(url, {
    method: "PATCH",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200, Updated):


{
    "key": "bc-client-secret",
    "created_at": "2026-05-04T08:50:00Z",
    "updated_at": "2026-06-12T10:00:00Z"
}
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Example response (422, Validation failed):


{
    "message": "The value field is required.",
    "errors": {
        "value": [
            "..."
        ]
    }
}
 

Request      

PATCH api/v1/secrets/{key}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

key   string     

The secret key to update. Example: bc-client-secret

Body Parameters

value   string     

The new secret value. Example: sk_live_new_value

Delete a secret

requires authentication

Removes the secret identified by {key} for the current tenant. The lookup is explicitly scoped to the current tenant, so a key belonging to another tenant — or one that does not exist at all — yields a 404 without leaking existence either way.

Example request:
curl --request DELETE \
    "https://integrations.revenexx.com/api/v1/secrets/STRIPE_API_KEY" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/secrets/STRIPE_API_KEY"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, Deleted):

Empty response
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Request      

DELETE api/v1/secrets/{key}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

key   string     

The secret key to delete. Example: STRIPE_API_KEY

Triggers

Manage triggers for a workflow. Triggers are the entry points that can start a workflow execution (manual, schedule, webhook, or event). They live as DB entities rather than inside the workflow blob, and are referenced in blob edges via their stable UUID handle.

List triggers for a workflow

requires authentication

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/1/triggers?next_fire_count=3" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/triggers"
);

const params = {
    "next_fire_count": "3",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        {
            "id": 12,
            "workflow_id": 1,
            "handle": "6ff8f7f6-1eb3-3525-be4a-3932c805afed",
            "type": "webhook",
            "name": "PSP webhook",
            "config": {
                "method": "POST",
                "public": true,
                "inbound": {
                    "endpoint_id": "inb_abc123",
                    "url": "https://webhooks.revenexx.com/in/inb_abc123"
                }
            },
            "active": true,
            "created_at": "2026-06-15T10:00:00Z",
            "updated_at": "2026-06-15T10:00:00Z"
        }
    ]
}
 

Example response (200, A schedule trigger):


{
    "data": [
        {
            "id": 13,
            "workflow_id": 1,
            "handle": "9f1c8e2a-0b4d-4f7e-9a3c-1d2e3f4a5b6c",
            "type": "schedule",
            "name": null,
            "config": {
                "cron": "0 3 * * *",
                "timezone": "Europe/Berlin"
            },
            "active": true,
            "schedule": {
                "cron": "0 3 * * *",
                "timezone": "Europe/Berlin",
                "timezone_source": "schedule",
                "holds_firings": true,
                "not_firing_reason": null,
                "next_fire_at": "2026-08-22T01:00:00Z",
                "next_fire_times": [
                    {
                        "at": "2026-08-22T01:00:00Z",
                        "local": "2026-08-22T03:00:00+02:00"
                    }
                ],
                "last_dispatch_at": "2026-08-21T01:00:00Z",
                "last_run": {
                    "id": 4711,
                    "status": "completed",
                    "error": null,
                    "started_at": "2026-08-21T01:00:01Z",
                    "completed_at": "2026-08-21T01:00:44Z"
                }
            },
            "created_at": "2026-06-15T10:00:00Z",
            "updated_at": "2026-06-15T10:00:00Z"
        }
    ]
}
 

Request      

GET api/v1/workflows/{workflowId}/triggers

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

Query Parameters

next_fire_count   integer  optional    

optional How many upcoming firings each schedule reports (1–10). Defaults to 1. Example: 3

Response

Response Fields

data   object     
schedule   object     

Present only on schedule triggers: the firings the platform actually holds, and the last run it really performed.

holds_firings   boolean     

Whether the platform will fire this schedule. False whenever the scheduler would never select it — see not_firing_reason.

not_firing_reason   string     

Why there is no next firing: trigger_paused, workflow_inactive, workflow_not_ready, no_cron, invalid_cron, unknown_timezone, unsatisfiable. Null when it does fire.

next_fire_times   object[]     

The upcoming firings, each as a UTC instant (at) and the same moment in the schedule's own zone (local) — whose offset says which side of a clock change it falls on. Empty when the schedule holds none.

last_dispatch_at   string     

When the scheduler last DECIDED to fire this trigger. Stamped before the run exists, so it can differ from last_run — a firing that was dispatched and then lost.

last_run   object     

The last run this schedule actually started; null when it has never run.

timezone_source   string     

schedule when the zone is the schedule's own, platform_default when it carries none and runs in the platform's.

Create a trigger

requires authentication

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/workflows/1/triggers" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"handle\": \"6ff8f7f6-1eb3-3525-be4a-3932c805afed\",
    \"type\": \"webhook\",
    \"name\": \"PSP webhook\",
    \"active\": true,
    \"config\": {
        \"method\": \"POST\",
        \"public\": true,
        \"authProfile\": \"token\"
    }
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/triggers"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "handle": "6ff8f7f6-1eb3-3525-be4a-3932c805afed",
    "type": "webhook",
    "name": "PSP webhook",
    "active": true,
    "config": {
        "method": "POST",
        "public": true,
        "authProfile": "token"
    }
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201, Public webhook trigger):


{
    "id": 12,
    "workflow_id": 1,
    "handle": "6ff8f7f6-1eb3-3525-be4a-3932c805afed",
    "type": "webhook",
    "name": "PSP webhook",
    "config": {
        "method": "POST",
        "public": true,
        "inbound": {
            "endpoint_id": "inb_abc123",
            "url": "https://webhooks.revenexx.com/in/inb_abc123"
        }
    },
    "active": true,
    "created_at": "2026-06-15T10:00:00Z",
    "updated_at": "2026-06-15T10:00:00Z",
    "inbound_secret": "whsec_ing_onetime"
}
 

Example response (422, Validation failed):


{
    "message": "The config.method field is required.",
    "errors": {
        "config.method": [
            "The config.method field is required."
        ]
    }
}
 

Request      

POST api/v1/workflows/{workflowId}/triggers

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

Body Parameters

handle   string     

Stable UUID referenced by the workflow blob edges. Must be unique per tenant. Must be a valid UUID. Example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed

type   string     

Trigger entry-point type. Example: webhook

Must be one of:
  • manual
  • schedule
  • webhook
  • event
name   string  optional    

Optional human-readable label. Example: PSP webhook

active   boolean  optional    

Whether the trigger is enabled. Defaults to true. Example: true

config   object  optional    

Per-type trigger configuration. Fields depend on type: webhook → method (required HTTP method), optional public (expose via the inbound ingress, ADR-0066 — provisioning returns inbound_secret once), authProfile (ingress verification profile: none|token|basic|hmac-generic|stripe|standard-webhooks; default token) and authConfig, whose keys belong to the chosen profile (token: header, prefix; basic: username; hmac-generic: header, prefix, algorithm sha256|sha512, encoding hex|base64; none, stripe, standard-webhooks: none; mode enforce|annotate for all of them) — a key another profile owns is rejected. secretRef is deprecated — superseded by authProfile/authConfig; still accepted, and still read when a workflow template declares its required secrets, but it verifies nothing on the inbound path. schedule → cron (required) and optional timezone (IANA); manual → none.

Response

Response Fields

inbound_secret   string     

Present ONLY right after a public webhook endpoint is provisioned — the ingress ingest token, returned once and never stored (ADR-0066).

config   object     
inbound   object     

Server-owned ingress registration (endpoint_id, url) for a public webhook trigger. Clients cannot set or modify it.

Update a trigger

requires authentication

Example request:
curl --request PUT \
    "https://integrations.revenexx.com/api/v1/workflows/1/triggers/1" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"type\": \"webhook\",
    \"name\": \"PSP webhook\",
    \"active\": true,
    \"config\": {
        \"method\": \"POST\",
        \"public\": true,
        \"authProfile\": \"token\"
    }
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/triggers/1"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "type": "webhook",
    "name": "PSP webhook",
    "active": true,
    "config": {
        "method": "POST",
        "public": true,
        "authProfile": "token"
    }
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200, Public webhook trigger):


{
    "id": 12,
    "workflow_id": 1,
    "handle": "6ff8f7f6-1eb3-3525-be4a-3932c805afed",
    "type": "webhook",
    "name": "PSP webhook",
    "config": {
        "method": "POST",
        "public": true,
        "inbound": {
            "endpoint_id": "inb_abc123",
            "url": "https://webhooks.revenexx.com/in/inb_abc123"
        }
    },
    "active": true,
    "created_at": "2026-06-15T10:00:00Z",
    "updated_at": "2026-06-15T10:05:00Z",
    "inbound_secret": "whsec_ing_onetime"
}
 

Example response (422, Validation failed):


{
    "message": "The config.method field is required.",
    "errors": {
        "config.method": [
            "The config.method field is required."
        ]
    }
}
 

Request      

PUT api/v1/workflows/{workflowId}/triggers/{triggerId}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

triggerId   integer     

The trigger id. Example: 1

Body Parameters

type   string     

Trigger entry-point type. Example: webhook

Must be one of:
  • manual
  • schedule
  • webhook
  • event
name   string  optional    

Optional human-readable label. Example: PSP webhook

active   boolean     

Whether the trigger is enabled. Example: true

config   object  optional    

Per-type trigger configuration. Fields depend on type: webhook → method (required HTTP method), optional public (expose via the inbound ingress, ADR-0066 — provisioning returns inbound_secret once), authProfile (ingress verification profile: none|token|basic|hmac-generic|stripe|standard-webhooks; default token) and authConfig, whose keys belong to the chosen profile (token: header, prefix; basic: username; hmac-generic: header, prefix, algorithm sha256|sha512, encoding hex|base64; none, stripe, standard-webhooks: none; mode enforce|annotate for all of them) — a key another profile owns is rejected. secretRef is deprecated — superseded by authProfile/authConfig; still accepted, and still read when a workflow template declares its required secrets, but it verifies nothing on the inbound path. schedule → cron (required) and optional timezone (IANA); manual → none.

Response

Response Fields

inbound_secret   string     

Present ONLY right after a public webhook endpoint is provisioned — the ingress ingest token, returned once and never stored (ADR-0066).

config   object     
inbound   object     

Server-owned ingress registration (endpoint_id, url) for a public webhook trigger. Clients cannot set or modify it.

Delete a trigger

requires authentication

Example request:
curl --request DELETE \
    "https://integrations.revenexx.com/api/v1/workflows/1/triggers/1" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/triggers/1"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, Deleted):

Empty response
 

Request      

DELETE api/v1/workflows/{workflowId}/triggers/{triggerId}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

triggerId   integer     

The trigger id. Example: 1

Rotate the ingest secret of a public webhook trigger

requires authentication

Generates a fresh ingest secret on the trigger's ingress endpoint. The previous secret stops working immediately. Only available for webhook triggers that are exposed via the inbound ingress (config.public).

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/workflows/1/triggers/6ff8f7f6-1eb3-3525-be4a-3932c805afed/rotate-secret" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/triggers/6ff8f7f6-1eb3-3525-be4a-3932c805afed/rotate-secret"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200, Rotated):


{
    "inbound_secret": "whsec_ing_onetime"
}
 

Example response (409, Not exposed):


{
    "message": "Trigger is not exposed via the inbound ingress; set config.public first."
}
 

Example response (409, Ingress unavailable):


{
    "message": "Secret rotation is not available: the inbound ingress is not configured."
}
 

Request      

POST api/v1/workflows/{workflowId}/triggers/{triggerHandle}/rotate-secret

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

triggerHandle   string     

The trigger's UUID handle. Example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed

Response

Response Fields

inbound_secret   string     

The freshly generated ingress ingest token — returned once and never stored (ADR-0066).

Webhooks

Inbound endpoint that fires a workflow run for a webhook trigger. The trigger is addressed by its UUID handle in the URL; authentication is the standard JWT + X-Tenant-Id used by the rest of the API, so the trigger is resolved within the caller's tenant. The HTTP request (body, query, headers, method) is delivered to the workflow as the trigger payload, reachable via ${{ trigger.payload.* }}.

Fire a webhook trigger

requires authentication

Resolves the webhook trigger by handle, enforces the configured HTTP method, and starts a run. The mode follows the workflow's execution_mode (async_only → 202; sync_only/caller_decides → wait and return the result), and can be overridden per request with ?mode=sync|async when the policy allows it — a conflicting mode is rejected with 422.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/webhooks/0190a9c0-1111-7000-8000-000000000000?mode=async" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/webhooks/0190a9c0-1111-7000-8000-000000000000"
);

const params = {
    "mode": "async",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Completed (sync)):


{
    "id": 1,
    "workflow_id": 1,
    "status": "completed",
    "result": {}
}
 

Example response (202, Accepted (async)):


{
    "id": 1,
    "workflow_id": 1,
    "status": "running"
}
 

Example response (404, No such webhook trigger):


{
    "message": "No active webhook trigger with handle … exists."
}
 

Example response (405, Method not allowed):


{
    "message": "This webhook expects POST."
}
 

Example response (422, Mode conflicts with policy):


{
    "message": "Workflow does not allow sync execution (execution_mode=async_only)."
}
 

Request      

GET api/v1/webhooks/{handle}

POST api/v1/webhooks/{handle}

PUT api/v1/webhooks/{handle}

PATCH api/v1/webhooks/{handle}

DELETE api/v1/webhooks/{handle}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

handle   string     

The webhook trigger handle (UUID). Example: 0190a9c0-1111-7000-8000-000000000000

Query Parameters

mode   string  optional    

optional Force sync or async. Honored for caller_decides workflows; conflicting with async_only/sync_only yields 422. Example: async

Workflow Credentials

Read-only introspection of which typed credentials a workflow's nodes reference through their credentials-ref config fields. Useful for pre-run checks, access auditing, and impact analysis when a credential instance is rotated or deleted. The credential values are never touched here — only the instance UUIDs and their credential type slugs are reported.

List credential ids used across the tenant's workflows

requires authentication

Returns the distinct credential instance UUIDs referenced by every workflow of the current tenant. Optionally filtered to a single credential type slug via ?type={slug} and/or to active workflows only via ?active=true.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/used-credentials?type=revenexx%3Asmtp&active=1" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/used-credentials"
);

const params = {
    "type": "revenexx:smtp",
    "active": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Used credentials):


{
    "data": [
        "9f3e2c1a-0000-4000-8000-000000000001",
        "1ab2c3d4-0000-4000-8000-000000000002"
    ]
}
 

Request      

GET api/v1/workflows/used-credentials

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Query Parameters

type   string  optional    

Filter to a single credential type slug. Example: revenexx:smtp

active   boolean  optional    

Only consider active workflows. Example: true

List credential type slugs used across the tenant's workflows

requires authentication

Returns the distinct credential type slugs (e.g. revenexx:smtp) referenced by every workflow of the current tenant. Optionally filtered to active workflows only via ?active=true.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/used-credential-types?active=1" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/used-credential-types"
);

const params = {
    "active": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Used credential types):


{
    "data": [
        "revenexx:smtp",
        "revenexx:deepl"
    ]
}
 

Request      

GET api/v1/workflows/used-credential-types

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Query Parameters

active   boolean  optional    

Only consider active workflows. Example: true

List credential ids used by a workflow

requires authentication

Returns the distinct credential instance UUIDs the workflow's nodes reference, optionally filtered to a single credential type slug via ?type={slug}.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/1/credentials?type=revenexx%3Asmtp" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/credentials"
);

const params = {
    "type": "revenexx:smtp",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Used credentials):


{
    "data": [
        "9f3e2c1a-0000-4000-8000-000000000001",
        "1ab2c3d4-0000-4000-8000-000000000002"
    ]
}
 

Example response (404, Workflow not found):


{
    "message": "Not found."
}
 

Request      

GET api/v1/workflows/{workflowId}/credentials

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

Query Parameters

type   string  optional    

Filter to a single credential type slug. Example: revenexx:smtp

List credential type slugs used by a workflow

requires authentication

Returns the distinct credential type slugs (e.g. revenexx:smtp) the workflow's nodes reference through their credentials-ref config fields.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/1/credential-types" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/credential-types"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Used credential types):


{
    "data": [
        "revenexx:smtp",
        "revenexx:deepl"
    ]
}
 

Example response (404, Workflow not found):


{
    "message": "Not found."
}
 

Request      

GET api/v1/workflows/{workflowId}/credential-types

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

Workflow Revisions

Read the archived revision history of a workflow and restore an old revision. Every blob change to a workflow snapshots the previous revision into workflow_histories; these endpoints expose that history and let a client re-apply an old revision as a brand-new one.

All endpoints are scoped to the tenant resolved by the resolve.tenant middleware. Cross-tenant access returns 404 so the API never leaks the existence of resources owned by other tenants.

List workflow revisions

requires authentication

Returns the archived revisions of a workflow, newest first, paginated. List items carry metadata only — use the show endpoint to fetch a single revision's full blob and triggers.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/1/revisions?page=1&per_page=25" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/revisions"
);

const params = {
    "page": "1",
    "per_page": "25",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        {
            "id": 7,
            "revision": 2,
            "name": "Sync orders",
            "description": null,
            "active": true,
            "blob_definition_version": "v0-draft",
            "created_at": "2026-05-06T10:00:00Z",
            "updated_at": "2026-05-06T11:00:00Z",
            "archived_at": "2026-05-06T12:00:00Z"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "total": 1,
        "last_page": 1
    }
}
 

Example response (404, Workflow not found):


{
    "message": "Not found."
}
 

Request      

GET api/v1/workflows/{workflowId}/revisions

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

Query Parameters

page   integer  optional    

Page number to retrieve (1-based). Defaults to 1. Must be at least 1. Example: 1

per_page   integer  optional    

Number of revisions per page (1–100). Defaults to 25. Must be at least 1. Must not be greater than 100. Example: 25

Show a workflow revision

requires authentication

Returns a single archived revision in full, including its blob and triggers snapshot.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/1/revisions/1" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/revisions/1"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "id": 7,
    "revision": 1,
    "name": "Sync orders",
    "description": null,
    "active": true,
    "blob_definition_version": "v0-draft",
    "blob": {},
    "triggers": [],
    "created_at": "2026-05-06T10:00:00Z",
    "updated_at": "2026-05-06T11:00:00Z",
    "archived_at": "2026-05-06T12:00:00Z"
}
 

Example response (404, Revision or workflow not found):


{
    "message": "Not found."
}
 

Request      

GET api/v1/workflows/{workflowId}/revisions/{revision}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

revision   integer     

The archived revision number. Example: 1

Restore a workflow revision

requires authentication

Re-applies the given archived revision's definition (blob, blob_definition_version, and triggers) to the workflow as a brand-new revision. The current state is first snapshotted into the history, the revision counter is bumped, and a fresh bundle build is dispatched. The workflow's name, description and execution_mode are left unchanged. active is preserved too, except that restoring a schema- or manifest-invalid revision onto an active workflow forces it inactive to keep the active ⇒ valid invariant (PO-186) — the restore still succeeds.

Restoring a revision whose definition is identical to the current one is a safe no-op for the revision counter (no new history row is written), though triggers are still re-synced from the snapshot.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/workflows/1/revisions/1/restore" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/revisions/1/restore"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200, Restored):


{
    "id": 1,
    "name": "Sync orders",
    "revision": 3,
    "build_status": "pending",
    "blob": {},
    "triggers": []
}
 

Example response (404, Revision or workflow not found):


{
    "message": "Not found."
}
 

Request      

POST api/v1/workflows/{workflowId}/revisions/{revision}/restore

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

revision   integer     

The archived revision number to restore. Example: 1

Workflow Runs

Trigger and inspect executions of a workflow. Each run is handed off to the workflow's dedicated Temporal worker, which orchestrates node execution and streams status back.

List runs for a workflow

requires authentication

Returns all runs for the workflow, newest first.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/1/runs?page=1&per_page=25" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/runs"
);

const params = {
    "page": "1",
    "per_page": "25",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [],
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "total": 0,
        "last_page": 1
    }
}
 

Request      

GET api/v1/workflows/{workflowId}/runs

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

Query Parameters

page   integer  optional    

Page number to retrieve (1-based). Defaults to 1. Must be at least 1. Example: 1

per_page   integer  optional    

Number of runs per page (1–100). Defaults to 25. Must be at least 1. Must not be greater than 100. Example: 25

Start a workflow run

requires authentication

Execution behavior depends on workflow execution_mode and optional request mode:

For caller_decides, omitting mode defaults to async execution.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/workflows/1/runs" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"trigger_payload\": {
        \"source\": \"api\"
    },
    \"trigger_handle\": \"6ff8f7f6-1eb3-3525-be4a-3932c805afed\",
    \"mode\": \"sync\"
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/runs"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "trigger_payload": {
        "source": "api"
    },
    "trigger_handle": "6ff8f7f6-1eb3-3525-be4a-3932c805afed",
    "mode": "sync"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200, Sync completed):


{
    "id": 1,
    "workflow_id": 1,
    "status": "completed",
    "result": {
        "node-1": {
            "ok": true
        }
    },
    "completed_at": "2026-05-22T10:00:30Z"
}
 

Example response (202, Accepted):


{
    "id": 1,
    "workflow_id": 1,
    "status": "running",
    "temporal_workflow_id": "tenant:1:abc",
    "started_at": "2026-05-13T10:00:00Z"
}
 

Example response (404, Workflow not found):


{
    "message": "Not found."
}
 

Example response (422, Unresolved manifest issues (PO-186)):


{
    "message": "Workflow configuration has unresolved issues and cannot run. Fix the reported fields and try again.",
    "errors": {
        "blob.nodes.0.config.credentials": [
            "Required config field 'credentials' is missing."
        ]
    }
}
 

Example response (422, Mode not allowed):


{
    "message": "Workflow does not allow sync execution (execution_mode=async_only)."
}
 

Example response (422, Sync failed):


{
    "message": "Workflow execution failed.",
    "error": "node execution failed",
    "run": {
        "id": 1,
        "status": "failed"
    }
}
 

Example response (501, Sync not supported by starter):


{
    "message": "Synchronous workflow execution is not supported by the configured run starter driver."
}
 

Example response (503, Worker unavailable):


{
    "message": "Workflow worker is unavailable."
}
 

Example response (504, Sync timeout):


{
    "message": "Workflow execution timed out.",
    "error": "Sync run for workflow 1 exceeded timeout of 30 seconds."
}
 

Request      

POST api/v1/workflows/{workflowId}/runs

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

Body Parameters

trigger_payload   object  optional    

optional Arbitrary trigger payload forwarded to the workflow.

trigger_handle   string  optional    

UUID handle of the trigger to run as. Optional; defaults to the workflow's manual trigger. Must be a valid UUID. Example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed

mode   string  optional    

optional Requested run mode. Allowed: sync, async. Resolution depends on workflow execution_mode policy. Example: sync

Get a workflow run

requires authentication

Returns the current state of a workflow run. If the run is still in progress, the worker is polled for the latest Temporal status.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/1/runs/1" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/runs/1"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Completed):


{
    "id": 1,
    "workflow_id": 1,
    "status": "completed",
    "result": {},
    "completed_at": "2026-05-13T10:01:00Z"
}
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Request      

GET api/v1/workflows/{workflowId}/runs/{runId}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

runId   integer     

The run id. Example: 1

Get execution details for a workflow run

requires authentication

Returns the execution description (status, timing, history length, parent linkage) via {@see WorkflowExecutionInspector}. The inspector transparently routes between a live Temporal call and the local snapshot taken when the run reached a terminal status; the response carries a source field of "temporal" or "snapshot" so clients know which view they are seeing.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/details" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/details"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Live):


{
    "source": "temporal",
    "data": {
        "workflow_id": "tenant:1:abc",
        "run_id": "0190...",
        "status": "running"
    }
}
 

Example response (200, Snapshot):


{
    "source": "snapshot",
    "data": {
        "workflow_id": "tenant:1:abc",
        "run_id": "0190...",
        "status": "completed"
    }
}
 

Example response (404, Run not found):


{
    "message": "Not found."
}
 

Example response (409, Run not handed off to Temporal yet):


{
    "message": "Workflow run has no Temporal execution yet."
}
 

Example response (410, History unavailable):


{
    "message": "Workflow execution history is no longer available."
}
 

Example response (503, Temporal unavailable):


{
    "message": "Temporal is currently unavailable."
}
 

Request      

GET api/v1/workflows/{workflowId}/runs/{runId}/details

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

runId   integer     

The run id. Example: 1

Get execution history for a workflow run

requires authentication

Returns one page of history events via {@see WorkflowExecutionInspector}. Like the details endpoint, the response is hybrid-routed and carries a source field. The next_page_token is opaque (source-tagged with tmp: or snp:); pass it verbatim to the next call. Note that switching sources mid-pagination resets to page 1 (the response's source will change), which the client should treat as a fresh pagination.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/history?page_size=200" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/history"
);

const params = {
    "page_size": "200",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Live page):


{
    "source": "temporal",
    "data": [
        {
            "event_id": 1,
            "event_type": "workflow_execution_started"
        }
    ],
    "next_page_token": null
}
 

Example response (200, Snapshot page):


{
    "source": "snapshot",
    "data": [
        {
            "event_id": 1
        }
    ],
    "next_page_token": null
}
 

Example response (404, Run not found):


{
    "message": "Not found."
}
 

Example response (409, Run not handed off to Temporal yet):


{
    "message": "Workflow run has no Temporal execution yet."
}
 

Example response (410, History unavailable):


{
    "message": "Workflow execution history is no longer available."
}
 

Example response (422, Invalid page token):


{
    "message": "Invalid page_token."
}
 

Example response (503, Temporal unavailable):


{
    "message": "Temporal is currently unavailable."
}
 

Request      

GET api/v1/workflows/{workflowId}/runs/{runId}/history

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

runId   integer     

The run id. Example: 1

Query Parameters

page_token   string  optional    

Opaque continuation token returned by the previous page. Omit for the first page. Must not be greater than 8192 characters.

page_size   integer  optional    

Maximum number of history events per page (1–1000). Defaults to the server-configured value. Must be at least 1. Must not be greater than 1000. Example: 200

Get the per-node steps of a workflow run

requires authentication

Projects the run's flat Temporal history into an ordered list of node executions (one per scheduling, so a looped node yields one step per iteration) for the step-through replay inspector: per node the status, timing, duration, decoded input/config/output and any error. Like the history endpoint the response is hybrid-routed and carries a source field (snapshot for terminal snapshotted runs, temporal for live reads). Node ids match the workflow blob's canvas node ids.

A failed step's error carries the failure's code (as type) and a bounded message, and never a stack trace. Where the failure came from a node's own code it also carries frames: the author's file relative to the package it belongs to, a 1-based line and column, and the function name where one was recorded, innermost first. Only frames inside a node package appear. frames is absent where the failure produced none, or where the artifact the run executed has no source map stored — which is the case for every run recorded before this existed.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/steps?include_payloads=1" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/steps"
);

const params = {
    "include_payloads": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Projected steps):


{
    "source": "snapshot",
    "run": {
        "status": "completed",
        "started_at": "2026-05-22T10:00:00Z",
        "completed_at": "2026-05-22T10:00:05Z"
    },
    "steps": [
        {
            "node_id": "node-1",
            "node_slug": "revenexx:http-request",
            "activity_type": "executeNode",
            "status": "completed",
            "attempt": 0,
            "scheduled_event_id": 5,
            "duration_ms": 812,
            "input": {},
            "output": {},
            "branch": null,
            "error": null
        }
    ]
}
 

Example response (200, A step that failed inside a node):


{
    "source": "snapshot",
    "run": {
        "status": "failed"
    },
    "steps": [
        {
            "node_id": "f-empty-text",
            "node_slug": "revenexx:deepl-translate",
            "activity_type": "executeNode",
            "status": "failed",
            "attempt": 0,
            "scheduled_event_id": 5,
            "duration_ms": 41,
            "input": {},
            "output": null,
            "branch": null,
            "error": {
                "message": "No text found to translate",
                "type": "MISSING_TEXT",
                "frames": [
                    {
                        "file": "@revenexx/integrations-nodes-core/src/nodes/DeeplTranslateNode.ts",
                        "line": 5,
                        "column": 11,
                        "name": "requireText"
                    }
                ]
            }
        }
    ]
}
 

Example response (404, Run not found):


{
    "message": "Not found."
}
 

Example response (409, Run not handed off to Temporal yet):


{
    "message": "Workflow run has no Temporal execution yet."
}
 

Example response (410, History unavailable):


{
    "message": "Workflow execution history is no longer available."
}
 

Example response (503, Temporal unavailable):


{
    "message": "Temporal is currently unavailable."
}
 

Request      

GET api/v1/workflows/{workflowId}/runs/{runId}/steps

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

runId   integer     

The run id. Example: 1

Query Parameters

include_payloads   string  optional    

Whether to decode and include each step's input/config/output payloads. Defaults to true. Example: true

Must be one of:
  • true
  • false
  • 1
  • 0

Get the current call stack of a running workflow

requires authentication

Issues the built-in __stack_trace query against the workflow worker and returns the formatted stack trace as plain text. Only meaningful while the run is still executing; terminal runs return 409.

This is where the workflow is suspended, not where a step failed. For the latter, a failed step's error.frames on the steps endpoint names the line the node's author wrote.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/stack-trace" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/stack-trace"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Running):


{
    "stack_trace": "coroutine ..."
}
 

Example response (404, Run not found):


{
    "message": "Not found."
}
 

Example response (409, Run not handed off to Temporal yet):


{
    "message": "Workflow run has no Temporal execution yet."
}
 

Example response (409, Run is no longer running):


{
    "message": "Workflow run is no longer running."
}
 

Example response (410, Execution unknown to Temporal):


{
    "message": "Workflow execution history is no longer available."
}
 

Example response (503, Temporal unavailable):


{
    "message": "Temporal is currently unavailable."
}
 

Request      

GET api/v1/workflows/{workflowId}/runs/{runId}/stack-trace

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

runId   integer     

The run id. Example: 1

Cancel a running workflow run

requires authentication

Requests graceful cancellation of a still-running run: Temporal delivers a cancellation signal to the workflow, which may run cleanup before closing. The run stays running until the next status reconcile flips it to cancelled, so this returns HTTP 202.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/cancel" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"reason\": \"Superseded by a newer run\"
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/cancel"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "reason": "Superseded by a newer run"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (202, Cancellation requested):


{
    "id": 1,
    "workflow_id": 1,
    "status": "running"
}
 

Example response (404, Run not found):


{
    "message": "Not found."
}
 

Example response (409, Run already terminal):


{
    "message": "Workflow run is no longer running."
}
 

Example response (503, Temporal unavailable):


{
    "message": "Temporal is currently unavailable."
}
 

Request      

POST api/v1/workflows/{workflowId}/runs/{runId}/cancel

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

runId   integer     

The run id. Example: 1

Body Parameters

reason   string  optional    

Optional human-readable reason recorded on the Temporal execution and in the audit trail. Must not be greater than 255 characters. Example: Superseded by a newer run

Terminate a running workflow run

requires authentication

Forcefully terminates a still-running run with no cleanup and flips it to terminated immediately.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/terminate" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"reason\": \"Superseded by a newer run\"
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/terminate"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "reason": "Superseded by a newer run"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200, Terminated):


{
    "id": 1,
    "workflow_id": 1,
    "status": "terminated"
}
 

Example response (404, Run not found):


{
    "message": "Not found."
}
 

Example response (409, Run already terminal):


{
    "message": "Workflow run is no longer running."
}
 

Example response (503, Temporal unavailable):


{
    "message": "Temporal is currently unavailable."
}
 

Request      

POST api/v1/workflows/{workflowId}/runs/{runId}/terminate

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

runId   integer     

The run id. Example: 1

Body Parameters

reason   string  optional    

Optional human-readable reason recorded on the Temporal execution and in the audit trail. Must not be greater than 255 characters. Example: Superseded by a newer run

Re-run a previous workflow run

requires authentication

Starts a fresh run from the inputs (trigger payload + trigger handle) of an existing run, linking the new run to its source via retried_from_run_id. The source run may be in any state; the re-run always executes against the workflow's currently-built bundle and runs asynchronously.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/retry" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/retry"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (0, Workflow not runnable):


{
    "message": "Workflow bundle is not ready (build_"
}
 

Example response (202, Accepted):


{
    "id": 2,
    "workflow_id": 1,
    "status": "running",
    "retried_from_run_id": 1
}
 

Example response (404, Run not found):


{
    "message": "Not found."
}
 

Example response (422, Unresolved manifest issues (PO-186)):


{
    "message": "Workflow configuration has unresolved issues and cannot run. Fix the reported fields and try again.",
    "errors": {
        "blob.nodes.0.config.credentials": [
            "Required config field 'credentials' is missing."
        ]
    }
}
 

Example response (422, Trigger no longer resolvable):


{
    "message": "No active trigger with handle ... exists on this workflow."
}
 

Example response (503, Worker unavailable):


{
    "message": "Workflow worker is unavailable."
}
 

Request      

POST api/v1/workflows/{workflowId}/runs/{runId}/retry

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

runId   integer     

The id of the run to re-run. Example: 1

Resume a failed workflow run from its failed step

requires authentication

Resets the underlying Temporal execution to the workflow-task boundary before the failed step and re-drives it: already-completed steps keep their results, the failed step and everything downstream re-execute. The resumed run is tracked as a new run linked via resumed_from_run_id.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/resume" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"reason\": \"Superseded by a newer run\"
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1/runs/1/resume"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "reason": "Superseded by a newer run"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (202, Accepted):


{
    "id": 2,
    "workflow_id": 1,
    "status": "running",
    "resumed_from_run_id": 1
}
 

Example response (404, Run not found):


{
    "message": "Not found."
}
 

Example response (409, Run not resumable state):


{
    "message": "Only a failed run can be resumed."
}
 

Example response (409, Already resumed):


{
    "message": "Workflow run has already been resumed."
}
 

Example response (422, No reset point):


{
    "message": "Workflow run cannot be resumed: no suitable reset point in its history."
}
 

Example response (503, Temporal unavailable):


{
    "message": "Temporal is currently unavailable."
}
 

Request      

POST api/v1/workflows/{workflowId}/runs/{runId}/resume

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

workflowId   integer     

The workflow id. Example: 1

runId   integer     

The id of the failed run to resume. Example: 1

Body Parameters

reason   string  optional    

Optional human-readable reason recorded on the Temporal execution and in the audit trail. Must not be greater than 255 characters. Example: Superseded by a newer run

Workflows

Per-tenant workflow definitions. Every endpoint is scoped to the tenant resolved by the resolve.tenant middleware (from the JWT or X-Tenant-Id header). Cross-tenant access deliberately returns 404 so that the API does not leak the existence of resources owned by other tenants.

List workflows

requires authentication

Returns the workflows owned by the current tenant, ordered by name.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "data": [
        {
            "id": 1,
            "name": "Sync orders",
            "description": null,
            "blob_definition_version": "v0-draft",
            "blob": {},
            "active": true,
            "revision": 1,
            "created_at": "2026-05-06T10:00:00Z",
            "updated_at": "2026-05-06T10:00:00Z"
        }
    ]
}
 

Example response (401, Missing or invalid token):


{
    "message": "Unauthenticated."
}
 

Request      

GET api/v1/workflows

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Create a workflow

requires authentication

Persists a new workflow under the current tenant. The name must be unique within the tenant. The blob is validated against the schema registered for the supplied blob_definition_version.

Example request:
curl --request POST \
    "https://integrations.revenexx.com/api/v1/workflows" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"name\": \"Sync orders nightly\",
    \"description\": \"Pulls orders from the upstream API every night at 02:00 UTC.\",
    \"blob_definition_version\": \"v0-draft\",
    \"blob\": {
        \"nodes\": [],
        \"edges\": []
    },
    \"active\": true,
    \"execution_mode\": \"async_only\",
    \"triggers\": [
        {
            \"handle\": \"6ff8f7f6-1eb3-3525-be4a-3932c805afed\",
            \"type\": \"webhook\",
            \"name\": \"PSP webhook\",
            \"config\": {
                \"method\": \"POST\"
            },
            \"active\": true
        }
    ]
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "name": "Sync orders nightly",
    "description": "Pulls orders from the upstream API every night at 02:00 UTC.",
    "blob_definition_version": "v0-draft",
    "blob": {
        "nodes": [],
        "edges": []
    },
    "active": true,
    "execution_mode": "async_only",
    "triggers": [
        {
            "handle": "6ff8f7f6-1eb3-3525-be4a-3932c805afed",
            "type": "webhook",
            "name": "PSP webhook",
            "config": {
                "method": "POST"
            },
            "active": true
        }
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201, Created):


{
    "id": 1,
    "name": "Sync orders",
    "description": null,
    "blob_definition_version": "v0-draft",
    "blob": {},
    "active": true,
    "revision": 1,
    "warnings": [],
    "created_at": "2026-05-06T10:00:00Z",
    "updated_at": "2026-05-06T10:00:00Z"
}
 

Example response (422, Validation failed (structural, or manifest issues on an active save)):


{
    "message": "The given data was invalid.",
    "errors": {
        "blob.nodes.0.config.credentials": [
            "Required config field 'credentials' is missing."
        ]
    }
}
 

Request      

POST api/v1/workflows

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

Body Parameters

name   string     

Human-readable workflow name (unique per tenant). Must not be greater than 255 characters. Example: Sync orders nightly

description   string  optional    

Optional free-text description. Example: Pulls orders from the upstream API every night at 02:00 UTC.

blob_definition_version   string     

Identifier of the blob schema version. See GET /v1/schemas/workflow. Example: v0-draft

Must be one of:
  • v0-draft
blob   object  optional    

The workflow definition payload. Must match the schema for the supplied version.

active   boolean  optional    

Whether the workflow is active. Defaults to true when omitted. Example: true

execution_mode   string  optional    

Execution policy for run mode resolution. One of: async_only, sync_only, caller_decides. Defaults to async_only. Example: async_only

Must be one of:
  • async_only
  • sync_only
  • caller_decides
triggers   object[]  optional    

Optional set of trigger rows to sync alongside the workflow.

handle   string  optional    

Stable UUID referenced by the workflow blob edges. Must be unique per tenant. This field is required when triggers is present. Must be a valid UUID. Example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed

type   string  optional    

Trigger entry-point type. One of: manual, schedule, webhook, event. This field is required when triggers is present. Example: webhook

Must be one of:
  • manual
  • schedule
  • webhook
  • event
name   string  optional    

Optional human-readable label. Example: PSP webhook

config   object  optional    

Type-specific trigger configuration.

active   boolean  optional    

Whether the trigger is enabled. Defaults to true. Example: true

Response

Response Fields

warnings   object[]     

Non-blocking manifest cross-validation issues (PO-186). Empty when the blob resolves cleanly; each entry is {path, message} keyed by blob.…. An inactive workflow persists even with warnings; an active save/activation is rejected with 422 instead. Returned by create/update/show, not by the list endpoint.

Show a workflow

requires authentication

Returns a single workflow by id within the current tenant.

Example request:
curl --request GET \
    --get "https://integrations.revenexx.com/api/v1/workflows/1" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Success):


{
    "id": 1,
    "name": "Sync orders",
    "description": null,
    "blob_definition_version": "v0-draft",
    "blob": {},
    "active": true,
    "revision": 1,
    "warnings": [],
    "created_at": "2026-05-06T10:00:00Z",
    "updated_at": "2026-05-06T10:00:00Z"
}
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Request      

GET api/v1/workflows/{id}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

id   integer     

The workflow id. Example: 1

Replace a workflow

requires authentication

Performs a full-replace update of the workflow with the given id. All domain fields are required. The revision counter is incremented exactly when the supplied blob differs from the stored one, in which case the previous revision is snapshotted into the workflow history.

Example request:
curl --request PUT \
    "https://integrations.revenexx.com/api/v1/workflows/1" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production" \
    --data "{
    \"name\": \"Sync orders nightly\",
    \"description\": \"Pulls orders from the upstream API every night at 02:00 UTC.\",
    \"blob_definition_version\": \"v0-draft\",
    \"blob\": {
        \"nodes\": [],
        \"edges\": []
    },
    \"active\": true,
    \"execution_mode\": \"async_only\",
    \"triggers\": [
        {
            \"handle\": \"6ff8f7f6-1eb3-3525-be4a-3932c805afed\",
            \"type\": \"webhook\",
            \"name\": \"PSP webhook\",
            \"config\": {
                \"method\": \"POST\"
            },
            \"active\": true
        }
    ]
}"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};

let body = {
    "name": "Sync orders nightly",
    "description": "Pulls orders from the upstream API every night at 02:00 UTC.",
    "blob_definition_version": "v0-draft",
    "blob": {
        "nodes": [],
        "edges": []
    },
    "active": true,
    "execution_mode": "async_only",
    "triggers": [
        {
            "handle": "6ff8f7f6-1eb3-3525-be4a-3932c805afed",
            "type": "webhook",
            "name": "PSP webhook",
            "config": {
                "method": "POST"
            },
            "active": true
        }
    ]
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200, Updated):


{
    "id": 1,
    "name": "Sync orders",
    "description": null,
    "blob_definition_version": "v0-draft",
    "blob": {},
    "active": true,
    "revision": 2,
    "warnings": [],
    "created_at": "2026-05-06T10:00:00Z",
    "updated_at": "2026-05-06T11:00:00Z"
}
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Request      

PUT api/v1/workflows/{id}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

id   integer     

The workflow id. Example: 1

Body Parameters

name   string     

Human-readable workflow name (unique per tenant). Must not be greater than 255 characters. Example: Sync orders nightly

description   string  optional    

Optional free-text description. Example: Pulls orders from the upstream API every night at 02:00 UTC.

blob_definition_version   string     

Identifier of the blob schema version. See GET /v1/schemas/workflow. Example: v0-draft

Must be one of:
  • v0-draft
blob   object  optional    

The workflow definition payload. Must match the schema for the supplied version.

active   boolean     

Whether the workflow is active. Example: true

execution_mode   string     

Execution policy for run mode resolution. One of: async_only, sync_only, caller_decides. Example: async_only

Must be one of:
  • async_only
  • sync_only
  • caller_decides
triggers   object[]  optional    

Optional set of trigger rows to sync alongside the workflow.

handle   string  optional    

Stable UUID referenced by the workflow blob edges. Must be unique per tenant. This field is required when triggers is present. Must be a valid UUID. Example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed

type   string  optional    

Trigger entry-point type. One of: manual, schedule, webhook, event. This field is required when triggers is present. Example: webhook

Must be one of:
  • manual
  • schedule
  • webhook
  • event
name   string  optional    

Optional human-readable label. Example: PSP webhook

config   object  optional    

Type-specific trigger configuration.

active   boolean  optional    

Whether the trigger is enabled. Defaults to true. Example: true

Delete a workflow

requires authentication

Removes the workflow identified by {id} for the current tenant. Associated history rows are removed via cascade.

Example request:
curl --request DELETE \
    "https://integrations.revenexx.com/api/v1/workflows/1" \
    --header "Authorization: Bearer {ZITADEL_JWT}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "X-Tenant-Id: acme-production"
const url = new URL(
    "https://integrations.revenexx.com/api/v1/workflows/1"
);

const headers = {
    "Authorization": "Bearer {ZITADEL_JWT}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-Tenant-Id": "acme-production",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, Deleted):

Empty response
 

Example response (404, Not found):


{
    "message": "Not found."
}
 

Request      

DELETE api/v1/workflows/{id}

Headers

Authorization        

Example: Bearer {ZITADEL_JWT}

Content-Type        

Example: application/json

Accept        

Example: application/json

X-Tenant-Id        

Example: acme-production

URL Parameters

id   integer     

The workflow id. Example: 1