DonutworkHelp Center
Documentazione Donutwork

Customers

Manage customer lifecycle, professional profiles, and organizational associations.

Customers

The Customer API is the central hub for managing your client base. It allows you to synchronize your internal CRM data, manage billing profiles, and monitor subscription health across your organization.


List All Customers

Retrieve a paginated collection of customers associated with your organization. This endpoint is ideal for dashboard overviews and batch synchronization processes.

GET
/2026-02-01/customers.json
Required permissioncustomers:read

Path, query, and header parameters

sizeinteger· query
The number of customer records to return per page (maximum 100).
pageinteger· query
The specific results page to retrieve.

Responses

A paginated list of customers.

{
  "entities": "Customer",
  "count": 150,
  "per_page": 100,
  "pages": {
    "current": 1,
    "max": 2
  },
  "elements": [
    {
      "id": "67248941cbe2b1ff8b099a6c",
      "external_id": "CRM-UID-9921",
      "company_name": "Global Logistics S.A.",
      "email": "billing@globallogistics.com"
    }
  ]
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/customers.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers.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/customers.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.


Create a New Customer

Initialize a new customer profile. This is the prerequisite step for attaching subscriptions or processing one-time charges. High-quality data at this stage ensures accurate tax calculation and communication delivery.

POST
/2026-02-01/customers.json
Required permissioncustomers:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "customer": {
    "company_name": "Acme Corporation",
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane.doe@acme.com",
    "externalId": "CRM-UID-9921",
    "address": {
      "country": "US",
      "city": "San Francisco",
      "address": "123 Market St"
    }
  }
}
customer.company_namestringRequired
Official legal name of the organization.
customer.emailstringRequired
Primary contact email for billing and notifications.
customer.first_namestringRequired
Contact person's first name.
customer.last_namestringRequired
Contact person's last name.
customer.address.countrystringRequired
ISO 3166-1 alpha-2 country code (e.g., US, IT).
customer.externalIdstring
Your internal system identifier for cross-referencing.
customer.address.citystring
City of residence or incorporation.
customer.address.addressstring
Street address including number.
customer.address.postal_codestring
Postal or ZIP code.
customer.address.statestring
State, province, or region.
customer.address.vat_numberstring
Tax identifier for business entities.
customer.propertiesarray
Custom metadata fields for specialized integration logic.
customer.partner.idstring
Donutwork identifier for a referring partner.

Responses

Customer profile successfully created.

{
  "customer": {
    "id": "67c9...",
    "partner": null
  }
}

A profile with this email or externalId already exists.

{
  "error": "A customer already exists with this email"
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/customers.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "customer": {
    "company_name": "Acme Corporation",
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane.doe@acme.com",
    "externalId": "CRM-UID-9921",
    "address": {
      "country": "US",
      "city": "San Francisco",
      "address": "123 Market St"
    }
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "customer": {
      "company_name": "Acme Corporation",
      "first_name": "Jane",
      "last_name": "Doe",
      "email": "jane.doe@acme.com",
      "externalId": "CRM-UID-9921",
      "address": {
        "country": "US",
        "city": "San Francisco",
        "address": "123 Market St"
      }
    }
  }),
});
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/customers.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 => '{
  "customer": {
    "company_name": "Acme Corporation",
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane.doe@acme.com",
    "externalId": "CRM-UID-9921",
    "address": {
      "country": "US",
      "city": "San Francisco",
      "address": "123 Market St"
    }
  }
}',
]);
$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.


Retrieve Customer Details

Fetch the complete profile for a specific customer. This includes organizational metadata, contact information, and summarized subscription status.

GET
/2026-02-01/customers/{customerId}.json
Required permissioncustomers:read

Path, query, and header parameters

customerIdstring· pathRequired
The unique Donutwork identifier for the customer.

Responses

Complete customer profile retrieved.

{
  "id": "672489...",
  "email": "jane.doe@acme.com",
  "company_name": "Acme Corporation",
  "externalId": "CRM-UID-9921"
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID.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/customers/CUSTOMERID.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.


Search by External Identifier

Locate a customer profile using your internal system's identifier (externalId). This is essential for maintaining synchronization between Donutwork and your primary CRM/ERP.

GET
/2026-02-01/customers/customer.json
Required permissioncustomers:read

Path, query, and header parameters

externalIdstring· queryRequired
The unique identifier from your internal system.

Responses

Customer profile found.

{
  "id": "672489...",
  "email": "jane.doe@acme.com",
  "externalId": "CRM-UID-9921"
}

No customer matches the provided externalId.

{
  "error": "This customer does not exist"
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/customers/customer.json?externalId=CRM-UID-9921' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/customer.json?externalId=CRM-UID-9921', {
  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/customers/customer.json?externalId=CRM-UID-9921');
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 Customer Profile

Modify an existing customer record. This endpoint supports partial updates (PATCH-style behavior); only the fields included in the request body will be modified. This is useful for updating contact information or adding organizational tags.

PUT
/2026-02-01/customers/{customerId}.json
Required permissioncustomers:write

Path, query, and header parameters

customerIdstring· pathRequired
The unique Donutwork identifier for the customer.

Request Body

JSON
{
  "customer": {
    "email": "updated.contact@acme.com",
    "tags": [
      "enterprise",
      "priority-support"
    ]
  }
}
customer.emailstring
The updated primary contact email.
customer.first_namestring
Updated first name.
customer.last_namestring
Updated last name.
customer.company_namestring
Updated legal company name.
customer.tagsarray
A collection of labels for internal classification and reporting.
customer.address.countrystring
Updated ISO country code.

Responses

Customer profile successfully updated.

{
  "customer": {
    "id": "6724..."
  }
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "customer": {
    "email": "updated.contact@acme.com",
    "tags": [
      "enterprise",
      "priority-support"
    ]
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "customer": {
      "email": "updated.contact@acme.com",
      "tags": [
        "enterprise",
        "priority-support"
      ]
    }
  }),
});
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/customers/CUSTOMERID.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 => '{
  "customer": {
    "email": "updated.contact@acme.com",
    "tags": [
      "enterprise",
      "priority-support"
    ]
  }
}',
]);
$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.


