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.
customers:readPath, query, and header parameters
sizepageResponses
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.
customers:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"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_nameRequiredcustomer.emailRequiredcustomer.first_nameRequiredcustomer.last_nameRequiredcustomer.address.countryRequiredcustomer.externalIdcustomer.address.citycustomer.address.addresscustomer.address.postal_codecustomer.address.statecustomer.address.vat_numbercustomer.propertiescustomer.partner.idResponses
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.
customers:readPath, query, and header parameters
customerIdRequiredResponses
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.
customers:readPath, query, and header parameters
externalIdRequiredResponses
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.
customers:writePath, query, and header parameters
customerIdRequiredRequest Body
{
"customer": {
"email": "updated.contact@acme.com",
"tags": [
"enterprise",
"priority-support"
]
}
}customer.emailcustomer.first_namecustomer.last_namecustomer.company_namecustomer.tagscustomer.address.countryResponses
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.
customers:writePath, query, and header parameters
customerIdRequiredResponses
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.
customer_properties:readPath, query, and header parameters
customerIdRequiredResponses
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.
customer_properties:writePath, query, and header parameters
customerIdRequiredRequest Body
{
"CustomerProperties": {
"support_tier": "platinum",
"preferred_language": "en"
}
}CustomerPropertiesRequiredResponses
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.
Secure Hosted Payment Link
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.
customers:readPath, query, and header parameters
customerIdRequiredResponses
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.
customers:writePath, query, and header parameters
customerIdRequiredpaymentIdRequiredResponses
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.
customer_subscriptions:readPath, query, and header parameters
customerIdRequiredResponses
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.
customer_subscriptions:writePath, query, and header parameters
customerIdRequiredRequest Body
{
"subscription": {
"id": "PLAN_PREMIUM_V2",
"taxes": "TAX_STANDARD_22",
"global_discount": {
"value": 15,
"type": "percentage"
},
"addons": [
{
"element": "api_quota",
"quantity": 10000
}
]
}
}subscription.idRequiredsubscription.taxesRequiredsubscription.global_discount.valuesubscription.global_discount.typesubscription.addonsResponses
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.
customer_subscriptions:writePath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredResponses
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.
customer_subscriptions:readPath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredResponses
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.
customer_subscriptions:writePath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredRequest Body
{
"subscription": {
"status": "dismiss"
}
}subscription.statusRequiredResponses
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.
customer_subscriptions:writePath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredRequest Body
{
"subscription": {
"next_renew": "2026-08-01",
"reason": "Manual billing alignment"
}
}subscription.next_renewRequiredsubscription.reasonsubscription.idempotency_keyResponses
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
customer_subscriptions:readPath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredResponses
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
customer_subscriptions:writePath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredRequest Body
{
"lifecycle": {
"idempotency_key": "lifecycle-evt-001"
}
}lifecycle.idempotency_keyResponses
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
customer_subscriptions:writePath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredRequest Body
{
"lifecycle": {
"resume_at": "2026-10-01",
"idempotency_key": "resume-sub-123-20261001"
}
}lifecycle.resume_atlifecycle.idempotency_keyResponses
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
customer_subscriptions:writePath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredRequest Body
{
"lifecycle": {
"reason": "Customer requested cancellation",
"idempotency_key": "cancel-sub-123"
}
}lifecycle.reasonlifecycle.idempotency_keyResponses
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
customer_subscriptions:writePath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredRequest Body
{
"lifecycle": {
"reason": "Customer retained",
"idempotency_key": "undo-cancel-sub-123"
}
}lifecycle.reasonlifecycle.idempotency_keyResponses
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_atif provided).
Undo-cancel behavior:
- If a subscription is
cancel_pending, you can revert it toactivebefore 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.
customer_subscriptions:writePath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredRequest Body
{
"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.
customer_subscriptions:writePath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredRequest Body
{
"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
activetopast_dueand emitsCustomer.subscription.past_dueonce. - The platform retries once per billing day while preserving the original billing anchor.
- A successful retry returns the lifecycle to
activeand emitsCustomer.subscription.recovered. - The third failed billing day sets both lifecycle and legacy status to
cancelled, clearsnext_renew, and emitsCustomer.subscription.cancelled. - If the renewal job misses the final retry day, its next run closes the expired
past_duecycle without creating a fourth charge. - A successful manual reactivation emits
Customer.subscription.reactivated.
Change Plan or Addons with Immediate/Scheduled Application
customer_subscriptions:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"lifecycle": {
"plan_id": "PLAN_ENTERPRISE_V3",
"taxes": "TAX_STANDARD_22",
"when": "period_end",
"proration": true
}
}lifecycle.plan_idRequiredlifecycle.taxeslifecycle.whenlifecycle.prorationResponses
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.
customer_subscriptions:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"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)
customer_subscriptions:readPath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"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
customer_subscriptions:readPath, query, and header parameters
sizepageResponses
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.
| Area | Legacy Contract | Lifecycle Extension |
|---|---|---|
| Attach/Detach | Unchanged | Still valid |
| Status route | Unchanged payload keys | Additional lifecycle visibility |
| Addons route | Unchanged | Optional proration-aware lifecycle change-addons |
| Usage/metering | Unchanged | Unchanged |
| Existing panel/API flows | Unchanged | New 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.
customer_subscriptions:readPath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredResponses
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.
customer_subscriptions:writePath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredRequest Body
{
"addons": [
{
"element": "compute_units",
"quantity": 15
}
]
}addonsRequiredaddons[].elementRequiredaddons[].quantityRequiredResponses
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.
customer_subscriptions:writePath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredResponses
{
"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.
customer_subscriptions:writePath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredResponses
{
"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.
customer_subscriptions:writePath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredRequest Body
{
"usage": {
"amount": 10.5,
"value": 100,
"value_type": "api_requests",
"externalId": "evt_unique_uuid_123"
}
}usage.amountRequiredusage.valueRequiredusage.value_typeRequiredusage.externalIdRequiredResponses
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.
customer_subscriptions:readPath, query, and header parameters
customerIdRequiredsubscriptionIdRequiredResponses
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.
charges:writePath, query, and header parameters
customerIdRequiredRequest Body
{
"charge": {
"capture": true,
"services": [
{
"name": "Technical Consulting",
"amount": 250,
"taxProfile": "TAX_SERVICES_22"
}
]
}
}charge.capturecharge.servicesRequiredcharge.instructionResponses
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.
charges:readPath, query, and header parameters
pagesizechargeIdcustomerIdstatustypeinstructiondateFromdateToResponses
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.
charges:readPath, query, and header parameters
customerIdRequiredResponses
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.
charges:readPath, query, and header parameters
customerIdRequiredchargeIdRequiredResponses
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
charges:readPath, query, and header parameters
customerIdRequiredchargeIdRequiredResponses
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
charges:writePath, query, and header parameters
customerIdRequiredchargeIdRequiredRequest Body
{
"invoice": {
"number": "INV-2026-001",
"date": "2026-03-01",
"source": "erp-system",
"link": "https://erp.example.com/invoices/991.pdf"
}
}invoice.numberRequiredinvoice.dateRequiredinvoice.sourceinvoice.linkResponses
{
"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.
charges:readPath, query, and header parameters
chargeIdRequiredResponses
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.