DonutworkHelp Center
Api ReferenceDeveloper Tools
Donutwork documentation

Integrations & Capabilities

Orchestrate third-party application lifecycle, declare functional capabilities, and execute delegated actions.

Integrations & Capabilities API

The Integrations API lets a third-party application declare callable capabilities and expose them as managed actions inside Donutwork. Donutwork validates declared inputs, applies the configured application authentication, relays the request, and records operational metadata.


Lifecycle Management

Audit Active Integrations

Retrieve a paginated directory of all integrations currently authorized for your organization. This includes high-level connectivity status and identification handlers for each service.

GET
/2026-02-01/integrations.json
Required permissionintegrations:read

Path, query, and header parameters

sizeinteger· query
Maximum records per page (max 100).
pageinteger· query
Target page index.

Responses

Integration directory successfully retrieved.

{
  "entities": "Integration",
  "count": 1,
  "per_page": 100,
  "pages": {
    "current": 1,
    "max": 1
  },
  "elements": [
    {
      "id": "6908bffd...",
      "handler": "STRIPE_CONNECT",
      "name": "Stripe Global Payments",
      "status": "connected"
    }
  ]
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/integrations.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/integrations.json', {
  method: 'GET',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
  },
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/integrations.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
    ],
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

Declare Functional Capabilities

Register or update the actions (capabilities) exposed by your application. The declaration defines the input contract Donutwork validates before proxying a request to your service. You can also declare actions that appear in the backoffice and open your embedded app.

PUT
/2026-02-01/integrations.json
Required permissionintegrations:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "app_token": "ext_auth_token_9921",
  "capabilities": [
    {
      "name": "SYNC_INVENTORY",
      "method": "POST",
      "endpoint": "https://api.myapp.com/v1/sync",
      "scopes": [
        "workflow",
        "api"
      ],
      "inputs": [
        {
          "name": "sku",
          "type": "string",
          "required": true
        },
        {
          "name": "quantity",
          "type": "number",
          "required": true
        }
      ]
    }
  ],
  "ui_actions": [
    {
      "key": "print_customer",
      "label": "Print customer data",
      "placements": [
        "customer:details"
      ]
    }
  ]
}
capabilitiesarray
Proxied actions and their input schemas. Each action may specify scopes: workflow, api, or both. Omission clears the current capability list.
ui_actionsarray
Backoffice actions that open the app iframe at a supported placement. Omit to keep existing UI actions; send [] to remove them.
eventsarray
Declared integration events. Omission clears the current event list.
app_tokenstring
Authorization token used when Donutwork calls your service.

Responses

Capabilities successfully registered.

{
  "success": true,
  "message": "Capabilities declared successfully for MY_CUSTOM_APP",
  "app_handler": "MY_CUSTOM_APP",
  "actions_count": 1,
  "ui_actions_count": 1,
  "events_count": 0
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/integrations.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "app_token": "ext_auth_token_9921",
  "capabilities": [
    {
      "name": "SYNC_INVENTORY",
      "method": "POST",
      "endpoint": "https://api.myapp.com/v1/sync",
      "scopes": [
        "workflow",
        "api"
      ],
      "inputs": [
        {
          "name": "sku",
          "type": "string",
          "required": true
        },
        {
          "name": "quantity",
          "type": "number",
          "required": true
        }
      ]
    }
  ],
  "ui_actions": [
    {
      "key": "print_customer",
      "label": "Print customer data",
      "placements": [
        "customer:details"
      ]
    }
  ]
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/integrations.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "app_token": "ext_auth_token_9921",
    "capabilities": [
      {
        "name": "SYNC_INVENTORY",
        "method": "POST",
        "endpoint": "https://api.myapp.com/v1/sync",
        "scopes": [
          "workflow",
          "api"
        ],
        "inputs": [
          {
            "name": "sku",
            "type": "string",
            "required": true
          },
          {
            "name": "quantity",
            "type": "number",
            "required": true
          }
        ]
      }
    ],
    "ui_actions": [
      {
        "key": "print_customer",
        "label": "Print customer data",
        "placements": [
          "customer:details"
        ]
      }
    ]
  }),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/integrations.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'PUT',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{
  "app_token": "ext_auth_token_9921",
  "capabilities": [
    {
      "name": "SYNC_INVENTORY",
      "method": "POST",
      "endpoint": "https://api.myapp.com/v1/sync",
      "scopes": [
        "workflow",
        "api"
      ],
      "inputs": [
        {
          "name": "sku",
          "type": "string",
          "required": true
        },
        {
          "name": "quantity",
          "type": "number",
          "required": true
        }
      ]
    }
  ],
  "ui_actions": [
    {
      "key": "print_customer",
      "label": "Print customer data",
      "placements": [
        "customer:details"
      ]
    }
  ]
}',
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

capabilities[].scopes accepts "workflow", "api", or a non-empty array containing either or both. workflow makes the capability available to workflows; api allows calls through the integration proxy endpoints below. Omit scopes to preserve the existing unrestricted behavior, including Copilot access. An explicit scope list excludes Copilot.