Decommission Customer Profile

Permanently remove a customer profile and all associated metadata. This action is destructive and irreversible. Ensure that all active subscriptions are terminated before choosing to decommission a profile.

DELETE
/2026-02-01/customers/{customerId}.json
Required permissioncustomers:write

Path, query, and header parameters

customerIdstring· pathRequired
The unique Donutwork identifier for the customer.

Responses

Deletion request accepted and queued for processing.

{
  "id": "67c9...",
  "deleted": true
}

Request examples

curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID.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/customers/CUSTOMERID.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.


Customer Properties (Metadata)

Extend the customer profile with custom key-value pairs. Properties are ideal for storing domain-specific data, integration identifiers, or specialized configuration flags that are not captured by standard profile fields.

Retrieve All Properties

Fetch the complete set of custom metadata associated with a customer.

GET
/2026-02-01/customers/{customerId}/properties.json
Required permissioncustomer_properties:read

Path, query, and header parameters

customerIdstring· pathRequired
The unique Donutwork identifier for the customer.

Responses

Collection of customer-specific properties retrieved.

{
  "count": 2,
  "type": "CustomerProperties",
  "elements": {
    "crm_segment": "enterprise",
    "onboarding_status": "completed"
  }
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/properties.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/properties.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/customers/CUSTOMERID/properties.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.

Atomic Update/Insert (Upsert) Properties

Update existing property values or append new keys to the customer's property collection. This endpoint performs an "upsert" operation; keys that do not exist will be created, and existing keys will have their values overwritten.

PUT
/2026-02-01/customers/{customerId}/properties.json
Required permissioncustomer_properties:write

Path, query, and header parameters

customerIdstring· pathRequired
The unique Donutwork identifier for the customer.

Request Body

JSON
{
  "CustomerProperties": {
    "support_tier": "platinum",
    "preferred_language": "en"
  }
}
CustomerPropertiesobjectRequired
An object containing the key-value pairs to persist.

Responses

Properties successfully updated.

{
  "customer": {
    "id": "6724..."
  },
  "CustomerProperties": {
    "support_tier": "platinum",
    "preferred_language": "en"
  }
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/properties.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "CustomerProperties": {
    "support_tier": "platinum",
    "preferred_language": "en"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/properties.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "CustomerProperties": {
      "support_tier": "platinum",
      "preferred_language": "en"
    }
  }),
});
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/customers/CUSTOMERID/properties.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 => '{
  "CustomerProperties": {
    "support_tier": "platinum",
    "preferred_language": "en"
  }
}',
]);
$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.


Generate a unique, short-lived URL for the Donutwork Hosted Payment Page. This secure interface allow customers to update their sensitive payment information (e.g., Credit Card, SEPA Mandate) without sensitive data passing through your servers.

GET
/2026-02-01/customers/{customerId}/payment_link.json
Required permissioncustomers:read

Path, query, and header parameters

customerIdstring· pathRequired
The unique Donutwork identifier for the customer.

Responses

Secure payment link generated.

