DonutworkHelp Center
Applications
Donutwork documentation

How to Build a DonutWork App

A programming-agnostic guide to architecting, registering, and deploying third-party integrations within the DonutWork ecosystem.

Building for DonutWork

This guide provides a conceptual and technical blueprint for developing third-party applications. The DonutWork framework is explicitly programming language agnostic, relying on standard HTTP/REST protocols and secure UI embedding to facilitate cross-system orchestration.


1. Application Registration

Before you begin development, you must register your application to obtain the necessary credentials for identity and platform access.

  • Client ID: Your application's public identifier.
  • Client Secret: A secure credential used to authorize your app to act on behalf of a company.
  • Redirect URI: The URL where DonutWork will send authorization codes.

Refer to the Detailed OAuth Guide for the technical authentication handshake.


2. Capability Discovery (The Manifest)

DonutWork uses a Discovery Pattern to understand what your application is capable of. Instead of DonutWork calling your app blindly, your app must "declare" its capabilities (Actions).

Declaring Capabilities

Your application should send a PUT request to the /integrations.json endpoint. The manifest declares proxied capabilities for workflows and API calls, and optional UI actions that launch your app from the backoffice.

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": "YOUR_APP_SHARED_TOKEN",
  "capabilities": [
    {
      "name": "ping",
      "description": "Test connection health",
      "endpoint": "https://your-app.com/api/ping.json",
      "method": "GET",
      "scopes": [
        "workflow",
        "api"
      ],
      "inputs": []
    },
    {
      "name": "provision_resource",
      "description": "Create a new resource",
      "endpoint": "https://your-app.com/api/provision.json",
      "method": "POST",
      "inputs": [
        {
          "name": "resource_name",
          "type": "string",
          "required": true
        }
      ]
    }
  ],
  "ui_actions": [
    {
      "key": "print_customer",
      "label": "Print customer data",
      "placements": [
        "customer:details"
      ]
    }
  ]
}

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": "YOUR_APP_SHARED_TOKEN",
  "capabilities": [
    {
      "name": "ping",
      "description": "Test connection health",
      "endpoint": "https://your-app.com/api/ping.json",
      "method": "GET",
      "scopes": [
        "workflow",
        "api"
      ],
      "inputs": []
    },
    {
      "name": "provision_resource",
      "description": "Create a new resource",
      "endpoint": "https://your-app.com/api/provision.json",
      "method": "POST",
      "inputs": [
        {
          "name": "resource_name",
          "type": "string",
          "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": "YOUR_APP_SHARED_TOKEN",
    "capabilities": [
      {
        "name": "ping",
        "description": "Test connection health",
        "endpoint": "https://your-app.com/api/ping.json",
        "method": "GET",
        "scopes": [
          "workflow",
          "api"
        ],
        "inputs": []
      },
      {
        "name": "provision_resource",
        "description": "Create a new resource",
        "endpoint": "https://your-app.com/api/provision.json",
        "method": "POST",
        "inputs": [
          {
            "name": "resource_name",
            "type": "string",
            "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": "YOUR_APP_SHARED_TOKEN",
  "capabilities": [
    {
      "name": "ping",
      "description": "Test connection health",
      "endpoint": "https://your-app.com/api/ping.json",
      "method": "GET",
      "scopes": [
        "workflow",
        "api"
      ],
      "inputs": []
    },
    {
      "name": "provision_resource",
      "description": "Create a new resource",
      "endpoint": "https://your-app.com/api/provision.json",
      "method": "POST",
      "inputs": [
        {
          "name": "resource_name",
          "type": "string",
          "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.

  • app_token: DonutWork will include this Bearer token in every Authorization header when it calls your application. This allows your app to verify that the request is coming from DonutWork.
  • capabilities: An array of allowed actions, each with a defined endpoint, method, and expected inputs.
  • capabilities[].scopes: Optional "workflow", "api", or an array with both. Without it, the existing unrestricted behavior applies. With it, only the specified channels can call the capability.
  • ui_actions: An optional array of backoffice actions. Each action has a unique key, a visible label, and a placements array. The only supported placement is customer:details. Omit the field to keep existing UI actions, or send [] to clear them. See the API reference for limits and validation rules.

Each PUT replaces capabilities, events, and app_token; include their current values when updating only ui_actions.

Actions on Customer Details

For an installed app with an HTTPS canvas_uri, each customer:details action appears in the customer page's More actions menu. Selecting it opens your app in its iframe with action_type set to the declared key and action_id set to the customer ID. DonutWork signs both parameters with the other launch parameters. Your app must verify the iframe signature before using them. The customer ID is context, not permission to access that customer's data; use your authorized API credentials for any data request.


3. Communication Pattern (The Action Logic)

When a user or a generic workflow triggers an action, DonutWork acts as a Secure Proxy.

  1. Trigger: DonutWork identifies the target Action (e.g., provision_resource).
  2. Identification: DonutWork retrieves the endpoint and app_token from your saved manifest.
  3. Execution: DonutWork sends an HTTP request to your endpoint, including the app_token in the header and the required data in the body.
  4. Response: Your application processes the request and returns a standard JSON response.

4. UI Framework (The Iframe)

DonutWork embeds your application's frontend inside its own dashboard via an Iframe. To ensure a smooth user experience and maintain security, you must adhere to the following policies:

Because your application is running in an Iframe context (cross-site), traditional session cookies may be blocked by modern browsers. You must configure your session cookies with:

  • SameSite=None
  • Secure=true

Security Headers

DonutWork allows Iframe embedding from registered applications. On your end, you should ensure your headers allow the platform to display your content. For a deep dive into signature verification, see Iframe Context & Security.

Content-Security-Policy: frame-ancestors 'self' https://hub.donutwork.com;
X-Frame-Options: ALLOW-FROM https://hub.donutwork.com

5. Event Synchronization (Webhooks)

To react to platform events in real-time (e.g., when a user pays a bill), you should register a Webhook.

  1. Endpoint: Your application should provide a dedicated HTTPS URL to receive POST payloads.
  2. Key Rotation: During registration, you will receive a token. Use this key to verify the X-SIGNATURE header on every incoming request.

6. Uninstall Callback (Company Data Cleanup)

When a company uninstalls your app from DonutWork, the platform sends a server-to-server callback to your configured uninstall_url.

  • Method: DELETE
  • Content-Type: application/x-www-form-urlencoded
  • Body:
    • company_id: Company identifier to delete.
    • verify: HMAC signature computed as HMAC_SHA256(company_id, client_secret).

Example payload:

DELETE /uninstall HTTP/1.1
Content-Type: application/x-www-form-urlencoded

company_id=699ecb44790ac4eea20e9cd3&verify=7c72b2...

Verification rule on your side:

$expected = hash_hmac('sha256', $companyId, $clientSecret);
$isValid = hash_equals($expected, $verify);

Implementation requirements:

  • Validate company_id and verify before deleting anything.
  • Delete only tenant-scoped data for that company_id.
  • Make the endpoint idempotent (repeated calls must not fail).
  • Return a 2xx response only when cleanup is completed (or already completed).

If your endpoint returns non-2xx, DonutWork will treat the uninstall callback as failed and local uninstall will be blocked.


Summary Checklist

  • Register App and obtain Client ID/Secret.
  • Implement OAuth 2.0 to obtain Platform Access Tokens.
  • Use PUT /integrations.json to declare your app's endpoints.
  • Implement your API endpoints to handle requests from the DonutWork proxy.
  • Configure SameSite=None and Secure cookies for your Iframe UI.
  • Register Webhooks for real-time event processing.
  • Implement DELETE uninstall_url with company_id + HMAC verify and tenant-safe cleanup.

On this page