ui_actions is separate from proxied capabilities. Each entry requires a unique key (starts with a letter; then letters, digits, _, . or -; at most 100 characters), a non-empty label (at most 80 characters), and a non-empty placements array. Up to 20 UI actions can be declared. Currently customer:details is the only supported placement. Omitting ui_actions keeps the existing declaration; [] removes all UI actions. Every PUT still replaces capabilities, events, and app_token, so include their current values if they must remain active. See Iframe Context & Security for launch parameters and verification.


Resource Discovery

Retrieve Specific Integration

Fetch an integration's API-visible proxied actions, declared UI actions, and events. The app token is not included.

GET
/2026-02-01/integrations/{appHandler}.json
Required permissionintegrations:read

Path, query, and header parameters

appHandlerstring· pathRequired
The unique string identifier for the integration.

Responses

Detailed integration profile retrieved.

{
  "id": "6908...",
  "app_handler": "MY_CUSTOM_APP",
  "actions": [
    {
      "name": "SYNC_INVENTORY",
      "method": "POST",
      "scopes": [
        "workflow",
        "api"
      ]
    }
  ],
  "ui_actions": [
    {
      "key": "print_customer",
      "label": "Print customer data",
      "placements": [
        "customer:details"
      ]
    }
  ],
  "events": []
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER.json', {
  method: 'GET',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
  },
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
    ],
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.


Execution (Capability Proxy)

Dispatch Delegated Action

Execute a capability on a remote integration. Donutwork applies the configured authorization and validates declared inputs before relaying the request to the target service.

POST
/2026-02-01/integrations/{appHandler}/{appAction}.json
Required permissionintegrations:write

Path, query, and header parameters

appHandlerstring· pathRequired
The integration identifier.
appActionstring· pathRequired
The capability to execute.

Responses

Action executed and external response relayed.

{
  "success": true,
  "integration": "SLACK_ENT",
  "action": "SEND_MESSAGE",
  "external_status": 200,
  "response": {
    "ok": true,
    "ts": "123456789.0001"
  }
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/APPACTION.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/APPACTION.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
  },
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/APPACTION.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
    ],
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

The request payload for action dispatch is dynamic. Required body fields are validated at runtime using the integration capability declaration (capabilities[].inputs[] with required: true).

Read Delegated Action

GET
/2026-02-01/integrations/{appHandler}/{appAction}.json
Required permissionintegrations:read

Path, query, and header parameters

appHandlerstring· pathRequired
The integration identifier.
appActionstring· pathRequired
The capability to execute.

Responses

Action executed using GET forwarding.

{
  "success": true,
  "integration": "SLACK_ENT",
  "action": "GET_CHANNEL_INFO",
  "external_status": 200,
  "response": {}
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/APPACTION.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/APPACTION.json', {
  method: 'GET',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
  },
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/APPACTION.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
    ],
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

Update Delegated Action

PUT
/2026-02-01/integrations/{appHandler}/{appAction}.json
Required permissionintegrations:write

Path, query, and header parameters

appHandlerstring· pathRequired
The integration identifier.
appActionstring· pathRequired
The capability to execute.

Responses

Action executed using PUT forwarding.

{
  "success": true,
  "integration": "SLACK_ENT",
  "action": "UPSERT_TEMPLATE",
  "external_status": 200,
  "response": {}
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/APPACTION.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/APPACTION.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
  },
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/APPACTION.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'PUT',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
    ],
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

Delete Delegated Action

DELETE
/2026-02-01/integrations/{appHandler}/{appAction}.json
Required permissionintegrations:write

Path, query, and header parameters

appHandlerstring· pathRequired
The integration identifier.
appActionstring· pathRequired
The capability to execute.

Responses

Action executed using DELETE forwarding.

{
  "success": true,
  "integration": "SLACK_ENT",
  "action": "DELETE_MESSAGE",
  "external_status": 200,
  "response": {}
}

Request examples

curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/APPACTION.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/APPACTION.json', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
  },
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/APPACTION.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'DELETE',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
    ],
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

Emit Integration Event

Emit an application-scoped event so workflows and event listeners can react to integration activity.

POST
/2026-02-01/integrations/{appHandler}/events/{eventName}/emit.json
Required permissionintegration_events:write

Path, query, and header parameters

appHandlerstring· pathRequired
Application handler associated with the integration.
eventNamestring· pathRequired
Declared event name in `<AppHandler>.<EventName>` format.

Request Body

JSON
{
  "metadata": {
    "message_id": "msg_001",
    "status": "sent"
  },
  "entity_id": "SLACK_ENT"
}
metadataobject
Event payload metadata object.
entity_idstring
Entity identifier. If omitted, the app handler is used.

Responses

Event accepted and dispatched.

{
  "success": true,
  "app_handler": "SLACK",
  "event": "Slack.MESSAGE_SENT",
  "queued_workflows": 1
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/events/EVENTNAME/emit.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "metadata": {
    "message_id": "msg_001",
    "status": "sent"
  },
  "entity_id": "SLACK_ENT"
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/events/EVENTNAME/emit.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "metadata": {
      "message_id": "msg_001",
      "status": "sent"
    },
    "entity_id": "SLACK_ENT"
  }),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/integrations/APPHANDLER/events/EVENTNAME/emit.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{
  "metadata": {
    "message_id": "msg_001",
    "status": "sent"
  },
  "entity_id": "SLACK_ENT"
}',
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

Technical Implementation

On this page