{
  "url": "https://pay.donutwork.com/l/xxxxxxxxxx"
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/payment_link.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/payment_link.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/customers/CUSTOMERID/payment_link.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.

Remove Stored Payment Method

Remove a saved payment method from a customer billing profile. The paymentId path value must be the stored payment record id.

DELETE
/2026-02-01/customers/{customerId}/payment_methods/{paymentId}.json
Required permissioncustomers:write

Path, query, and header parameters

customerIdstring· pathRequired
The unique Donutwork identifier for the customer.
paymentIdstring· pathRequired
The stored payment record id to remove.

Responses

Payment method removed from the customer billing profile.

{
  "id": "seti_123",
  "deleted": true
}

The customer or payment method does not exist.

{
  "error": "This payment method does not exists for this customer"
}

Request examples

curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/payment_methods/PAYMENTID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/payment_methods/PAYMENTID.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/customers/CUSTOMERID/payment_methods/PAYMENTID.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.


Subscription Management

Manage recurring billing cycles and plan associations for your customers. Subscriptions automate the generation of invoices and payment processing based on predefined logic.

List Active and Historical Subscriptions

Retrieve a collection of all subscription entities associated with a customer, including active, canceled, and past-due states.

GET
/2026-02-01/customers/{customerId}/subscriptions.json
Required permissioncustomer_subscriptions:read

Path, query, and header parameters

customerIdstring· pathRequired
The unique Donutwork identifier for the customer.

Responses

A list of customer subscriptions.

{
  "type": "CustomerSubscriptions",
  "elements": [
    {
      "id": "sub_123",
      "name": "Enterprise SaaS Plan",
      "status": "success",
      "next_renew": "2026-04-01",
      "price": {
        "amount": 499,
        "vat": {
          "percentage": 22,
          "name": "IVA"
        }
      }
    }
  ]
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions.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/customers/CUSTOMERID/subscriptions.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.

Attach New Subscription

Provision a new subscription for a customer. This endpoint allows for runtime overrides of plan defaults, such as custom discounts and initial addon quantities.

POST
/2026-02-01/customers/{customerId}/subscriptions.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

customerIdstring· pathRequired
The unique Donutwork identifier for the customer.

Request Body

JSON
{
  "subscription": {
    "id": "PLAN_PREMIUM_V2",
    "taxes": "TAX_STANDARD_22",
    "global_discount": {
      "value": 15,
      "type": "percentage"
    },
    "addons": [
      {
        "element": "api_quota",
        "quantity": 10000
      }
    ]
  }
}
subscription.idstringRequired
The identifier of the target plan.
subscription.taxesstringRequired
The tax profile ID to apply to this subscription.
subscription.global_discount.valuenumber
The absolute or percentage value of the discount.
subscription.global_discount.typestring
The type of discount: 'percentage' or 'fixed'.
subscription.addonsarray
A list of addon overrides with their respective quantities.

Responses

Subscription successfully provisioned.

{
  "id": "sub_67c9...",
  "name": "Starter Monthly",
  "status": "success",
  "next_renew": "2026-04-28",
  "price": {
    "amount": 39,
    "vat": {
      "percentage": 22,
      "name": "VAT Italy"
    }
  },
  "metering": {
    "active": false,
    "balance": {
      "amount": 0,
      "vat": {
        "percentage": 22,
        "name": "VAT Italy"
      }
    }
  },
  "limits": [],
  "addons": [],
  "discount": {
    "value": 0,
    "type": "fixed",
    "until": null
  },
  "lifecycle_status": "active",
  "cancel_at_period_end": false,
  "jobId": "123456"
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "subscription": {
    "id": "PLAN_PREMIUM_V2",
    "taxes": "TAX_STANDARD_22",
    "global_discount": {
      "value": 15,
      "type": "percentage"
    },
    "addons": [
      {
        "element": "api_quota",
        "quantity": 10000
      }
    ]
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "subscription": {
      "id": "PLAN_PREMIUM_V2",
      "taxes": "TAX_STANDARD_22",
      "global_discount": {
        "value": 15,
        "type": "percentage"
      },
      "addons": [
        {
          "element": "api_quota",
          "quantity": 10000
        }
      ]
    }
  }),
});
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/customers/CUSTOMERID/subscriptions.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 => '{
  "subscription": {
    "id": "PLAN_PREMIUM_V2",
    "taxes": "TAX_STANDARD_22",
    "global_discount": {
      "value": 15,
      "type": "percentage"
    },
    "addons": [
      {
        "element": "api_quota",
        "quantity": 10000
      }
    ]
  }
}',
]);
$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.

jobId is the processing identifier returned by the platform for the subscription request.
If an identifier cannot be assigned, jobId is returned as false.

Detach Subscription

Detach a subscription from the customer profile.

DELETE
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The subscription ID to remove from the customer.

Responses

Subscription detached from customer profile.

{
  "id": "sub_67c9...",
  "deleted": true
}

Request examples

curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID.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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID.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.

Retrieve Subscription Status

Read the operational status of a specific customer subscription.

GET
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/status.json
Required permissioncustomer_subscriptions:read

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The subscription ID.

Responses

Current status for the selected subscription.

{
  "id": "sub_123",
  "status": "active",
  "next_renew": "2026-05-01"
}

Subscription does not exist for the specified customer.

{
  "error": "This subscription does not exists for this customer"
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/status.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/status.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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/status.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.

Modify Subscription Status

Manually transition a subscription between operational states. This is typically used for administrative overrides or manual churn management.

PUT
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/status.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The subscription ID.

Request Body

JSON
{
  "subscription": {
    "status": "dismiss"
  }
}
subscription.statusstringRequired
Target status. Supported values: `active`, `dismiss`.

Responses

Status successfully updated.

{
  "id": "sub_123",
  "status": "dismiss",
  "next_renew": null
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/status.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "subscription": {
    "status": "dismiss"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/status.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "subscription": {
      "status": "dismiss"
    }
  }),
});
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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/status.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 => '{
  "subscription": {
    "status": "dismiss"
  }
}',
]);
$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 Subscription Next Renewal

Move the next renewal boundary for an active customer subscription.

PUT
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/next-renewal.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The customer subscription ID.

Request Body

JSON
{
  "subscription": {
    "next_renew": "2026-08-01",
    "reason": "Manual billing alignment"
  }
}
subscription.next_renewstringRequired
New next renewal date in `YYYY-MM-DD` format. Must be today or a future date.
subscription.reasonstring
Optional operator note stored in the subscription amendment metadata.
subscription.idempotency_keystring
Optional idempotency key for safe retries.

Responses

Next renewal date updated and audit log emitted.

{
  "id": "sub_123",
  "status": "success",
  "previous_next_renew": "2026-05-01",
  "next_renew": "2026-08-01"
}

Invalid date format, invalid date, date in the past, or date too far in the future.

Subscription state does not allow manual renewal-date updates, or date is unchanged.

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/next-renewal.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "subscription": {
    "next_renew": "2026-08-01",
    "reason": "Manual billing alignment"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/next-renewal.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "subscription": {
      "next_renew": "2026-08-01",
      "reason": "Manual billing alignment"
    }
  }),
});
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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/next-renewal.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 => '{
  "subscription": {
    "next_renew": "2026-08-01",
    "reason": "Manual billing alignment"
  }
}',
]);
$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.


Advanced Subscription Lifecycle (Additive)

These endpoints extend subscription lifecycle management without replacing existing legacy routes. Use them when you need pause/resume, plan amendments, scheduled changes, and proration-aware updates.

Read Extended Lifecycle State

GET
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/lifecycle.json
Required permissioncustomer_subscriptions:read

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The subscription ID.

Responses

Extended lifecycle metadata for the subscription.

{
  "id": "sub_123",
  "status": "success",
  "lifecycle_status": "active",
  "cancel_at_period_end": false,
  "scheduled_change": null,
  "carryover_credit": 0
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle.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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle.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.

Pause, Resume, Reactivate, Cancel-at-Period-End

PUT
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/pause.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The subscription ID.

Request Body

JSON
{
  "lifecycle": {
    "idempotency_key": "lifecycle-evt-001"
  }
}
lifecycle.idempotency_keystring
Optional idempotency key for safe retries.

Responses

Lifecycle transition applied.

{
  "id": "sub_123",
  "lifecycle_status": "paused",
  "amendment": {
    "action": "pause",
    "status": "applied"
  }
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/pause.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "lifecycle": {
    "idempotency_key": "lifecycle-evt-001"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/pause.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lifecycle": {
      "idempotency_key": "lifecycle-evt-001"
    }
  }),
});
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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/pause.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 => '{
  "lifecycle": {
    "idempotency_key": "lifecycle-evt-001"
  }
}',
]);
$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.

Resume a paused subscription

PUT
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/resume.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The paused subscription ID.

Request Body

JSON
{
  "lifecycle": {
    "resume_at": "2026-10-01",
    "idempotency_key": "resume-sub-123-20261001"
  }
}
lifecycle.resume_atdate
Optional future resume date. Omit to resume now and re-anchor renewal to today.
lifecycle.idempotency_keystring
Stable key for an exact logical retry.

Responses

Subscription resumed or scheduled according to the supplied date.

{
  "id": "sub_123",
  "lifecycle": {
    "status": "active"
  },
  "cancel_at_period_end": false,
  "amendment": {
    "action": "resume",
    "status": "applied"
  }
}

Current lifecycle state does not allow resume.

{
  "error": "Subscription cannot be resumed from its current state"
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/resume.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "lifecycle": {
    "resume_at": "2026-10-01",
    "idempotency_key": "resume-sub-123-20261001"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/resume.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lifecycle": {
      "resume_at": "2026-10-01",
      "idempotency_key": "resume-sub-123-20261001"
    }
  }),
});
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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/resume.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 => '{
  "lifecycle": {
    "resume_at": "2026-10-01",
    "idempotency_key": "resume-sub-123-20261001"
  }
}',
]);
$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.

Cancel at period end

PUT
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/cancel-at-period-end.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The active subscription ID.

Request Body

JSON
{
  "lifecycle": {
    "reason": "Customer requested cancellation",
    "idempotency_key": "cancel-sub-123"
  }
}
lifecycle.reasonstring
Operator-visible cancellation reason.
lifecycle.idempotency_keystring
Stable key for an exact logical retry.

Responses

Cancellation scheduled for the current renewal boundary.

{
  "id": "sub_123",
  "cancel_at_period_end": true,
  "lifecycle": {
    "status": "cancel_pending"
  },
  "amendment": {
    "action": "cancel_at_period_end",
    "status": "applied"
  }
}

Subscription state cannot be scheduled for cancellation.

{
  "error": "Subscription cannot be cancelled at period end from its current state"
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/cancel-at-period-end.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "lifecycle": {
    "reason": "Customer requested cancellation",
    "idempotency_key": "cancel-sub-123"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/cancel-at-period-end.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lifecycle": {
      "reason": "Customer requested cancellation",
      "idempotency_key": "cancel-sub-123"
    }
  }),
});
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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/cancel-at-period-end.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 => '{
  "lifecycle": {
    "reason": "Customer requested cancellation",
    "idempotency_key": "cancel-sub-123"
  }
}',
]);
$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.

Undo a scheduled cancellation

PUT
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/undo-cancel-at-period-end.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The cancel-pending subscription ID.

Request Body

JSON
{
  "lifecycle": {
    "reason": "Customer retained",
    "idempotency_key": "undo-cancel-sub-123"
  }
}
lifecycle.reasonstring
Operator-visible reason for restoring renewal.
lifecycle.idempotency_keystring
Stable key for an exact logical retry.

Responses

Scheduled cancellation removed before the renewal boundary.

{
  "id": "sub_123",
  "cancel_at_period_end": false,
  "lifecycle": {
    "status": "active"
  },
  "amendment": {
    "action": "undo_cancel_at_period_end",
    "status": "applied"
  }
}

No reversible period-end cancellation exists.

{
  "error": "Subscription is not pending cancellation"
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/undo-cancel-at-period-end.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "lifecycle": {
    "reason": "Customer retained",
    "idempotency_key": "undo-cancel-sub-123"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/undo-cancel-at-period-end.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lifecycle": {
      "reason": "Customer retained",
      "idempotency_key": "undo-cancel-sub-123"
    }
  }),
});
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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/undo-cancel-at-period-end.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 => '{
  "lifecycle": {
    "reason": "Customer retained",
    "idempotency_key": "undo-cancel-sub-123"
  }
}',
]);
$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.

Manual renewal retry and expired-subscription reactivation are documented separately below.

Pause/resume billing behavior:

  • No retroactive automatic billing is created when resuming after a long pause.
  • If resumed two months later (for example pause on May 1, 2026 and resume on July 1, 2026), renewal is re-anchored to the resume date (or future resume_at if provided).

Undo-cancel behavior:

  • If a subscription is cancel_pending, you can revert it to active before renewal boundary.
  • Plan/addons remain the current ones on the subscription; cancellation flag is removed.

Retry a Past-Due Renewal

Queue one immediate manual payment attempt for a subscription that is still past_due and inside its three-day recovery window. The retry keeps the original billing anchor. A successful payment moves the subscription to active; a failure leaves it past_due, or closes it as cancelled when the final recovery day is reached.

Only one manual retry charge can be created for the same subscription, billing cycle, and calendar day. Repeated requests are therefore safe from duplicate charges.

PUT
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/retry-renewal.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The past-due customer subscription ID.

Request Body

JSON
{
  "lifecycle": {}
}

Responses

The idempotent renewal retry was queued.

{
  "id": "sub_123",
  "status": "success",
  "lifecycle_status": "past_due",
  "job_id": "123456",
  "queued_at": 1788602400
}

The subscription is not past due, the recovery window expired, a charge is pending, or today's manual retry was already attempted.

The retry could not be queued. No payment attempt was started.

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/retry-renewal.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "lifecycle": {}
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/retry-renewal.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lifecycle": {}
  }),
});
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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/retry-renewal.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 => '{
  "lifecycle": {}
}',
]);
$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.

Reactivate an Expired Subscription

Use reactivation for a cancelled subscription or a legacy subscription whose renewal date is older than the three-day recovery window. Reactivation is different from resume: it immediately charges a full new billing period and anchors future renewals to the reactivation day. It never creates automatic retroactive charges for the expired interval.

Each request creates a normal, independently traceable charge. If a payment fails, the subscription remains expired and an operator can submit another reactivation later; that request creates a separate charge. A new request is rejected only while another charge for the same subscription is still pending.

PUT
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/reactivate.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The customer subscription ID.

Request Body

JSON
{
  "lifecycle": {}
}

Responses

The new period was paid and the subscription was reactivated from today.

{
  "id": "sub_123",
  "status": "success",
  "lifecycle_status": "active",
  "charge_id": "68c123...",
  "next_renew": "2026-10-20",
  "renewal_recovery": {
    "status": "reactivated",
    "reactivated_on": "2026-09-20"
  }
}

The subscription is not expired/cancelled or another renewal charge is still pending.

The reactivation charge could not be created or captured. A failed charge remains traceable and the subscription remains unchanged.

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/reactivate.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "lifecycle": {}
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/reactivate.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lifecycle": {}
  }),
});
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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/lifecycle/reactivate.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 => '{
  "lifecycle": {}
}',
]);
$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.

Failed-renewal lifecycle:

  • The first failed renewal moves the lifecycle from active to past_due and emits Customer.subscription.past_due once.
  • The platform retries once per billing day while preserving the original billing anchor.
  • A successful retry returns the lifecycle to active and emits Customer.subscription.recovered.
  • The third failed billing day sets both lifecycle and legacy status to cancelled, clears next_renew, and emits Customer.subscription.cancelled.
  • If the renewal job misses the final retry day, its next run closes the expired past_due cycle without creating a fourth charge.
  • A successful manual reactivation emits Customer.subscription.reactivated.

Change Plan or Addons with Immediate/Scheduled Application

PUT
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/change-plan.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "lifecycle": {
    "plan_id": "PLAN_ENTERPRISE_V3",
    "taxes": "TAX_STANDARD_22",
    "when": "period_end",
    "proration": true
  }
}
lifecycle.plan_idstringRequired
Target plan identifier.
lifecycle.taxesstring
Tax profile override.
lifecycle.whenstring
`immediate` (default) or `period_end`.
lifecycle.prorationboolean
Enable or disable proration logic.

Responses

Plan change accepted (immediate or scheduled).

{
  "id": "sub_123",
  "scheduled_change": {
    "type": "plan",
    "apply_on": "2026-06-01"
  },
  "amendment": {
    "action": "change_plan",
    "status": "applied"
  }
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMER_ID/subscriptions/SUBSCRIPTION_ID/lifecycle/change-plan.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "lifecycle": {
    "plan_id": "PLAN_ENTERPRISE_V3",
    "taxes": "TAX_STANDARD_22",
    "when": "period_end",
    "proration": true
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMER_ID/subscriptions/SUBSCRIPTION_ID/lifecycle/change-plan.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lifecycle": {
      "plan_id": "PLAN_ENTERPRISE_V3",
      "taxes": "TAX_STANDARD_22",
      "when": "period_end",
      "proration": true
    }
  }),
});
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/customers/CUSTOMER_ID/subscriptions/SUBSCRIPTION_ID/lifecycle/change-plan.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 => '{
  "lifecycle": {
    "plan_id": "PLAN_ENTERPRISE_V3",
    "taxes": "TAX_STANDARD_22",
    "when": "period_end",
    "proration": true
  }
}',
]);
$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.

PUT
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/change-addons.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "lifecycle": {
    "addons": [
      {
        "element": "workspace_seat",
        "quantity": 25
      }
    ],
    "when": "immediate",
    "proration": true
  }
}

Responses

Addon amendment applied with optional proration.

{
  "id": "sub_123",
  "carryover_credit": 18.5,
  "amendment": {
    "action": "change_addons",
    "status": "applied"
  }
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMER_ID/subscriptions/SUBSCRIPTION_ID/lifecycle/change-addons.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "lifecycle": {
    "addons": [
      {
        "element": "workspace_seat",
        "quantity": 25
      }
    ],
    "when": "immediate",
    "proration": true
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMER_ID/subscriptions/SUBSCRIPTION_ID/lifecycle/change-addons.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lifecycle": {
      "addons": [
        {
          "element": "workspace_seat",
          "quantity": 25
        }
      ],
      "when": "immediate",
      "proration": true
    }
  }),
});
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/customers/CUSTOMER_ID/subscriptions/SUBSCRIPTION_ID/lifecycle/change-addons.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 => '{
  "lifecycle": {
    "addons": [
      {
        "element": "workspace_seat",
        "quantity": 25
      }
    ],
    "when": "immediate",
    "proration": true
  }
}',
]);
$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.

Proration Preview (No Mutation)

POST
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/preview.json
Required permissioncustomer_subscriptions:read

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "lifecycle": {
    "action": "change_addons",
    "addons": [
      {
        "element": "workspace_seat",
        "quantity": 40
      }
    ]
  }
}

Responses

Economic preview of the amendment without persisting changes.

{
  "action": "change_addons",
  "proration": {
    "old_due": 499,
    "new_due": 679,
    "delta": 180,
    "direction": "debit"
  }
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMER_ID/subscriptions/SUBSCRIPTION_ID/lifecycle/preview.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "lifecycle": {
    "action": "change_addons",
    "addons": [
      {
        "element": "workspace_seat",
        "quantity": 40
      }
    ]
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMER_ID/subscriptions/SUBSCRIPTION_ID/lifecycle/preview.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lifecycle": {
      "action": "change_addons",
      "addons": [
        {
          "element": "workspace_seat",
          "quantity": 40
        }
      ]
    }
  }),
});
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/customers/CUSTOMER_ID/subscriptions/SUBSCRIPTION_ID/lifecycle/preview.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 => '{
  "lifecycle": {
    "action": "change_addons",
    "addons": [
      {
        "element": "workspace_seat",
        "quantity": 40
      }
    ]
  }
}',
]);
$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.

Amendment History

GET
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/amendments.json
Required permissioncustomer_subscriptions:read

Path, query, and header parameters

sizeinteger· query
Max entries per page (1-100).
pageinteger· query
Page index.

Responses

Paginated amendment stream for audit and support operations.

{
  "type": "CustomerSubscriptionAmendments",
  "elements": [
    {
      "action": "change_addons",
      "when": "immediate",
      "status": "applied"
    }
  ]
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMER_ID/subscriptions/SUBSCRIPTION_ID/amendments.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMER_ID/subscriptions/SUBSCRIPTION_ID/amendments.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/customers/CUSTOMER_ID/subscriptions/SUBSCRIPTION_ID/amendments.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.

Backward Compatibility

Lifecycle endpoints are additive and opt-in. Existing integrations can keep using current routes unchanged.

AreaLegacy ContractLifecycle Extension
Attach/DetachUnchangedStill valid
Status routeUnchanged payload keysAdditional lifecycle visibility
Addons routeUnchangedOptional proration-aware lifecycle change-addons
Usage/meteringUnchangedUnchanged
Existing panel/API flowsUnchangedNew actions available when explicitly called

Subscription Addons

Manage the quantity of modular features (addons) attached to a specific subscription. Addons allow for flexible, feature-based pricing.

Audit Addon Quantities

Retrieve a list of all addons currently active on a subscription along with their provisioned quantities.

GET
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/addons.json
Required permissioncustomer_subscriptions:read

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The subscription ID.

Responses

List of provisioned addons.

[
  {
    "element": "compute_units",
    "quantity": 10
  },
  {
    "element": "secondary_domains",
    "quantity": 2
  }
]

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/addons.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/addons.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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/addons.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.

Reconcile Addon Quantities

Update the provisioned quantities for one or more addons. This operation is additive or disruptive based on the provided quantity values.

PUT
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}/addons.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The subscription ID.

Request Body

JSON
{
  "addons": [
    {
      "element": "compute_units",
      "quantity": 15
    }
  ]
}
addonsarrayRequired
Addon collection to upsert.
addons[].elementstringRequired
Addon identifier.
addons[].quantityintegerRequired
Requested addon quantity.

Responses

Addon quantities successfully updated.

{
  "status": "updated"
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/addons.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "addons": [
    {
      "element": "compute_units",
      "quantity": 15
    }
  ]
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/addons.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "addons": [
      {
        "element": "compute_units",
        "quantity": 15
      }
    ]
  }),
});
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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID/addons.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 => '{
  "addons": [
    {
      "element": "compute_units",
      "quantity": 15
    }
  ]
}',
]);
$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.


Simulation Interface (Sandbox Only)

Accelerate development cycles by simulating subscription lifecycle events in non-production environments.

Simulate Success Event

Force a successful renewal or provisioning event for a subscription.

POST
/2026-02-01/customers/{customerId}/subscriptions/simulate/{subscriptionId}.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The subscription ID.

Responses

{
  "status": "simulated_success"
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/simulate/SUBSCRIPTIONID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/simulate/SUBSCRIPTIONID.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/customers/CUSTOMERID/subscriptions/simulate/SUBSCRIPTIONID.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.

Simulate Failure Event

Force a failed renewal or payment rejection event.

DELETE
/2026-02-01/customers/{customerId}/subscriptions/simulate/{subscriptionId}.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The subscription ID.

Responses

{
  "status": "simulated_failure"
}

Request examples

curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/simulate/SUBSCRIPTIONID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/simulate/SUBSCRIPTIONID.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/customers/CUSTOMERID/subscriptions/simulate/SUBSCRIPTIONID.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.


Metered Usage Tracking

For subscriptions with metering: true, consumption must be reported via these endpoints. Usage is aggregated and billed at the end of the current billing cycle.

Report Usage Event

Record a new consumption event (e.g., API requests, data processed, seats occupied). Each record requires a unique externalId to prevent idempotent replay errors.

POST
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}.json
Required permissioncustomer_subscriptions:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The subscription ID.

Request Body

JSON
{
  "usage": {
    "amount": 10.5,
    "value": 100,
    "value_type": "api_requests",
    "externalId": "evt_unique_uuid_123"
  }
}
usage.amountnumberRequired
The monetary value or unit count to bill.
usage.valuenumberRequired
The raw consumption value.
usage.value_typestringRequired
The metric identifier (e.g., 'storage_gb', 'api_calls').
usage.externalIdstringRequired
A unique identifier for idempotency.

Responses

Usage successfully recorded.

{
  "id": "usage_67c9...",
  "amount": 10.5
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "usage": {
    "amount": 10.5,
    "value": 100,
    "value_type": "api_requests",
    "externalId": "evt_unique_uuid_123"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "usage": {
      "amount": 10.5,
      "value": 100,
      "value_type": "api_requests",
      "externalId": "evt_unique_uuid_123"
    }
  }),
});
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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID.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 => '{
  "usage": {
    "amount": 10.5,
    "value": 100,
    "value_type": "api_requests",
    "externalId": "evt_unique_uuid_123"
  }
}',
]);
$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.

Audit Usage History

Retrieve a paginated history of all usage events reported for a specific subscription.

GET
/2026-02-01/customers/{customerId}/subscriptions/{subscriptionId}.json
Required permissioncustomer_subscriptions:read

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
subscriptionIdstring· pathRequired
The subscription ID.

Responses

Paginated usage history retrieved.

{
  "type": "CustomerSubscriptionUsages",
  "elements": []
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID.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/customers/CUSTOMERID/subscriptions/SUBSCRIPTIONID.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.


Ad-hoc Charges

Charges represent immediate, non-recurring billable events created independently of a subscription's billing cycle (e.g., setup fees, professional services).

Create One-Time Charge

Generate an ad-hoc charge. If the capture flag is set to true, the system will immediately attempt to authorize and capture the funds using the customer's primary payment method.

POST
/2026-02-01/customers/{customerId}/charges.json
Required permissioncharges:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.

Request Body

JSON
{
  "charge": {
    "capture": true,
    "services": [
      {
        "name": "Technical Consulting",
        "amount": 250,
        "taxProfile": "TAX_SERVICES_22"
      }
    ]
  }
}
charge.captureboolean
Whether to attempt immediate payment capture.
charge.servicesarrayRequired
A list of line items included in the charge.
charge.instructionstring
Payment instruction (e.g., 'sepa_debit', 'manual').

Responses

Charge created and payment status returned.

{
  "status": "success",
  "id": "ch_777",
  "captured": true
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/charges.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "charge": {
    "capture": true,
    "services": [
      {
        "name": "Technical Consulting",
        "amount": 250,
        "taxProfile": "TAX_SERVICES_22"
      }
    ]
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/charges.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "charge": {
      "capture": true,
      "services": [
        {
          "name": "Technical Consulting",
          "amount": 250,
          "taxProfile": "TAX_SERVICES_22"
        }
      ]
    }
  }),
});
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/customers/CUSTOMERID/charges.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 => '{
  "charge": {
    "capture": true,
    "services": [
      {
        "name": "Technical Consulting",
        "amount": 250,
        "taxProfile": "TAX_SERVICES_22"
      }
    ]
  }
}',
]);
$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.

Import Already Paid External Charges

POST /customers/{customerId}/charges/import.json with charges:write records a payment already collected by another system. It preserves the supplied net amounts, tax amounts, and dates, creates a paid one-shot charge, and activates the normal partner commission and event flow without calling a payment gateway or requiring a customer payment method.

Use the dedicated external charge import guide for the complete JSON body, cURL example, validation rules, 201/200 responses, conflicts, and retry handling. Do not use capture: true to register an already collected payment. Imported charges are identified by instruction: external and externally_managed: true in charge reads; the existing invoice endpoint can still update their invoice metadata.

List Organizational Charges

List charges across the authenticated company when reconciling payments independently from customer records.

GET
/2026-02-01/charges.json
Required permissioncharges:read

Path, query, and header parameters

pageinteger· query
Page number starting at 1.
sizeinteger· query
Records per page, up to 100.
chargeIdstring· query
Exact 24-character charge ID. Cannot be combined with date filters.
customerIdstring· query
Exact 24-character customer ID.
statusenum· query
pending, paid, failed, refunded, awaiting_response, or awaiting_payment.
typeenum· query
one-shot, subscription, or refund.
instructionenum· query
direct, manual, sepa_debit, or external (imported payments).
dateFromdate|timestamp· query
Inclusive lower creation bound; YYYY-MM-DD is interpreted in UTC.
dateTodate|timestamp· query
Inclusive upper creation bound; YYYY-MM-DD includes the full UTC day.

Responses

Paginated company charge inventory.

{
  "type": "Charges",
  "count": 2,
  "per_page": 100,
  "pages": {
    "current": 1,
    "max": 1
  },
  "elements": []
}

A filter or filter combination is invalid.

{
  "error": "chargeId cannot be combined with dateFrom/dateTo filters"
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/charges.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/charges.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/charges.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.

List Customer Charges

Retrieve all charges (including pending and failed attempts) associated with a specific customer profile.

GET
/2026-02-01/customers/{customerId}/charges.json
Required permissioncharges:read

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.

Responses

List of customer-specific charges.

{
  "type": "CustomerCharges",
  "elements": []
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/charges.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/charges.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/customers/CUSTOMERID/charges.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.

Charge Details and Reconciliation

Retrieve detailed metadata for a specific charge, including its internal lifecycle state and tax calculations.

GET
/2026-02-01/customers/{customerId}/charges/{chargeId}.json
Required permissioncharges:read

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
chargeIdstring· pathRequired
The unique charge identifier.

Responses

Full charge metadata.

{
  "id": "ch_777",
  "customer_id": "cust_123",
  "total": 305,
  "type": "one-shot",
  "instruction": "direct",
  "externally_managed": false,
  "paid_at": "2026-03-01T10:00:00+00:00",
  "status": "paid",
  "statuses": {
    "pending": false,
    "paid": true,
    "refunded": false
  },
  "date": {
    "date": "2026-03-01 10:00:00.000000",
    "timezone": "+00:00"
  }
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/charges/CHARGEID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/charges/CHARGEID.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/customers/CUSTOMERID/charges/CHARGEID.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.

External Invoice Synchronization

Manage links to external accounting systems (e.g., Fatture in Cloud) for each charge.

Retrieve Invoice Metadata

GET
/2026-02-01/customers/{customerId}/charges/{chargeId}/invoice.json
Required permissioncharges:read

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
chargeIdstring· pathRequired
The unique charge identifier.

Responses

Invoice synchronization details.

{
  "number": "INV-2026-001",
  "date": "2026-03-01",
  "source": "accounting-system",
  "link": "https://docs.acme.com/inv/123.pdf"
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/charges/CHARGEID/invoice.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/charges/CHARGEID/invoice.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/customers/CUSTOMERID/charges/CHARGEID/invoice.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.

Persist Invoice Metadata

PUT
/2026-02-01/customers/{customerId}/charges/{chargeId}/invoice.json
Required permissioncharges:write

Path, query, and header parameters

customerIdstring· pathRequired
The customer ID.
chargeIdstring· pathRequired
The unique charge identifier.

Request Body

JSON
{
  "invoice": {
    "number": "INV-2026-001",
    "date": "2026-03-01",
    "source": "erp-system",
    "link": "https://erp.example.com/invoices/991.pdf"
  }
}
invoice.numberstringRequired
Invoice number.
invoice.datestringRequired
Invoice issue date.
invoice.sourcestring
Originating source system identifier.
invoice.linkstring
Public or internal URL to invoice document.

Responses

{
  "status": "success"
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/charges/CHARGEID/invoice.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "invoice": {
    "number": "INV-2026-001",
    "date": "2026-03-01",
    "source": "erp-system",
    "link": "https://erp.example.com/invoices/991.pdf"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/customers/CUSTOMERID/charges/CHARGEID/invoice.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "invoice": {
      "number": "INV-2026-001",
      "date": "2026-03-01",
      "source": "erp-system",
      "link": "https://erp.example.com/invoices/991.pdf"
    }
  }),
});
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/customers/CUSTOMERID/charges/CHARGEID/invoice.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 => '{
  "invoice": {
    "number": "INV-2026-001",
    "date": "2026-03-01",
    "source": "erp-system",
    "link": "https://erp.example.com/invoices/991.pdf"
  }
}',
]);
$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.


Global Charge Resolution

Resolve charge details using only the global unique identifier. This is optimized for callback handlers and webhook reconciliation modules.

GET
/2026-02-01/charges/{chargeId}.json
Required permissioncharges:read

Path, query, and header parameters

chargeIdstring· pathRequired
The global unique identifier for the charge.

Responses

Charge metadata resolved globally.

{
  "id": "ch_777",
  "customer_id": "cust_123"
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/charges/CHARGEID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/charges/CHARGEID.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/charges/CHARGEID.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.

On this page