Dukanam Mobile API
Introduction
Interactive documentation for the versioned Dukanam mobile API.
WhatsApp OTP is Dukanam's primary mobile authentication flow. The helper below intentionally uses the legacy password fallback to create a disposable Sanctum device token for interactive API documentation. The token is saved only in this browser and automatically supplied to every authenticated Try It Out request.
Authenticating requests
To authenticate requests, include an Authorization header with the value "Bearer {ACCESS_TOKEN}".
All authenticated endpoints are marked with a requires authentication badge in the documentation below.
WhatsApp OTP is the primary auth flow. The documentation helper above uses the legacy password fallback and automatically copies the returned data.token into every authenticated endpoint.
Authentication
Primary WhatsApp OTP authentication for mobile clients.
Request a WhatsApp verification code.
The response is intentionally identical for an existing account and a new phone number, so callers cannot use this endpoint to discover users. During the resend cooldown, supply your existing challenge_id to resume that same pending code without sending another message. Keep it private.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/auth/otp/request" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"phone\": \"9876543210\",
\"challenge_id\": \"00000000-0000-4000-8000-000000000001\"
}"
const url = new URL(
"https://dukanam.com/api/v1/auth/otp/request"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"phone": "9876543210",
"challenge_id": "00000000-0000-4000-8000-000000000001"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/auth/otp/request';
$response = $client->post(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'phone' => '9876543210',
'challenge_id' => '00000000-0000-4000-8000-000000000001',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (202):
{
"message": "If this number can receive WhatsApp, a verification code has been sent.",
"challenge_id": "00000000-0000-4000-8000-000000000001",
"expires_in_seconds": 600,
"resend_in_seconds": 60
}
Example response (422):
{
"message": "The given data was invalid.",
"errors": {
"phone": [
"Enter a valid 10-digit Indian mobile number."
]
}
}
Example response (429):
{
"message": "Please wait before requesting another verification code."
}
Example response (503):
{
"message": "WhatsApp verification is not available right now. Please use email and password instead."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
challenge_id
string
Opaque identifier required to verify the code.
expires_in_seconds
integer
Remaining whole seconds before this code expires, rounded down.
resend_in_seconds
integer
Remaining whole seconds in the resend cooldown; decreases when resuming an existing challenge.
Verify a WhatsApp code and sign in to the matching account.
After a valid OTP, an existing verified identity takes precedence. Otherwise, a unique normalized user.phone match signs in immediately without email, password, or account linking. Its verified identity is saved automatically if it has none; an existing different verified identity is preserved. Business and customer contact numbers are never used to select an account. Multiple matching user accounts return 422 with errors.code and no token or registration proof; offer password sign-in instead of choosing an account. Only an unknown phone receives a short-lived, one-time registration proof. Open account setup directly; do not show an account-linking step. Keep the proof secret and never put it in a URL.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/auth/otp/verify" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"challenge_id\": \"00000000-0000-4000-8000-000000000001\",
\"code\": \"123456\",
\"device_name\": \"Priya\'s phone\",
\"fcm_token\": \"eY2x9_example_fcm_registration_token\"
}"
const url = new URL(
"https://dukanam.com/api/v1/auth/otp/verify"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"challenge_id": "00000000-0000-4000-8000-000000000001",
"code": "123456",
"device_name": "Priya's phone",
"fcm_token": "eY2x9_example_fcm_registration_token"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/auth/otp/verify';
$response = $client->post(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'challenge_id' => '00000000-0000-4000-8000-000000000001',
'code' => '123456',
'device_name' => 'Priya\'s phone',
'fcm_token' => 'eY2x9_example_fcm_registration_token',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"user": {
"id": 1,
"name": "Shop Owner",
"email": "[email protected]",
"phone": null
},
"token": "1|example-mobile-token",
"default_business_id": 1
}
}
Example response (200):
{
"data": {
"requires_registration": true,
"registration_proof": "one-time-registration-proof"
}
}
Example response (422):
{
"message": "The given data was invalid.",
"errors": {
"code": [
"The verification code is invalid or has expired."
]
}
}
Example response (422, Ambiguous saved phone):
{
"message": "The given data was invalid.",
"errors": {
"code": [
"This number is saved on more than one account. Sign in with email and password to continue."
]
}
}
Example response (503):
{
"message": "WhatsApp verification is not available right now. Please use email and password instead."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
user
object
Present for an existing verified identity or unique normalized user.phone match.
token
string
Sanctum bearer token, present for an existing account.
default_business_id
integer|null
Last selected eligible business, otherwise the first eligible business by name; null if none. Present after sign-in.
requires_registration
boolean
Present and true only when the verified phone matches no account. Open account setup directly.
registration_proof
string
One-time secret for account setup; also accepted by the legacy enrollment endpoint.
Create an account after WhatsApp verification.
The proof is single-use and expires quickly. A recovery password is required for sensitive actions, while WhatsApp remains the primary sign-in route.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/auth/otp/register" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"challenge_id\": \"00000000-0000-4000-8000-000000000001\",
\"registration_proof\": \"one-time-registration-proof\",
\"name\": \"Priya Rao\",
\"business_name\": \"Priya Textiles\",
\"email\": \"[email protected]\",
\"password\": \"SecurePassword123!\",
\"device_name\": \"Priya\'s phone\",
\"fcm_token\": \"eY2x9_example_fcm_registration_token\",
\"referral_code\": \"K7M2QX9A\",
\"password_confirmation\": \"SecurePassword123!\",
\"acquisition\": {
\"utm_source\": \"google\",
\"utm_medium\": \"organic\",
\"utm_campaign\": \"launch\",
\"referrer_host\": \"www.google.com\"
}
}"
const url = new URL(
"https://dukanam.com/api/v1/auth/otp/register"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"challenge_id": "00000000-0000-4000-8000-000000000001",
"registration_proof": "one-time-registration-proof",
"name": "Priya Rao",
"business_name": "Priya Textiles",
"email": "[email protected]",
"password": "SecurePassword123!",
"device_name": "Priya's phone",
"fcm_token": "eY2x9_example_fcm_registration_token",
"referral_code": "K7M2QX9A",
"password_confirmation": "SecurePassword123!",
"acquisition": {
"utm_source": "google",
"utm_medium": "organic",
"utm_campaign": "launch",
"referrer_host": "www.google.com"
}
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/auth/otp/register';
$response = $client->post(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'challenge_id' => '00000000-0000-4000-8000-000000000001',
'registration_proof' => 'one-time-registration-proof',
'name' => 'Priya Rao',
'business_name' => 'Priya Textiles',
'email' => '[email protected]',
'password' => 'SecurePassword123!',
'device_name' => 'Priya\'s phone',
'fcm_token' => 'eY2x9_example_fcm_registration_token',
'referral_code' => 'K7M2QX9A',
'password_confirmation' => 'SecurePassword123!',
'acquisition' => ['utm_source' => 'google', 'utm_medium' => 'organic', 'utm_campaign' => 'launch', 'referrer_host' => 'www.google.com'],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"user": {
"id": 1,
"name": "Priya Rao",
"email": "[email protected]",
"phone": null
},
"business": {
"id": 1,
"name": "Priya Textiles",
"role": "owner",
"is_owner": true
},
"token": "1|example-mobile-token"
}
}
Example response (422):
{
"message": "The given data was invalid.",
"errors": {
"registration_proof": [
"Your verified phone session has expired. Request a new WhatsApp code."
]
}
}
Example response (503):
{
"message": "WhatsApp verification is not available right now. Please use email and password instead."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
business
object
is_owner
boolean
Always true for the newly registered account's workspace.
Link a newly verified WhatsApp number to an existing password account (legacy compatibility).
Clients must first obtain the same short-lived registration proof used for new account creation. The email/password check makes enrollment an explicit migration path instead of trusting mutable historical contact phone fields.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/auth/otp/link" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"challenge_id\": \"00000000-0000-4000-8000-000000000001\",
\"registration_proof\": \"one-time-registration-proof\",
\"email\": \"[email protected]\",
\"password\": \"SecurePassword123!\",
\"device_name\": \"Priya\'s phone\",
\"fcm_token\": \"eY2x9_example_fcm_registration_token\"
}"
const url = new URL(
"https://dukanam.com/api/v1/auth/otp/link"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"challenge_id": "00000000-0000-4000-8000-000000000001",
"registration_proof": "one-time-registration-proof",
"email": "[email protected]",
"password": "SecurePassword123!",
"device_name": "Priya's phone",
"fcm_token": "eY2x9_example_fcm_registration_token"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/auth/otp/link';
$response = $client->post(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'challenge_id' => '00000000-0000-4000-8000-000000000001',
'registration_proof' => 'one-time-registration-proof',
'email' => '[email protected]',
'password' => 'SecurePassword123!',
'device_name' => 'Priya\'s phone',
'fcm_token' => 'eY2x9_example_fcm_registration_token',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"user": {
"id": 1,
"name": "Shop Owner",
"email": "[email protected]",
"phone": null
},
"token": "1|example-mobile-token",
"default_business_id": 1
}
}
Example response (422):
{
"message": "The given data was invalid.",
"errors": {
"registration_proof": [
"Your verified phone session has expired. Request a new WhatsApp code."
]
}
}
Example response (503):
{
"message": "WhatsApp verification is not available right now. Please use email and password instead."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
default_business_id
integer|null
Last selected eligible business, otherwise the first eligible business by name; null if none.
Register an account and its first workspace.
The returned business includes role: owner and is_owner: true immediately,
without a follow-up workspace request. Ownership is determined by owner_user_id.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/auth/register" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"b\",
\"email\": \"[email protected]\",
\"phone\": \"i\",
\"business_name\": \"y\",
\"password\": \"pBNvYg\",
\"referral_code\": \"K7M2QX9A\",
\"acquisition\": {
\"utm_source\": \"google\",
\"utm_medium\": \"organic\",
\"utm_campaign\": \"launch\",
\"referrer_host\": \"www.google.com\"
},
\"device_name\": \"Scribe API Docs\",
\"fcm_token\": \"eY2x9_example_fcm_registration_token\"
}"
const url = new URL(
"https://dukanam.com/api/v1/auth/register"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "b",
"email": "[email protected]",
"phone": "i",
"business_name": "y",
"password": "pBNvYg",
"referral_code": "K7M2QX9A",
"acquisition": {
"utm_source": "google",
"utm_medium": "organic",
"utm_campaign": "launch",
"referrer_host": "www.google.com"
},
"device_name": "Scribe API Docs",
"fcm_token": "eY2x9_example_fcm_registration_token"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/auth/register';
$response = $client->post(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'b',
'email' => '[email protected]',
'phone' => 'i',
'business_name' => 'y',
'password' => 'pBNvYg',
'referral_code' => 'K7M2QX9A',
'acquisition' => ['utm_source' => 'google', 'utm_medium' => 'organic', 'utm_campaign' => 'launch', 'referrer_host' => 'www.google.com'],
'device_name' => 'Scribe API Docs',
'fcm_token' => 'eY2x9_example_fcm_registration_token',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"user": {
"id": 1,
"name": "Shop Owner",
"email": "[email protected]"
},
"business": {
"id": 1,
"name": "Owner Shop",
"role": "owner",
"is_owner": true
},
"token": "1|example-mobile-token"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
business
object
is_owner
boolean
Always true for the newly registered account's workspace.
Sign in with the password fallback and return the default business.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/auth/login" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"email\": \"[email protected]\",
\"password\": \"|]|{+-\",
\"remember\": false,
\"fcm_token\": \"v\",
\"device_name\": \"Scribe API Docs\"
}"
const url = new URL(
"https://dukanam.com/api/v1/auth/login"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"email": "[email protected]",
"password": "|]|{+-",
"remember": false,
"fcm_token": "v",
"device_name": "Scribe API Docs"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/auth/login';
$response = $client->post(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'email' => '[email protected]',
'password' => '|]|{+-',
'remember' => false,
'fcm_token' => 'v',
'device_name' => 'Scribe API Docs',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"user": {
"id": 1,
"name": "Shop Owner",
"email": "[email protected]",
"phone": null
},
"token": "1|example-mobile-token",
"default_business_id": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
default_business_id
integer|null
Last selected eligible business, otherwise the first eligible business by name; null if none.
Email a password reset link.
Always returns the same response so callers cannot discover registered email addresses.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/auth/forgot-password" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"email\": \"[email protected]\"
}"
const url = new URL(
"https://dukanam.com/api/v1/auth/forgot-password"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"email": "[email protected]"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/auth/forgot-password';
$response = $client->post(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'email' => '[email protected]',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"message": "If an account exists for that email address, a password reset link has been sent."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Reset an account password.
A successful reset revokes every mobile bearer token and sends a security notification.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/auth/reset-password" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"token\": \"reset-token\",
\"email\": \"[email protected]\",
\"password\": \"new-secure-password\",
\"password_confirmation\": \"new-secure-password\"
}"
const url = new URL(
"https://dukanam.com/api/v1/auth/reset-password"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"token": "reset-token",
"email": "[email protected]",
"password": "new-secure-password",
"password_confirmation": "new-secure-password"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/auth/reset-password';
$response = $client->post(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'token' => 'reset-token',
'email' => '[email protected]',
'password' => 'new-secure-password',
'password_confirmation' => 'new-secure-password',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"message": "Password reset successfully. Sign in with your new password."
}
Example response (422):
{
"message": "The given data was invalid.",
"errors": {
"email": [
"This password reset token is invalid."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Return the current user and active workspaces available under each plan and seat assignment.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/auth/me" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/auth/me"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/auth/me';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"user": {
"id": 1,
"name": "Shop Owner",
"email": "[email protected]",
"phone": null
},
"businesses": [
{
"id": 1,
"name": "My Shop",
"role": "owner",
"is_owner": true
}
],
"default_business_id": 1,
"workspaces": {
"limit": 1,
"used": 1,
"remaining": 0,
"can_create": false
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
businesses
object
is_owner
boolean
Whether owner_user_id matches the authenticated user.
default_business_id
integer|null
Last selected business if still accessible, otherwise the first eligible business by name; null when none is available.
workspaces
object
How many workspaces the account's plan covers and how many it already owns. A plan covers every workspace its owner holds, so this allowance is account-wide rather than per workspace.
limit
integer
Workspaces the current plan covers. 0 means unlimited.
used
integer
Active workspaces the account already owns. Workspaces it merely belongs to are not counted.
remaining
integer|null
Workspaces still available, or null when the limit is unlimited.
can_create
boolean
Whether POST /api/v1/businesses would succeed rather than return workspace_limit_reached.
Update or clear the current user's Firebase Cloud Messaging registration token.
requires authentication
Send null to remove the currently stored token.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/auth/fcm-token" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"fcm_token\": \"eY2x9_example_fcm_registration_token\"
}"
const url = new URL(
"https://dukanam.com/api/v1/auth/fcm-token"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"fcm_token": "eY2x9_example_fcm_registration_token"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/auth/fcm-token';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'fcm_token' => 'eY2x9_example_fcm_registration_token',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"message": "FCM token updated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
DELETE api/v1/auth/token
requires authentication
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/auth/token" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/auth/token"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/auth/token';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
DELETE api/v1/auth/tokens
requires authentication
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/auth/tokens" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/auth/tokens"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/auth/tokens';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Businesses
List the active workspaces available under the caller's current plan and seat assignment.
requires authentication
Ended paid subscriptions automatically fall back to Free Essentials. The ended subscription remains in history.
Business resources include invoice_allowance (limit, used, remaining, can_create, resets_at) and
owner-only free_transition (subscription_id, reason, requires_acknowledgement; null otherwise).
When acknowledgement is required, offer paid plans and Continue on Free via POST billing/continue-free.
Keep viewing, downloading, and exporting existing invoices available throughout this flow.
is_active describes workspace access, independently of paid subscription status.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
theme
string|null
Workspace preset key or canonical custom #RRGGBB. Null uses the default palette.
theme_colours
object
Effective workspace label, brand, brand_dark and soft colour tokens.
logo_url
string|null
Temporary signed object-storage URL when the logo is stored on S3. Refresh the resource after it expires.
gst
object
Registration capabilities: registered, can_charge, masters_enabled, workspace_enabled. Unregistered businesses have all four false. Hide GST navigation, masters, calculation controls and return-filing UI in that case; plan features alone do not establish GST eligibility.
is_owner
boolean
Whether owner_user_id matches the authenticated user, independent of the membership pivot.
Create a business for the signed-in account.
requires authentication
Any authenticated account can create its first or an additional business, even without an accessible workspace or while another workspace is in setup. The caller becomes its owner. Creation uses the same setup service as web registration: an active Free subscription, Walk-in Customer, and default payment accounts are created atomically. Existing memberships and the saved default business are unchanged; use the selection endpoint to change it. Continue guided onboarding from the language step using the returned ID. Supply contact phone, store type, and tax details through onboarding. Duplicate names are allowed and receive distinct slugs. Each successful request creates a new business; this endpoint is not idempotent.
Send skip_setup to create a workspace that is immediately usable, with
onboarding already marked complete and every step recorded as completed.
Nothing is guessed: only the name is set, and GST, address, and store
details are entered later through the settings endpoint. This is what the
web account menu does, because an account adding a second workspace has
already been through guided setup once. Omit it to keep the guided flow.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Priya Textiles\",
\"skip_setup\": true
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Priya Textiles",
"skip_setup": true
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Priya Textiles',
'skip_setup' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 2,
"name": "Priya Textiles",
"slug": "priya-textiles",
"role": "owner",
"is_owner": true,
"onboarding": {
"current_step": "language",
"completed_steps": [],
"skipped_steps": [],
"completed_at": null
},
"subscription": {
"status": "active",
"billing_interval": "monthly",
"provider": "manual",
"plan": {
"id": 1,
"name": "Free",
"slug": "free"
}
}
}
}
Example response (401):
{
"message": "Unauthenticated."
}
Example response (409):
{
"message": "Your plan covers 1 workspace.",
"code": "workspace_limit_reached",
"limit": 1,
"used": 1
}
Example response (422):
{
"message": "The name field is required.",
"errors": {
"name": [
"The name field is required."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
The standard business resource, including the new business ID, ownership, subscription, and onboarding state.
is_owner
boolean
Always true; ownership is assigned to the authenticated account.
onboarding
object
current_step
string
language for a guided business, or complete when skip_setup was sent.
Select the business to open by default on the next sign-in.
requires authentication
Only an active, plan-eligible membership can be selected. The preference is shared with web sign-in; existing browser sessions retain their current workspace. API requests always use the explicit business ID in their URL. Selection is allowed before onboarding completes so clients can switch out of an unfinished workspace. Follow data.business.onboarding afterward.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/select" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/select"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/select';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"default_business_id": 1,
"business": {
"id": 1,
"name": "My Shop",
"role": "owner",
"is_owner": true,
"onboarding": {
"current_step": "complete"
}
}
}
}
Example response (401):
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "Your role requires an eligible team or accountant plan."
}
Example response (404):
{
"message": "Not Found"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
default_business_id
integer
The selected business to open on the next sign-in.
business
object
The selected business, including the caller's role and onboarding state.
Show the workspace and its current subscription.
requires authentication
Ended paid subscriptions automatically fall back to Free Essentials. The ended subscription remains in history.
Free features and limits apply on both web and API; is_active describes workspace access.
Scheduled cancellations retain paid access until ends_at. A renewal due date alone does not end access.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
theme
string|null
Workspace preset key or custom #RRGGBB. Null uses the default palette.
theme_colours
object
Effective workspace label, brand, brand_dark and soft colour tokens, derived for readable custom accents.
logo_url
string|null
Temporary signed object-storage URL when the logo is stored on S3. Refresh the resource after it expires.
receiving_bank_account
object|null
Active receiving bank with institution, account_name, account_number, ifsc, branch and show_on_invoice. Returned only with accounting permission.
is_owner
boolean
Whether owner_user_id matches the authenticated user, independent of the membership pivot.
subscription
object
plan
object
limits
object
items
integer
Maximum catalogue item count; 0 means unlimited.
manual_sharing
object
enabled
boolean
Always true because manual sharing is available on every plan.
Update business settings.
requires authentication
The default_locale and manual-sharing template fields may only be changed by workspace owners
and admins. Manual sharing itself is available on every plan.
GSTIN is required for a regular or composition registration, and its first two digits must match state_code. An unregistered business has its GSTIN cleared. state_code must be an Indian state or union territory (96, 97 and 99 are refused), and default_place_of_supply refuses 99.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/settings" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"b\",
\"legal_name\": \"n\",
\"phone\": \"g\",
\"email\": \"[email protected]\",
\"gst_registration_type\": \"unregistered\",
\"gstin\": \"36ABCDE1234F1Z5\",
\"address_line_1\": \"w\",
\"address_line_2\": \"p\",
\"city\": \"w\",
\"state_code\": \"36\",
\"pincode\": \"569775\",
\"default_place_of_supply\": 1,
\"invoice_prefix\": \"HAWIOT\\/26-27\\/\",
\"prices_include_tax\": false,
\"authorized_signatory\": \"g\",
\"bank_details\": \"z\",
\"upi_id\": \"m\",
\"receiving_bank_account\": {
\"institution\": \"HDFC Bank\",
\"account_name\": \"Anika Stores\",
\"account_number\": \"001234567890\",
\"ifsc\": \"HDFC0000123\",
\"branch\": \"Pune\",
\"show_on_invoice\": true
},
\"theme\": \"#2563EB\",
\"custom_theme_colour\": \"#2563EB\",
\"default_locale\": \"en\",
\"invoice_share_message_template\": \"w\",
\"min_shelf_life_days\": 25,
\"short_expiry_action\": \"warn\",
\"notification_preferences\": [
{
\"enabled\": false,
\"days\": [
-364
],
\"threshold\": 6
}
]
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/settings"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "b",
"legal_name": "n",
"phone": "g",
"email": "[email protected]",
"gst_registration_type": "unregistered",
"gstin": "36ABCDE1234F1Z5",
"address_line_1": "w",
"address_line_2": "p",
"city": "w",
"state_code": "36",
"pincode": "569775",
"default_place_of_supply": 1,
"invoice_prefix": "HAWIOT\/26-27\/",
"prices_include_tax": false,
"authorized_signatory": "g",
"bank_details": "z",
"upi_id": "m",
"receiving_bank_account": {
"institution": "HDFC Bank",
"account_name": "Anika Stores",
"account_number": "001234567890",
"ifsc": "HDFC0000123",
"branch": "Pune",
"show_on_invoice": true
},
"theme": "#2563EB",
"custom_theme_colour": "#2563EB",
"default_locale": "en",
"invoice_share_message_template": "w",
"min_shelf_life_days": 25,
"short_expiry_action": "warn",
"notification_preferences": [
{
"enabled": false,
"days": [
-364
],
"threshold": 6
}
]
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/settings';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'b',
'legal_name' => 'n',
'phone' => 'g',
'email' => '[email protected]',
'gst_registration_type' => 'unregistered',
'gstin' => '36ABCDE1234F1Z5',
'address_line_1' => 'w',
'address_line_2' => 'p',
'city' => 'w',
'state_code' => '36',
'pincode' => '569775',
'default_place_of_supply' => 1,
'invoice_prefix' => 'HAWIOT/26-27/',
'prices_include_tax' => false,
'authorized_signatory' => 'g',
'bank_details' => 'z',
'upi_id' => 'm',
'receiving_bank_account' => ['institution' => 'HDFC Bank', 'account_name' => 'Anika Stores', 'account_number' => '001234567890', 'ifsc' => 'HDFC0000123', 'branch' => 'Pune', 'show_on_invoice' => true],
'theme' => '#2563EB',
'custom_theme_colour' => '#2563EB',
'default_locale' => 'en',
'invoice_share_message_template' => 'w',
'min_shelf_life_days' => 25,
'short_expiry_action' => 'warn',
'notification_preferences' => [
[
'enabled' => false,
'days' => [-364],
'threshold' => 6,
],
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 1,
"name": "Anika Stores",
"slug": "anika-stores",
"role": "owner",
"phone": "9876543210",
"email": "[email protected]",
"legal_name": "Anika Stores Private Limited",
"gstin": "27AAPFU0939F1ZV",
"gst_registration_type": "regular",
"currency": "INR",
"timezone": "Asia/Kolkata",
"address": {
"line_1": "12 Market Road",
"line_2": null,
"city": "Pune",
"state_code": "27",
"pincode": "411001"
},
"default_place_of_supply": "27",
"invoice_prefix": "INV",
"prices_include_tax": true,
"upi_id": "anikastores@bank",
"theme": "#2563EB",
"default_locale": "hi",
"is_active": true,
"manual_sharing": {
"enabled": true,
"invoice_share_message_template": "Hello {customer_name}, invoice {invoice_number} for {invoice_total} is ready."
},
"subscription": {
"status": "active",
"billing_interval": "monthly",
"trial_ends_at": null,
"renews_at": "2026-09-19T00:00:00.000000Z",
"ends_at": null,
"cancelled_at": null,
"plan": {
"id": 2,
"name": "Smart",
"slug": "smart",
"features": [
"inventory",
"pos",
"expenses"
],
"limits": []
}
},
"receiving_bank_account": {
"id": 2,
"institution": "HDFC Bank",
"account_name": "Anika Stores",
"account_number": "001234567890",
"ifsc": "HDFC0000123",
"branch": "Pune",
"show_on_invoice": true
},
"theme_colours": {
"label": "Custom colour",
"brand": "#2159D4",
"brand_dark": "#19439F",
"soft": "#E5ECFD"
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
theme
string|null
Saved preset key or canonical custom #RRGGBB, independently per workspace.
theme_colours
object
Effective label, brand, brand_dark and soft tokens. Custom colours generate readable shades for white and tinted backgrounds; use these tokens instead of raw theme hex for interface text and buttons.
receiving_bank_account
object|null
Current receiving bank, including full account details. Only returned to members with accounting permission. Details are encrypted at rest and omitted from audit snapshots.
GET api/v1/businesses/{business}/dashboard
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/dashboard" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/dashboard"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/dashboard';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Business onboarding
Complete the resumable setup required after registering a new business. Until onboarding finishes, clients may resubmit a completed required step to correct saved data without moving the current step backwards.
GET api/v1/businesses/{business}/onboarding
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/onboarding" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/onboarding"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/onboarding';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
PATCH api/v1/businesses/{business}/onboarding/language
requires authentication
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/onboarding/language" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"locale\": \"hi\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/onboarding/language"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"locale": "hi"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/onboarding/language';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'locale' => 'hi',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
POST api/v1/businesses/{business}/onboarding/business
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/onboarding/business" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "name=Veera Stores"\
--form "store_type=kirana-store"\
--form "supply_type=goods"\
--form "phone=9876543210"\
--form "logo=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/businesses/1/onboarding/business"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('name', 'Veera Stores');
body.append('store_type', 'kirana-store');
body.append('supply_type', 'goods');
body.append('phone', '9876543210');
body.append('logo', document.querySelector('input[name="logo"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/onboarding/business';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'name',
'contents' => 'Veera Stores'
],
[
'name' => 'store_type',
'contents' => 'kirana-store'
],
[
'name' => 'supply_type',
'contents' => 'goods'
],
[
'name' => 'phone',
'contents' => '9876543210'
],
[
'name' => 'logo',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
logo_url
string|null
Temporary signed object-storage URL when the logo is stored on S3. Refresh the resource after it expires.
PATCH api/v1/businesses/{business}/onboarding/tax
requires authentication
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/onboarding/tax" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"gst_status\": \"registered\",
\"gst_registration_type\": \"normal\",
\"gstin\": \"29ABCDE1234F1Z5\",
\"legal_name\": \"a\",
\"address_line_1\": \"12 Market Road\",
\"address_line_2\": \"k\",
\"city\": \"Bengaluru\",
\"state_code\": \"29\",
\"pincode\": \"560001\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/onboarding/tax"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"gst_status": "registered",
"gst_registration_type": "normal",
"gstin": "29ABCDE1234F1Z5",
"legal_name": "a",
"address_line_1": "12 Market Road",
"address_line_2": "k",
"city": "Bengaluru",
"state_code": "29",
"pincode": "560001"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/onboarding/tax';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'gst_status' => 'registered',
'gst_registration_type' => 'normal',
'gstin' => '29ABCDE1234F1Z5',
'legal_name' => 'a',
'address_line_1' => '12 Market Road',
'address_line_2' => 'k',
'city' => 'Bengaluru',
'state_code' => '29',
'pincode' => '560001',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
POST api/v1/businesses/{business}/onboarding/steps/{step}
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/onboarding/steps/architecto" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"skip\": true
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/onboarding/steps/architecto"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"skip": true
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/onboarding/steps/architecto';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'skip' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Check the setup product workbook against existing masters only; no masters are created.
requires authentication
Nonblank category/subcategory/brand/manufacturer names must exist in this business. A supplied taxable GST percentage for a Regular GST business must match an existing master. Blank organisation columns may be assigned later. Preview rows include nullable resolved master IDs. Reuploading an unconfirmed file refreshes its validation and returns the same record with HTTP 200. Already imported files remain unchanged; fresh uploads return HTTP 201.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/onboarding/product-imports" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "file=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/businesses/1/onboarding/product-imports"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('file', document.querySelector('input[name="file"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/onboarding/product-imports';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'file',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Import every validated product row without exceeding the current plan's total item allowance.
requires authentication
Rechecks the workbook before writing; new SKU/barcode conflicts return 422 under file. Renamed/deleted classification or GST masters also return 422 under file; no masters are created.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/onboarding/product-imports/1/confirm" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/onboarding/product-imports/1/confirm"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/onboarding/product-imports/1/confirm';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (409):
{
"message": "This file has already been imported."
}
Example response (422):
{
"message": "Your current plan includes up to 5 items. Upgrade to Smart Books to add more.",
"errors": {
"plan": [
"Your current plan includes up to 5 items. Upgrade to Smart Books to add more."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
GST verification
Verify an Indian GSTIN through the configured Masters India taxpayer service. The endpoint is available during onboarding so a merchant can use the verified legal name and registered address before saving their business tax profile.
Look up and normalize an Indian GSTIN.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/gstin/lookup?gstin=03AAFCE1234J1Z0" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gstin/lookup"
);
const params = {
"gstin": "03AAFCE1234J1Z0",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gstin/lookup';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'gstin' => '03AAFCE1234J1Z0',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"gstin": "03AAFCE1234J1Z0",
"legal_name": "Edsafe Logistics Private Limited",
"trade_name": "EdLogistics Private Limited",
"status": "Active",
"registration_type": "Regular",
"constitution": "Private Limited Company",
"einvoice_status": "Yes",
"registration_date": "2019-04-01",
"last_updated": "2026-01-15",
"state_code": "03",
"state_name": "Punjab",
"address": {
"line1": "Number 234, 55 Wide Road, Ground Floor, Opposite Upkar Transport",
"line2": "Godown Area, Zirakpur, SASNagar",
"city": "Zirakpur",
"district": "SASNagar",
"postal_code": "140603",
"state_code": "03",
"country_code": "IN"
},
"suggested_business": {
"legal_name": "Edsafe Logistics Private Limited",
"gst_registration_type": "normal",
"gstin": "03AAFCE1234J1Z0",
"address_line_1": "Number 234, 55 Wide Road, Ground Floor, Opposite Upkar Transport",
"address_line_2": "Godown Area, Zirakpur, SASNagar",
"city": "Zirakpur",
"state_code": "03",
"pincode": "140603",
"default_place_of_supply": "03"
},
"suggested_contact": {
"name": "EdLogistics Private Limited",
"company_name": "EdLogistics Private Limited",
"gst_treatment": "registered_regular",
"gstin": "03AAFCE1234J1Z0",
"billing_address_line_1": "Number 234, 55 Wide Road, Ground Floor, Opposite Upkar Transport",
"billing_address_line_2": "Godown Area, Zirakpur, SASNagar",
"billing_city": "Zirakpur",
"billing_state_code": "03",
"billing_pincode": "140603"
}
}
}
Example response (422):
{
"message": "We could not verify this GSTIN. Check it and try again."
}
Example response (503):
{
"message": "GST verification is temporarily unavailable. Please try again shortly."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Contacts
Loading customers and suppliers from a spreadsheet, which is how a shop moves its party list off whatever it used before. A file is uploaded, checked, and reviewed before anything is written; parties appear only when the import is committed, and then all of them at once.
Download a contact ledger statement PDF.
requires authentication
The statement includes dated movements, running receivable and payable balances, and period totals.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/contacts/1/ledger.pdf?from=2026-04-01&to=2027-03-31" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contacts/1/ledger.pdf"
);
const params = {
"from": "2026-04-01",
"to": "2027-03-31",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contacts/1/ledger.pdf';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'from' => '2026-04-01',
'to' => '2027-03-31',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
Binary data - The generated ledger statement PDF.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Email a contact ledger statement.
requires authentication
Sends one email to each chosen person at the party with the statement PDF for the period attached and a button to the signed public statement. Choose people by the key values in send_recipients from GET contacts/{contact}; each must have an email address. Leave from and to empty for all dates. Every email send is recorded in the audit log.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/contacts/1/ledger/email" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"recipients\": [
\"primary\",
\"person-12\"
],
\"recipient_emails\": {
\"primary\": \"[email protected]\"
},
\"subject\": \"Sri Lakshmi Traders | Purchase order: PO-0007\",
\"message\": \"Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.\",
\"from\": \"2026-04-01\",
\"to\": \"2027-03-31\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contacts/1/ledger/email"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"recipients": [
"primary",
"person-12"
],
"recipient_emails": {
"primary": "[email protected]"
},
"subject": "Sri Lakshmi Traders | Purchase order: PO-0007",
"message": "Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.",
"from": "2026-04-01",
"to": "2027-03-31"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contacts/1/ledger/email';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'recipients' => ['primary', 'person-12'],
'recipient_emails' => ['primary' => '[email protected]'],
'subject' => 'Sri Lakshmi Traders | Purchase order: PO-0007',
'message' => 'Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.',
'from' => '2026-04-01',
'to' => '2027-03-31',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (202):
{
"message": "Emailed to Ravi Kumar.",
"data": {
"sent_to": [
{
"key": "primary",
"name": "Ravi Kumar",
"email": "[email protected]"
}
]
}
}
Example response (422):
{
"message": "Ravi Kumar has no email address.",
"errors": {
"recipients.0": [
"Ravi Kumar has no email address."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Stop trading with a party, or start again.
requires authentication
Its own endpoint rather than a field on update, which re-records the opening balance and will reverse and replace its accounting entry when it looks changed. Retiring a party is a one-word decision and has no business touching their khata. An inactive party keeps its balance and every document it already sits on; it is simply no longer offered when raising a new one.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/contacts/1/active" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"is_active\": false
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contacts/1/active"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"is_active": false
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contacts/1/active';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'is_active' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "The walk-in customer is where every untagged counter sale is billed, so it cannot be made inactive.",
"errors": {
"is_active": [
"The walk-in customer is where every untagged counter sale is billed, so it cannot be made inactive."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Bulk update contact status or party role.
requires authentication
Atomic: all IDs must belong to this business and the caller must be authorized for every current and target role. Missing/cross-business IDs return 404; denied roles return 403; invalid actions, duplicate IDs and protected walk-in changes return 422. Existing documents, balances, journals and profile fields are preserved. Use one ID for an individual role change.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/contacts/bulk" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"contact_ids\": [
1,
2
],
\"action\": \"both\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contacts/bulk"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"contact_ids": [
1,
2
],
"action": "both"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contacts/bulk';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'contact_ids' => [1, 2],
'action' => 'both',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"updated_count": 2
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
View the party's private profile photo.
requires authentication
Requires membership, khata access and sales permission for customers or purchases permission for suppliers. Both parties permit either permission. Missing photos return 404. No public storage path or signed public link is exposed. Production photos use private S3; legacy images are read from their recorded disk through this same authenticated endpoint.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/contacts/1/photo" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contacts/1/photo"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contacts/1/photo';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
Binary data - The saved JPEG, PNG or WebP photo, with private/no-store headers.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
GET api/v1/businesses/{business}/contacts
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/contacts?status=active%0A%0AThe+%60search%60+term+also+matches+the+name+and+phone+of+a+contact%27s+additional+contacts." \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"type\": \"customer\",
\"search\": \"b\",
\"status\": \"all\",
\"per_page\": 22
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contacts"
);
const params = {
"status": "active
The `search` term also matches the name and phone of a contact's additional contacts.",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"type": "customer",
"search": "b",
"status": "all",
"per_page": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contacts';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'status' => 'active
The `search` term also matches the name and phone of a contact's additional contacts.',
],
'json' => [
'type' => 'customer',
'search' => 'b',
'status' => 'all',
'per_page' => 22,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
has_photo
boolean
Whether this party has a private profile photo.
photo_url
string
Authenticated API photo endpoint, or null.
additional_contacts
object[]
People at the party beyond the primary contact (contact_person, phone, email), in display order, each with id, name, designation, phone and email.
is_active
boolean
Whether the shop still trades with this party. An inactive party keeps its khata and every document it already sits on, but is no longer offered when raising a new one.
credit_limit_paise
integer
The most this customer may owe at once, or null when the party carries no limit.
credit_available_paise
integer
Headroom left before the credit limit is reached, never negative, or null when the party carries no limit.
shared_ledger_url
string
Signed public ledger link showing the complete statement with print and PDF sharing actions. The link expires after 90 days.
Create a contact and record its opening balance.
requires authentication
Customer opening balances are recorded in Accounts receivable; supplier opening balances are recorded in Accounts payable, offset by Owner equity. A customer may also carry a credit_limit, the most it may owe at once; sales invoices and manual khata credits past it are refused with HTTP 422.
A business party keeps its primary contact in contact_person, phone and email, and may list more people in additional_contacts.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/contacts" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "type=customer"\
--form "is_active="\
--form "profile_type=consumer"\
--form "name=b"\
--form "company_name=n"\
--form "contact_person=Anita Rao"\
--form "phone=g"\
--form "[email protected]"\
--form "gst_treatment=unregistered"\
--form "gstin=29ABCDE1234F1Z5"\
--form "pan=ABCDE1234F"\
--form "country=United Arab Emirates"\
--form "foreign_tax_id=100123456700003"\
--form "latitude=17.448583"\
--form "longitude=78.390803"\
--form "address=d"\
--form "billing_address_line_1=l"\
--form "billing_address_line_2=j"\
--form "billing_city=n"\
--form "billing_state_code=1"\
--form "billing_region=Dubai"\
--form "billing_pincode=569775"\
--form "billing_postal_code=SW1A 1AA"\
--form "shipping_same_as_billing="\
--form "shipping_address_line_1=n"\
--form "shipping_address_line_2=g"\
--form "shipping_city=z"\
--form "shipping_state_code=1"\
--form "shipping_region=Dubai"\
--form "shipping_pincode=569775"\
--form "shipping_postal_code=SW1A 1AA"\
--form "price_list_id=7"\
--form "default_discount_percent=2"\
--form "credit_limit=50000"\
--form "opening_balance=22"\
--form "opening_balance_side=receivable"\
--form "opening_balance_date=2026-01-15"\
--form "additional_contacts[][name]=Suresh Gupta"\
--form "additional_contacts[][designation]=Accounts"\
--form "additional_contacts[][phone]=9811111111"\
--form "additional_contacts[][email][email protected]"\
--form "remove_photo="\
--form "photo=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contacts"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('type', 'customer');
body.append('is_active', '');
body.append('profile_type', 'consumer');
body.append('name', 'b');
body.append('company_name', 'n');
body.append('contact_person', 'Anita Rao');
body.append('phone', 'g');
body.append('email', '[email protected]');
body.append('gst_treatment', 'unregistered');
body.append('gstin', '29ABCDE1234F1Z5');
body.append('pan', 'ABCDE1234F');
body.append('country', 'United Arab Emirates');
body.append('foreign_tax_id', '100123456700003');
body.append('latitude', '17.448583');
body.append('longitude', '78.390803');
body.append('address', 'd');
body.append('billing_address_line_1', 'l');
body.append('billing_address_line_2', 'j');
body.append('billing_city', 'n');
body.append('billing_state_code', '1');
body.append('billing_region', 'Dubai');
body.append('billing_pincode', '569775');
body.append('billing_postal_code', 'SW1A 1AA');
body.append('shipping_same_as_billing', '');
body.append('shipping_address_line_1', 'n');
body.append('shipping_address_line_2', 'g');
body.append('shipping_city', 'z');
body.append('shipping_state_code', '1');
body.append('shipping_region', 'Dubai');
body.append('shipping_pincode', '569775');
body.append('shipping_postal_code', 'SW1A 1AA');
body.append('price_list_id', '7');
body.append('default_discount_percent', '2');
body.append('credit_limit', '50000');
body.append('opening_balance', '22');
body.append('opening_balance_side', 'receivable');
body.append('opening_balance_date', '2026-01-15');
body.append('additional_contacts[][name]', 'Suresh Gupta');
body.append('additional_contacts[][designation]', 'Accounts');
body.append('additional_contacts[][phone]', '9811111111');
body.append('additional_contacts[][email]', '[email protected]');
body.append('remove_photo', '');
body.append('photo', document.querySelector('input[name="photo"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contacts';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'type',
'contents' => 'customer'
],
[
'name' => 'is_active',
'contents' => ''
],
[
'name' => 'profile_type',
'contents' => 'consumer'
],
[
'name' => 'name',
'contents' => 'b'
],
[
'name' => 'company_name',
'contents' => 'n'
],
[
'name' => 'contact_person',
'contents' => 'Anita Rao'
],
[
'name' => 'phone',
'contents' => 'g'
],
[
'name' => 'email',
'contents' => '[email protected]'
],
[
'name' => 'gst_treatment',
'contents' => 'unregistered'
],
[
'name' => 'gstin',
'contents' => '29ABCDE1234F1Z5'
],
[
'name' => 'pan',
'contents' => 'ABCDE1234F'
],
[
'name' => 'country',
'contents' => 'United Arab Emirates'
],
[
'name' => 'foreign_tax_id',
'contents' => '100123456700003'
],
[
'name' => 'latitude',
'contents' => '17.448583'
],
[
'name' => 'longitude',
'contents' => '78.390803'
],
[
'name' => 'address',
'contents' => 'd'
],
[
'name' => 'billing_address_line_1',
'contents' => 'l'
],
[
'name' => 'billing_address_line_2',
'contents' => 'j'
],
[
'name' => 'billing_city',
'contents' => 'n'
],
[
'name' => 'billing_state_code',
'contents' => '1'
],
[
'name' => 'billing_region',
'contents' => 'Dubai'
],
[
'name' => 'billing_pincode',
'contents' => '569775'
],
[
'name' => 'billing_postal_code',
'contents' => 'SW1A 1AA'
],
[
'name' => 'shipping_same_as_billing',
'contents' => ''
],
[
'name' => 'shipping_address_line_1',
'contents' => 'n'
],
[
'name' => 'shipping_address_line_2',
'contents' => 'g'
],
[
'name' => 'shipping_city',
'contents' => 'z'
],
[
'name' => 'shipping_state_code',
'contents' => '1'
],
[
'name' => 'shipping_region',
'contents' => 'Dubai'
],
[
'name' => 'shipping_pincode',
'contents' => '569775'
],
[
'name' => 'shipping_postal_code',
'contents' => 'SW1A 1AA'
],
[
'name' => 'price_list_id',
'contents' => '7'
],
[
'name' => 'default_discount_percent',
'contents' => '2'
],
[
'name' => 'credit_limit',
'contents' => '50000'
],
[
'name' => 'opening_balance',
'contents' => '22'
],
[
'name' => 'opening_balance_side',
'contents' => 'receivable'
],
[
'name' => 'opening_balance_date',
'contents' => '2026-01-15'
],
[
'name' => 'additional_contacts[][name]',
'contents' => 'Suresh Gupta'
],
[
'name' => 'additional_contacts[][designation]',
'contents' => 'Accounts'
],
[
'name' => 'additional_contacts[][phone]',
'contents' => '9811111111'
],
[
'name' => 'additional_contacts[][email]',
'contents' => '[email protected]'
],
[
'name' => 'remove_photo',
'contents' => ''
],
[
'name' => 'photo',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
has_photo
boolean
Whether this party has a private profile photo.
photo_url
string
Authenticated API photo endpoint, or null.
latitude
number
Contact location latitude in decimal degrees, or null when not set.
longitude
number
Contact location longitude in decimal degrees, or null when not set.
additional_contacts
object[]
People at the party beyond the primary contact, in display order, each with id, name, designation, phone and email.
credit_limit_paise
integer
The most this customer may owe at once, or null when the party carries no limit.
credit_available_paise
integer
Headroom left before the credit limit is reached, never negative, or null when the party carries no limit.
shared_ledger_url
string
Signed public ledger link showing the complete statement with print and PDF sharing actions. The link expires after 90 days.
GET api/v1/businesses/{business}/contacts/{id}
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/contacts/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contacts/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contacts/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
has_photo
boolean
Whether this party has a private profile photo.
photo_url
string
Authenticated API photo endpoint, or null.
latitude
number
Contact location latitude in decimal degrees, or null when not set.
longitude
number
Contact location longitude in decimal degrees, or null when not set.
additional_contacts
object[]
People at the party beyond the primary contact, in display order, each with id, name, designation, phone and email.
shared_ledger_url
string
Signed public ledger link showing the complete statement with print and PDF sharing actions. The link expires after 90 days.
send_recipients
object[]
People at the party a document can be sent to: the primary contact (key primary) then each additional contact (key person-{id}), each with name, designation, phone, whatsapp_phone (digits with country code, or null) and email. Only people with a phone or email are listed. Pass the keys to the email endpoints.
Update a contact and replace its opening-balance journal.
requires authentication
Omit additional_contacts to leave the list as it is; send the complete list to replace it, or an empty array to remove every additional contact.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/businesses/1/contacts/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "type=customer"\
--form "is_active="\
--form "profile_type=consumer"\
--form "name=b"\
--form "company_name=n"\
--form "contact_person=Anita Rao"\
--form "phone=g"\
--form "[email protected]"\
--form "gst_treatment=unregistered"\
--form "gstin=29ABCDE1234F1Z5"\
--form "pan=ABCDE1234F"\
--form "country=United Arab Emirates"\
--form "foreign_tax_id=100123456700003"\
--form "latitude=17.448583"\
--form "longitude=78.390803"\
--form "address=d"\
--form "billing_address_line_1=l"\
--form "billing_address_line_2=j"\
--form "billing_city=n"\
--form "billing_state_code=1"\
--form "billing_region=Dubai"\
--form "billing_pincode=569775"\
--form "billing_postal_code=SW1A 1AA"\
--form "shipping_same_as_billing="\
--form "shipping_address_line_1=n"\
--form "shipping_address_line_2=g"\
--form "shipping_city=z"\
--form "shipping_state_code=1"\
--form "shipping_region=Dubai"\
--form "shipping_pincode=569775"\
--form "shipping_postal_code=SW1A 1AA"\
--form "price_list_id=7"\
--form "default_discount_percent=2"\
--form "credit_limit=50000"\
--form "opening_balance=22"\
--form "opening_balance_side=receivable"\
--form "opening_balance_date=2026-01-15"\
--form "additional_contacts[][name]=Suresh Gupta"\
--form "additional_contacts[][designation]=Accounts"\
--form "additional_contacts[][phone]=9811111111"\
--form "additional_contacts[][email][email protected]"\
--form "remove_photo="\
--form "photo=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contacts/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('type', 'customer');
body.append('is_active', '');
body.append('profile_type', 'consumer');
body.append('name', 'b');
body.append('company_name', 'n');
body.append('contact_person', 'Anita Rao');
body.append('phone', 'g');
body.append('email', '[email protected]');
body.append('gst_treatment', 'unregistered');
body.append('gstin', '29ABCDE1234F1Z5');
body.append('pan', 'ABCDE1234F');
body.append('country', 'United Arab Emirates');
body.append('foreign_tax_id', '100123456700003');
body.append('latitude', '17.448583');
body.append('longitude', '78.390803');
body.append('address', 'd');
body.append('billing_address_line_1', 'l');
body.append('billing_address_line_2', 'j');
body.append('billing_city', 'n');
body.append('billing_state_code', '1');
body.append('billing_region', 'Dubai');
body.append('billing_pincode', '569775');
body.append('billing_postal_code', 'SW1A 1AA');
body.append('shipping_same_as_billing', '');
body.append('shipping_address_line_1', 'n');
body.append('shipping_address_line_2', 'g');
body.append('shipping_city', 'z');
body.append('shipping_state_code', '1');
body.append('shipping_region', 'Dubai');
body.append('shipping_pincode', '569775');
body.append('shipping_postal_code', 'SW1A 1AA');
body.append('price_list_id', '7');
body.append('default_discount_percent', '2');
body.append('credit_limit', '50000');
body.append('opening_balance', '22');
body.append('opening_balance_side', 'receivable');
body.append('opening_balance_date', '2026-01-15');
body.append('additional_contacts[][name]', 'Suresh Gupta');
body.append('additional_contacts[][designation]', 'Accounts');
body.append('additional_contacts[][phone]', '9811111111');
body.append('additional_contacts[][email]', '[email protected]');
body.append('remove_photo', '');
body.append('photo', document.querySelector('input[name="photo"]').files[0]);
fetch(url, {
method: "PUT",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contacts/1';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'type',
'contents' => 'customer'
],
[
'name' => 'is_active',
'contents' => ''
],
[
'name' => 'profile_type',
'contents' => 'consumer'
],
[
'name' => 'name',
'contents' => 'b'
],
[
'name' => 'company_name',
'contents' => 'n'
],
[
'name' => 'contact_person',
'contents' => 'Anita Rao'
],
[
'name' => 'phone',
'contents' => 'g'
],
[
'name' => 'email',
'contents' => '[email protected]'
],
[
'name' => 'gst_treatment',
'contents' => 'unregistered'
],
[
'name' => 'gstin',
'contents' => '29ABCDE1234F1Z5'
],
[
'name' => 'pan',
'contents' => 'ABCDE1234F'
],
[
'name' => 'country',
'contents' => 'United Arab Emirates'
],
[
'name' => 'foreign_tax_id',
'contents' => '100123456700003'
],
[
'name' => 'latitude',
'contents' => '17.448583'
],
[
'name' => 'longitude',
'contents' => '78.390803'
],
[
'name' => 'address',
'contents' => 'd'
],
[
'name' => 'billing_address_line_1',
'contents' => 'l'
],
[
'name' => 'billing_address_line_2',
'contents' => 'j'
],
[
'name' => 'billing_city',
'contents' => 'n'
],
[
'name' => 'billing_state_code',
'contents' => '1'
],
[
'name' => 'billing_region',
'contents' => 'Dubai'
],
[
'name' => 'billing_pincode',
'contents' => '569775'
],
[
'name' => 'billing_postal_code',
'contents' => 'SW1A 1AA'
],
[
'name' => 'shipping_same_as_billing',
'contents' => ''
],
[
'name' => 'shipping_address_line_1',
'contents' => 'n'
],
[
'name' => 'shipping_address_line_2',
'contents' => 'g'
],
[
'name' => 'shipping_city',
'contents' => 'z'
],
[
'name' => 'shipping_state_code',
'contents' => '1'
],
[
'name' => 'shipping_region',
'contents' => 'Dubai'
],
[
'name' => 'shipping_pincode',
'contents' => '569775'
],
[
'name' => 'shipping_postal_code',
'contents' => 'SW1A 1AA'
],
[
'name' => 'price_list_id',
'contents' => '7'
],
[
'name' => 'default_discount_percent',
'contents' => '2'
],
[
'name' => 'credit_limit',
'contents' => '50000'
],
[
'name' => 'opening_balance',
'contents' => '22'
],
[
'name' => 'opening_balance_side',
'contents' => 'receivable'
],
[
'name' => 'opening_balance_date',
'contents' => '2026-01-15'
],
[
'name' => 'additional_contacts[][name]',
'contents' => 'Suresh Gupta'
],
[
'name' => 'additional_contacts[][designation]',
'contents' => 'Accounts'
],
[
'name' => 'additional_contacts[][phone]',
'contents' => '9811111111'
],
[
'name' => 'additional_contacts[][email]',
'contents' => '[email protected]'
],
[
'name' => 'remove_photo',
'contents' => ''
],
[
'name' => 'photo',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
has_photo
boolean
Whether this party has a private profile photo.
photo_url
string
Authenticated API photo endpoint, or null.
latitude
number
Contact location latitude in decimal degrees, or null when not set.
longitude
number
Contact location longitude in decimal degrees, or null when not set.
additional_contacts
object[]
People at the party beyond the primary contact, in display order, each with id, name, designation, phone and email.
shared_ledger_url
string
Signed public ledger link showing the complete statement with print and PDF sharing actions. The link expires after 90 days.
DELETE api/v1/businesses/{business}/contacts/{id}
requires authentication
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/contacts/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contacts/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contacts/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Download the party import template.
requires authentication
A CSV carrying the documented headers and two example rows. The headers are not the only
ones accepted — common labels from other billing software are recognised too, and a
column_map on upload settles anything that cannot be guessed.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/contact-imports/template" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contact-imports/template"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contact-imports/template';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
Binary data - The party import template as CSV.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Upload a party file and stage it for review.
requires authentication
Creates no contacts. The file is stored, checksummed, and checked row by row away from this
request, so the response may come back still uploaded; poll the read endpoint until the
status settles on validated or invalid.
profile_type and gst_treatment need not be in the file. A column stating either always
wins; otherwise a row with a GSTIN is taken as a registered business, one with a company
name as an unregistered business, and one with neither as a walk-in consumer.
An Other Contacts column lists a business party's additional contacts in one cell, each
person as Name | Role | Phone | Email and people separated by ;. A file without the
column leaves an updated party's additional contacts alone; with it, the cell replaces them,
and a blank cell removes them.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/contact-imports" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "duplicate_mode=skip"\
--form "column_map[name]=Party Name"\
--form "column_map[phone]=Mobile No"\
--form "file=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contact-imports"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('duplicate_mode', 'skip');
body.append('column_map[name]', 'Party Name');
body.append('column_map[phone]', 'Mobile No');
body.append('file', document.querySelector('input[name="file"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contact-imports';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'duplicate_mode',
'contents' => 'skip'
],
[
'name' => 'column_map[name]',
'contents' => 'Party Name'
],
[
'name' => 'column_map[phone]',
'contents' => 'Mobile No'
],
[
'name' => 'file',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
duplicate_of
string
uuid of an earlier import of a byte-identical file, or null. Present so a repeat upload can be questioned rather than silently loaded twice.
data
object
preview
object
additional_contacts
object[]
The additional contacts the row will store, each with name, designation, phone and email, or null when the file has no Other Contacts column and the party's existing ones are left as they are.
Read a staged import.
requires authentication
Returns the status, the row counts, every validation error with its row number in the sheet, and a preview sample saying what each row would do.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/contact-imports/6ff8f7f6-1eb3-3525-be4a-3932c805afed" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contact-imports/6ff8f7f6-1eb3-3525-be4a-3932c805afed"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contact-imports/6ff8f7f6-1eb3-3525-be4a-3932c805afed';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create and update every checked row.
requires authentication
The whole file is written in one transaction: a failure anywhere leaves the party list exactly as it was. Rows carrying an opening balance record a dated khata entry, never a silent balance on the record.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/contact-imports/6ff8f7f6-1eb3-3525-be4a-3932c805afed/commit" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contact-imports/6ff8f7f6-1eb3-3525-be4a-3932c805afed/commit"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contact-imports/6ff8f7f6-1eb3-3525-be4a-3932c805afed/commit';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (409):
{
"message": "This file has not passed validation."
}
Example response (409):
{
"message": "This file has already been imported."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Discard a staged import.
requires authentication
Removes the stored file and its checked rows. An import that has already created parties cannot be discarded, because it is the record of what was loaded.
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/contact-imports/6ff8f7f6-1eb3-3525-be4a-3932c805afed" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contact-imports/6ff8f7f6-1eb3-3525-be4a-3932c805afed"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contact-imports/6ff8f7f6-1eb3-3525-be4a-3932c805afed';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (409):
{
"message": "An imported file cannot be discarded."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Khata ledger
GET api/v1/businesses/{business}/ledger-entries
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/ledger-entries" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"contact_id\": 16,
\"kind\": \"n\",
\"from\": \"2026-01-15\",
\"to\": \"2026-01-15\",
\"per_page\": 22
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/ledger-entries"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"contact_id": 16,
"kind": "n",
"from": "2026-01-15",
"to": "2026-01-15",
"per_page": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/ledger-entries';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'contact_id' => 16,
'kind' => 'n',
'from' => '2026-01-15',
'to' => '2026-01-15',
'per_page' => 22,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Record a manual khata adjustment.
requires authentication
The receivable or payable adjustment is offset to Owner equity. Use invoice, purchase, expense, and payment endpoints for operational transactions.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/ledger-entries" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"contact_id\": 16,
\"kind\": \"customer_credit\",
\"amount\": 22,
\"occurred_on\": \"2026-01-15\",
\"reference\": \"g\",
\"note\": \"z\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/ledger-entries"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"contact_id": 16,
"kind": "customer_credit",
"amount": 22,
"occurred_on": "2026-01-15",
"reference": "g",
"note": "z"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/ledger-entries';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'contact_id' => 16,
'kind' => 'customer_credit',
'amount' => 22,
'occurred_on' => '2026-01-15',
'reference' => 'g',
'note' => 'z',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
GET api/v1/businesses/{business}/ledger-entries/{ledgerEntry_id}
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/ledger-entries/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/ledger-entries/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/ledger-entries/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Reverse a manual khata adjustment and its journal.
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/ledger-entries/1/reverse" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"reason\": \"b\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/ledger-entries/1/reverse"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"reason": "b"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/ledger-entries/1/reverse';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'reason' => 'b',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Inventory
Suppliers of an item: the terms agreed with each (rate, lead time, minimum order, their own item code, preferred flag) beside what purchase invoices show was actually billed.
List item transactions, newest first.
requires authentication
Includes stock movements and invoices, purchase invoices, and returns with no recorded movement for this item, including untracked goods and services. Documents are restricted by tenant, workspace role, and plan. A document with a stock movement is represented only by its movements. Fully cancelled manual adjustments and their exact same-date, same-warehouse reversal are omitted; their audit records remain stored.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/items/1/transactions?limit=10" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"limit\": 1
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/transactions"
);
const params = {
"limit": "10",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"limit": 1
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/transactions';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'limit' => '10',
],
'json' => [
'limit' => 1,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
id
integer|string
Numeric movement ID or namespaced document ID.
record_type
string
stock_movement or document.
value_basis
string
stock_cost or document_total.
movement_type
string
Movement type or invoice, purchase_invoice, sales_return, purchase_return.
reference
string
Source document reference.
quantity
number
Signed stock change for movements; summed document line quantity for documents.
value_paise
integer
Signed stock cost for movements; item line totals including tax for documents. Document totals are not stock valuations.
reason
string|null
Movement reason or document status, including Voided.
Item stock ledger.
requires authentication
Effective stock movements of the item with their source documents, party, quantity in and out,
the stock held straight after it, and its cost. Closing stock is anchored to the item's
current stock, so the newest row always matches stock_quantity. meta.summary totals
the filtered movements. Document links respect workspace role and plan: web_url is null
when the caller may not open that document.
Fully cancelled manual adjustments and their exact same-date, same-warehouse reversal
are omitted from rows, totals and closing balances. The original audit records remain.
Real opening stock, active adjustments, sales voids and invoice revisions remain visible.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/items/1/ledger?from=2026-09-01&to=2026-09-30&type=purchase&warehouse=1&sort=date&direction=desc&per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"per_page\": 1
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/ledger"
);
const params = {
"from": "2026-09-01",
"to": "2026-09-30",
"type": "purchase",
"warehouse": "1",
"sort": "date",
"direction": "desc",
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"per_page": 1
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/ledger';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'from' => '2026-09-01',
'to' => '2026-09-30',
'type' => 'purchase',
'warehouse' => '1',
'sort' => 'date',
'direction' => 'desc',
'per_page' => '25',
'page' => '1',
],
'json' => [
'per_page' => 1,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 1,
"occurred_on": "2026-10-02",
"movement_type": "opening",
"label": "Opening stock",
"documents": [],
"party": null,
"stock_in": 12.5,
"stock_out": 0,
"closing_stock": 12.5,
"unit": "pcs",
"unit_cost_paise": 8000,
"total_cost_paise": 100000,
"batch_number": null,
"warehouse": {
"id": 1,
"name": "Main warehouse",
"location": null
},
"reason": "Opening stock",
"can_edit_opening_stock": true
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"summary": {
"in_quantity": 12.5,
"in_value_paise": 100000,
"out_quantity": 0,
"out_value_paise": 0,
"net_quantity": 12.5,
"net_value_paise": 100000,
"count": 1
},
"stock_quantity": 12.5,
"track_inventory": true
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
documents
object[]
Source document chain, oldest first, such as the purchase order before the purchase invoice it became. Each has code (PO, INV, QT, DC, SR, PR), type, label, number, and web_url. Sales and purchase invoices both use INV; type tells them apart.
party
object|null
Customer or supplier on the source document: id, name, type.
stock_in
number
Quantity received; 0 for an outward movement.
stock_out
number
Quantity issued, as a positive number; 0 for an inward movement.
closing_stock
number
Stock held straight after this movement.
unit_cost_paise
integer
Cost per unit at the time of the movement.
total_cost_paise
integer
Signed stock cost of the movement; negative for stock going out.
batch_number
string|null
Lot the movement touched, for batch-tracked items.
warehouse
object|null
Warehouse the stock moved in: id, name, location.
can_edit_opening_stock
boolean
Whether this opening entry supports correction or removal by the caller. Removed opening entries are omitted from rows and totals; use PATCH or DELETE /items/{item}/opening-stock/{movement} with this row's id.
meta
object
summary
object
in_quantity
number
Total effective quantity received in the filtered range, excluding fully cancelled manual adjustment pairs.
in_value_paise
integer
Stock cost of what was received.
out_quantity
number
Total quantity issued in the filtered range.
out_value_paise
integer
Stock cost of what was issued.
net_quantity
number
in_quantity minus out_quantity.
net_value_paise
integer
in_value_paise minus out_value_paise.
count
integer
Number of movements in the filtered range. Without a warehouse filter, stock transfers are left out of the totals because they net to zero; filter by type=transfer or a warehouse to count them.
Correct opening stock.
requires authentication
Replaces this opening entry's quantity and cost, retaining its date, warehouse, and batch. Stock and balanced Inventory / Owner equity journals are corrected atomically. Original journals and before/after values remain in the audit trail. No current-stock adjustment is created. Zero quantity removes the entry from ledger rows and totals. Corrections that would make subsequent item, warehouse, or batch stock negative return 422. Only item-sourced, unreversed opening entries are supported; serialised items and stock placed into batches after opening must use their unit/batch workflows. Requires inventory feature and permission.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/items/1/opening-stock/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"quantity\": 12.5,
\"unit_cost\": 80,
\"reason\": \"Corrected the initial stock count\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/opening-stock/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"quantity": 12.5,
"unit_cost": 80,
"reason": "Corrected the initial stock count"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/opening-stock/1';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'quantity' => 12.5,
'unit_cost' => 80,
'reason' => 'Corrected the initial stock count',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 1,
"quantity": 12.5,
"unit_cost_paise": 8000,
"total_cost_paise": 100000,
"stock_quantity": 12.5
}
}
Example response (422):
{
"message": "This opening quantity is needed by later stock movements.",
"errors": {
"quantity": [
"This opening quantity is needed by later stock movements."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Remove opening stock.
requires authentication
Sets this opening entry's quantity and value to zero and omits it from ledger rows, transaction history, and totals. Adjusts current stock in its original warehouse and batch, reverses its opening journal on the original date, and retains the audit trail. Does not delete purchases or sales. The same eligibility and nonnegative-stock rules as correction apply. Repeating removal is safe. Requires inventory feature and permission.
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/items/1/opening-stock/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"reason\": \"Opening stock was entered twice\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/opening-stock/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"reason": "Opening stock was entered twice"
};
fetch(url, {
method: "DELETE",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/opening-stock/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'reason' => 'Opening stock was entered twice',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204):
Empty response
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List an item's suppliers.
requires authentication
Linked suppliers and every supplier a purchase invoice shows the item was bought from,
preferred first, then linked, then most recently bought from. A supplier seen only in
purchase history has linked false and a null id; link it with the create endpoint.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/items/1/suppliers" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/suppliers"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/suppliers';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
id
integer|null
Link id for update and delete; null for a supplier seen only in purchase history.
linked
boolean
Whether terms have been recorded for this supplier.
contact
object
Supplier contact: id, name, phone, type.
purchase_price_paise
integer|null
Agreed purchase rate.
lead_time_days
integer|null
Agreed lead time in days.
minimum_order_quantity
number|null
Minimum order quantity in the item's unit.
is_preferred
boolean
The supplier whose rate opens new purchase lines when no supplier is chosen. At most one per item.
last_purchase_price_paise
integer|null
Rate on the latest non-void purchase invoice from this supplier.
last_purchased_on
string|null
Date of that purchase invoice.
total_purchased_quantity
number
Quantity bought from this supplier across non-void purchase invoices.
purchase_count
integer
Number of those purchase invoices.
observed_lead_time_days
number|null
Average days from purchase order to purchase invoice, when invoices were raised from orders.
price_above_agreed
boolean
The last purchase rate is above the agreed rate.
Link a supplier to an item.
requires authentication
Marking a supplier preferred clears the flag on the item's other suppliers.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/items/1/suppliers" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"contact_id\": 17,
\"supplier_sku\": \"MC-TAP-01\",
\"purchase_price\": 110.5,
\"minimum_order_quantity\": 12,
\"lead_time_days\": 7,
\"is_preferred\": true,
\"notes\": \"Delivers on Tuesdays.\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/suppliers"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"contact_id": 17,
"supplier_sku": "MC-TAP-01",
"purchase_price": 110.5,
"minimum_order_quantity": 12,
"lead_time_days": 7,
"is_preferred": true,
"notes": "Delivers on Tuesdays."
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/suppliers';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'contact_id' => 17,
'supplier_sku' => 'MC-TAP-01',
'purchase_price' => 110.5,
'minimum_order_quantity' => 12.0,
'lead_time_days' => 7,
'is_preferred' => true,
'notes' => 'Delivers on Tuesdays.',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "This supplier is already linked to the item.",
"errors": {
"contact_id": [
"This supplier is already linked to the item."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update the terms agreed with a supplier.
requires authentication
Only the fields sent change. Send a field as null to clear it.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/items/1/suppliers/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"contact_id\": 16,
\"supplier_sku\": \"n\",
\"purchase_price\": 108,
\"minimum_order_quantity\": 16,
\"lead_time_days\": 5,
\"is_preferred\": true,
\"notes\": \"i\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/suppliers/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"contact_id": 16,
"supplier_sku": "n",
"purchase_price": 108,
"minimum_order_quantity": 16,
"lead_time_days": 5,
"is_preferred": true,
"notes": "i"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/suppliers/1';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'contact_id' => 16,
'supplier_sku' => 'n',
'purchase_price' => 108.0,
'minimum_order_quantity' => 16,
'lead_time_days' => 5,
'is_preferred' => true,
'notes' => 'i',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Unlink a supplier from an item.
requires authentication
Purchase history is kept, so the supplier still appears in the list with linked false
if a purchase invoice shows the item was bought from them.
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/items/1/suppliers/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/suppliers/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/suppliers/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List warehouses.
requires authentication
Default first, then by name, including inactive ones.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/warehouses" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/warehouses"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/warehouses';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
location
string|null
Short location, such as an area or section.
is_default
boolean
Used when a document line or stock change names no warehouse. Exactly one per business.
is_active
boolean
Inactive warehouses keep their history but cannot be chosen on new lines.
items_held
integer
Number of items with stock in this warehouse.
quantity
number
Total units held, across items.
Create a warehouse.
requires authentication
Marking it default takes the flag off the current default.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/warehouses" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Sanitary godown\",
\"location\": \"Gomaty Nagar\",
\"address\": \"12 Station Road, Lucknow\",
\"is_default\": false,
\"is_active\": false
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/warehouses"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Sanitary godown",
"location": "Gomaty Nagar",
"address": "12 Station Road, Lucknow",
"is_default": false,
"is_active": false
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/warehouses';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Sanitary godown',
'location' => 'Gomaty Nagar',
'address' => '12 Station Road, Lucknow',
'is_default' => false,
'is_active' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "Another warehouse already has this name.",
"errors": {
"name": [
"Another warehouse already has this name."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update a warehouse.
requires authentication
Only the fields sent change. The default warehouse cannot be deactivated or un-defaulted directly: make another warehouse the default instead. A warehouse holding stock cannot be deactivated until its stock is transferred out.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/businesses/1/warehouses/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"b\",
\"location\": \"n\",
\"address\": \"g\",
\"is_default\": false,
\"is_active\": false
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/warehouses/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "b",
"location": "n",
"address": "g",
"is_default": false,
"is_active": false
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/warehouses/1';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'b',
'location' => 'n',
'address' => 'g',
'is_default' => false,
'is_active' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Delete a warehouse.
requires authentication
Only a warehouse that is not the default and never held stock can be deleted; otherwise deactivate it.
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/warehouses/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/warehouses/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/warehouses/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "This warehouse has stock history. Deactivate it instead.",
"errors": {
"warehouse": [
"This warehouse has stock history. Deactivate it instead."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Transfer stock between warehouses.
requires authentication
Moves stock of one item from one warehouse to another. It records a transfer_out and a
transfer_in movement in the item ledger; the item's total stock, cost, and accounts are
unchanged. The source warehouse must hold the quantity.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/items/1/stock-transfers" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"from_warehouse_id\": 1,
\"to_warehouse_id\": 2,
\"quantity\": 5,
\"transferred_on\": \"2026-09-29\",
\"note\": \"Moved for the weekend sale\",
\"item_batch_id\": 3,
\"serial_numbers\": [
\"IMEI-001\"
]
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/stock-transfers"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"from_warehouse_id": 1,
"to_warehouse_id": 2,
"quantity": 5,
"transferred_on": "2026-09-29",
"note": "Moved for the weekend sale",
"item_batch_id": 3,
"serial_numbers": [
"IMEI-001"
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/stock-transfers';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'from_warehouse_id' => 1,
'to_warehouse_id' => 2,
'quantity' => 5.0,
'transferred_on' => '2026-09-29',
'note' => 'Moved for the weekend sale',
'item_batch_id' => 3,
'serial_numbers' => ['IMEI-001'],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "Main warehouse holds only 2 PCS of Tap.",
"errors": {
"quantity": [
"Main warehouse holds only 2 PCS of Tap."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
batches
object[]
Lots moved: item_batch_id, batch_number, quantity.
serial_numbers
string[]
Units moved.
id
integer
Transfer id.
warehouse_stocks
object[]
The item's stock by warehouse after the transfer: warehouse_id, name, location, is_default, quantity.
List an item's batches.
requires authentication
Ordered first-expiry-first-out, so the first sellable row is the lot the counter should
pre-select. Expired lots are listed — the shop still has to see and clear them — but
carry is_expired, and they can never be billed.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/items/1/batches?in_stock=1&sellable=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"in_stock\": false,
\"sellable\": false
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/batches"
);
const params = {
"in_stock": "1",
"sellable": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"in_stock": false,
"sellable": false
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/batches';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'in_stock' => '1',
'sellable' => '1',
],
'json' => [
'in_stock' => false,
'sellable' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
quantity
number
On-hand quantity of this lot.
is_expired
boolean
Whether the lot is past its expiry date; an expired lot is refused on any sale.
days_to_expiry
integer|null
Days until expiry, negative once past; null when the lot has no expiry date.
status
string
available, on_hold, or recalled. Only an available lot can be billed.
status_reason
string|null
Why the lot was held or recalled; null once released.
sale_state
string
Where the lot stands for sale: sellable, short_expiry (sellable with a warning), short_expiry_blocked, on_hold, recalled, expired, or empty.
is_sellable
boolean
Whether a bill may name this lot today.
sale_message
string|null
A sentence to show beside the lot: the warning for a short-expiry lot, or why a blocked lot cannot be sold.
meta
object
fefo_item_batch_id
integer|null
The lot to pre-select: earliest expiry with stock that may be sold today.
min_shelf_life_days
integer
Days a lot must have left to be sold without a warning or block; 0 means no rule. The item's own figure wins over the business default.
short_expiry_action
string
warn or block: what happens to a lot inside the minimum shelf life.
batched_quantity
number
Sum of every lot, which equals the item's stock for a batch-tracked item.
Open a batch, optionally with the stock already on the shelf.
requires authentication
Batches are usually created implicitly by a purchase invoice. This is for opening stock
and for a lot the shop is recording by hand. A positive quantity records an opening stock
movement and its inventory journal, exactly as opening stock on the item master does.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/items/1/batches" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"warehouse_id\": 16,
\"batch_number\": \"n\",
\"expiry_date\": \"2026-01-15\",
\"manufactured_on\": \"2026-01-15\",
\"mrp\": 7,
\"purchase_price\": 16,
\"quantity\": 17
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/batches"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"warehouse_id": 16,
"batch_number": "n",
"expiry_date": "2026-01-15",
"manufactured_on": "2026-01-15",
"mrp": 7,
"purchase_price": 16,
"quantity": 17
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/batches';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'warehouse_id' => 16,
'batch_number' => 'n',
'expiry_date' => '2026-01-15',
'manufactured_on' => '2026-01-15',
'mrp' => 7,
'purchase_price' => 16,
'quantity' => 17,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "Turn on batch tracking for this item first.",
"errors": {
"item_id": [
"Turn on batch tracking for this item first."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Correct a batch, and adjust the stock it holds.
requires authentication
A changed quantity records an inventory adjustment for the difference, the same journal an
item-master stock correction records, so the books follow the shelf.
status pulls the lot from sale or puts it back. on_hold and recalled both stop it being
billed — on invoices, POS, and delivery challans — while the stock stays on the shelf and in
the books; a status_reason is required for either and is shown to whoever tries to sell
the lot. available releases it. Sales returns into the lot and purchase returns out of it
still work, so recalled stock can go back to the supplier.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/items/1/batches/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"warehouse_id\": 16,
\"batch_number\": \"n\",
\"expiry_date\": \"2026-01-15\",
\"manufactured_on\": \"2026-01-15\",
\"mrp\": 7,
\"purchase_price\": 16,
\"quantity\": 17,
\"status\": \"on_hold\",
\"status_reason\": \"Customer complaint, checking the carton\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/batches/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"warehouse_id": 16,
"batch_number": "n",
"expiry_date": "2026-01-15",
"manufactured_on": "2026-01-15",
"mrp": 7,
"purchase_price": 16,
"quantity": 17,
"status": "on_hold",
"status_reason": "Customer complaint, checking the carton"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/batches/1';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'warehouse_id' => 16,
'batch_number' => 'n',
'expiry_date' => '2026-01-15',
'manufactured_on' => '2026-01-15',
'mrp' => 7,
'purchase_price' => 16,
'quantity' => 17,
'status' => 'on_hold',
'status_reason' => 'Customer complaint, checking the carton',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "Say why this batch is being held, so the counter knows.",
"errors": {
"status_reason": [
"Say why this batch is being held, so the counter knows."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Remove an empty batch.
requires authentication
A lot that ever held stock is part of the audit trail, so only an empty batch with no movements against it can be deleted. Everything else stays and simply reads as zero.
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/items/1/batches/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/batches/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/batches/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Write off stock from a batch.
requires authentication
For goods leaving without a sale — expired, damaged, or recalled stock being destroyed. The
quantity leaves the lot and the item, a write_off stock movement is recorded, and the cost
at the item's weighted average moves from Inventory to the Stock written off expense
account. Any lot may be written off, including an expired, held, or recalled one. It cannot
be undone; stock found again is added back with a batch quantity correction.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/items/1/batches/1/write-offs" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"quantity\": 4,
\"reason\": \"expired\",
\"note\": \"Destroyed with the chemists\' association\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/batches/1/write-offs"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"quantity": 4,
"reason": "expired",
"note": "Destroyed with the chemists' association"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/batches/1/write-offs';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'quantity' => 4.0,
'reason' => 'expired',
'note' => 'Destroyed with the chemists\' association',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"batch": {
"id": 18,
"batch_number": "AMX-2291",
"quantity": 0,
"status": "available",
"sale_state": "empty",
"is_sellable": false
},
"write_off": {
"quantity": 4,
"value_paise": 32000,
"reason": "expired",
"note": null,
"stock_movement_id": 311,
"occurred_on": "2026-09-28"
}
}
}
Example response (422):
{
"message": "Batch AMX-2291 holds only 4 — you cannot write off more than that.",
"errors": {
"quantity": [
"Batch AMX-2291 holds only 4 — you cannot write off more than that."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
batch
object
The lot after the write-off, in the same shape as the batch list.
write_off
object
quantity
number
Quantity removed.
value_paise
integer
Loss booked at cost, in paise.
reason
string
The reason code sent.
stock_movement_id
integer
The write_off stock movement recorded.
List an item's units.
requires authentication
Ordered oldest first, so the shelf clears in the order it filled and the first row is a
sensible default pick. Sold and written-off units are listed too — the shop still has to
see them — but carry on_shelf: false and can never be billed.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/items/1/serials?on_shelf=1&status=sold&q=3567" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"on_shelf\": false,
\"q\": \"b\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/serials"
);
const params = {
"on_shelf": "1",
"status": "sold",
"q": "3567",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"on_shelf": false,
"q": "b"
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/serials';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'on_shelf' => '1',
'status' => 'sold',
'q' => '3567',
],
'json' => [
'on_shelf' => false,
'q' => 'b',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
status
string
Where the unit is: in_stock, sold, returned, or void.
on_shelf
boolean
Whether the unit can be billed; a returned unit can.
under_warranty
boolean
Whether cover still runs, by the unit's own date.
meta
object
on_shelf_count
integer
Units available to sell, which equals the item's stock for a serial-tracked item.
Search every unit in the catalogue.
requires authentication
This is the lookup the counter actually uses: a customer walks in with a handset and the shop needs to know whether it sold it, to whom, when, and whether cover still runs. Serial history and item names remain available after the item is soft deleted.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/item-serials?q=356938&item_id=12&status=sold" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"q\": \"b\",
\"item_id\": 16
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/item-serials"
);
const params = {
"q": "356938",
"item_id": "12",
"status": "sold",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"q": "b",
"item_id": 16
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/item-serials';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'q' => '356938',
'item_id' => '12',
'status' => 'sold',
],
'json' => [
'q' => 'b',
'item_id' => 16,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
purchase
object|null
The purchase bill the unit arrived on, and from whom.
sale
object|null
The bill the unit last left on, and to whom.
One unit by its exact serial number.
requires authentication
Exact match only, ignoring case and surrounding space — a scanner that silently picks the
nearest handset is worse than one that says it does not know this unit. A miss answers
404 carrying the code back, so the app can offer to record it.
Serial history and item names remain available after the item is soft deleted.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/item-serials/356938035643809" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/item-serials/356938035643809"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/item-serials/356938035643809';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Example response (404):
{
"message": "No unit is recorded with this serial number.",
"serial_number": "356938035643809"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Record units the shop is already holding.
requires authentication
Units normally arrive on a purchase invoice, which captures the serial off each box. This is for opening stock and for a unit being recorded by hand. Every serial listed records an opening stock movement and its inventory journal, exactly as opening stock does.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/items/1/serials" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"warehouse_id\": 16,
\"serial_numbers\": [
\"356938035643809\",
\"356938035643810\"
],
\"warranty_until\": \"2027-09-13\",
\"received_on\": \"2026-01-15\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/serials"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"warehouse_id": 16,
"serial_numbers": [
"356938035643809",
"356938035643810"
],
"warranty_until": "2027-09-13",
"received_on": "2026-01-15"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/serials';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'warehouse_id' => 16,
'serial_numbers' => ['356938035643809', '356938035643810'],
'warranty_until' => '2027-09-13',
'received_on' => '2026-01-15',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "Turn on serial tracking for this item first.",
"errors": {
"item_id": [
"Turn on serial tracking for this item first."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Correct one unit.
requires authentication
The serial number and its warranty date can be corrected — a mistyped IMEI is a real and common problem. Where the unit is cannot: that follows the documents it moved on, and a status typed by hand would put the shelf and the books out of step.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/items/1/serials/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"serial_number\": \"b\",
\"warranty_until\": \"2026-01-15\",
\"received_on\": \"2026-01-15\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/serials/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"serial_number": "b",
"warranty_until": "2026-01-15",
"received_on": "2026-01-15"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/serials/1';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'serial_number' => 'b',
'warranty_until' => '2026-01-15',
'received_on' => '2026-01-15',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Remove a unit that was never sold.
requires authentication
A unit that has been billed is part of the audit trail and stays, however it was later returned. A unit on the shelf can be removed — a serial typed twice, or a box that turned out to be empty — and doing so takes it out of stock with an adjustment, not silently.
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/items/1/serials/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/serials/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/serials/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
One item by its exact barcode.
requires authentication
The catalogue search is a guess machine: it matches part of a name, part of an SKU, and hands back a list. That is right for a person typing "parle" and wrong for a scanner. An app that silently bills the first of three near-matches costs the shop a wrong bill and a stock count that stops tallying, and nobody notices for a month — so this answers with one item or with nothing, and never guesses.
Matching ignores surrounding space and reads a GS1 code the way GS1 does, so the 12-digit UPC-A a camera reports and the 13-digit EAN-13 printed on the same pack find the same item. A miss answers 404 carrying the code back, so the app can offer to create an item with it rather than make the shopkeeper retype thirteen digits off a crumpled packet.
Lots and units ride along exactly as they do in the POS feed, because a batched or serialised item cannot be billed without naming which one leaves, and the counter has to work without signal.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/items/by-barcode/8901234567890?contact=42" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"contact\": 16
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/by-barcode/8901234567890"
);
const params = {
"contact": "42",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"contact": 16
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/by-barcode/8901234567890';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'contact' => '42',
],
'json' => [
'contact' => 16,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Example response (404):
{
"message": "No item is recorded with this barcode.",
"barcode": "8901234567890"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
image_url
string|null
The item's first photo, so the scanner can show the pack it just read; null when the item has no photo.
One item by the QR sticker the shop printed for it.
requires authentication
The companion to scanning a barcode, for the half of the shelf a barcode can never cover. A barcode belongs to whoever made the pack, so loose rice, re-packed dal and the shop's own mixture have no code to scan and never will; a QR is the shop's own label for its own goods, so Dukanam mints it and the counter can scan it like anything else.
Send the payload exactly as the scanner read it. A Dukanam sticker encodes a URL, so that is usually what arrives, but the bare token is accepted too — a client should never have to take a URL apart to scan a label. A payload that is not one of ours is a miss, not an error, and is never guessed at: like the barcode endpoint, this answers with one item or with nothing.
The code travels as a query parameter, not in the path, and that is the whole reason this
endpoint is shaped differently from items/by-barcode/{code}. A barcode is usually digits
and only rarely holds a slash; a QR payload is a URL and holds slashes every time, and an
encoded slash in a path segment is refused or quietly collapsed by a fair number of
proxies. Percent-encode the value as you would any query parameter.
A token is minted per item and never reissued, because once a sticker is on a shelf it is out of our hands and a rotated token would strand every label already stuck down. Tokens are unique platform-wide but resolved within the workspace in the URL, so one shop's sticker cannot read an item out of another shop.
Lots and units ride along exactly as they do for a barcode scan, because a batched or serialised item cannot be billed without naming which one leaves.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/items/by-qr?code=https%3A%2F%2Fdukanam.com%2Fq%2F7Fq2bXm9KdLp3RtVw8ZaCe&contact=42" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"code\": \"b\",
\"contact\": 22
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/by-qr"
);
const params = {
"code": "https://dukanam.com/q/7Fq2bXm9KdLp3RtVw8ZaCe",
"contact": "42",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"code": "b",
"contact": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/by-qr';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'code' => 'https://dukanam.com/q/7Fq2bXm9KdLp3RtVw8ZaCe',
'contact' => '42',
],
'json' => [
'code' => 'b',
'contact' => 22,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Example response (404):
{
"message": "No item is recorded with this QR code."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
image_url
string|null
The item's first photo, so the scanner can show what it just read; null when the item has no photo.
Barcode labels for one item or for many.
requires authentication
Two shapes of the same job. GET items/{item}/barcode-labels prints one item's sticker —
what an app offers from an item screen after assigning a barcode. GET items/barcode-labels?items[]=
prints a run of them, which is what a shop actually does: a delivery arrives and forty
lines need shelf labels before opening. Both are this one endpoint, so a client that
learns the options once can print either.
format=json, the default, returns the code as an inline SVG per item together with the
chosen stationery's measurements, so an app can lay the sheet out itself or send one
label to a Bluetooth label printer. format=pdf returns the whole sheet already laid out
on the chosen stationery, for handing to the platform print service.
code_kind chooses what the sticker carries, and the two are not interchangeable. A
barcode reprints a number somebody else issued for a pack they made, so an item that
holds no barcode cannot have one invented for it and is skipped. A QR carries a token
Dukanam mints for the item, so it labels the loose rice, the re-packed dal and everything
else no manufacturer ever numbered — a QR run skips nothing. Scan them back with
items/by-barcode/{code} and items/by-qr?code= respectively.
Copies are per item, because a delivery is never uniform: copies[12]=6&copies[19]=2
prints six of one line and two of another. A single copies=10 prints ten of everything.
Items not named in the map print once.
An item that cannot be labelled is never silently dropped from the run. It comes back in
meta.skipped with a reason, so a shopkeeper counting stickers against a delivery note is
told which two are missing and why, rather than finding out at the shelf.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/items/barcode-labels?items[]=12&items[]=19&copies=10&layout=a4-65&width_mm=50&height_mm=30&code_kind=qr&show_name=1&show_price=1&show_mrp=&show_sku=&show_business=&format=json" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"items\": [
16
],
\"copies\": [
22
],
\"layout\": \"a4-65\",
\"width_mm\": 25,
\"height_mm\": 21,
\"code_kind\": \"barcode\",
\"show_business\": false,
\"show_name\": false,
\"show_sku\": false,
\"show_price\": false,
\"show_mrp\": false,
\"format\": \"json\",
\"search\": \"m\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/barcode-labels"
);
const params = {
"items[0]": "12",
"items[1]": "19",
"copies": "10",
"layout": "a4-65",
"width_mm": "50",
"height_mm": "30",
"code_kind": "qr",
"show_name": "1",
"show_price": "1",
"show_mrp": "0",
"show_sku": "0",
"show_business": "0",
"format": "json",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"items": [
16
],
"copies": [
22
],
"layout": "a4-65",
"width_mm": 25,
"height_mm": 21,
"code_kind": "barcode",
"show_business": false,
"show_name": false,
"show_sku": false,
"show_price": false,
"show_mrp": false,
"format": "json",
"search": "m"
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/barcode-labels';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'items[0]' => '12',
'items[1]' => '19',
'copies' => '10',
'layout' => 'a4-65',
'width_mm' => '50',
'height_mm' => '30',
'code_kind' => 'qr',
'show_name' => '1',
'show_price' => '1',
'show_mrp' => '0',
'show_sku' => '0',
'show_business' => '0',
'format' => 'json',
],
'json' => [
'items' => [16],
'copies' => [22],
'layout' => 'a4-65',
'width_mm' => 25,
'height_mm' => 21,
'code_kind' => 'barcode',
'show_business' => false,
'show_name' => false,
'show_sku' => false,
'show_price' => false,
'show_mrp' => false,
'format' => 'json',
'search' => 'm',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Example response (422):
{
"message": "That would print 720 labels. Print at most 500 at a time.",
"errors": {
"copies": [
"That would print 720 labels. Print at most 500 at a time."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
item_id
integer
The catalogue item this label belongs to.
barcode
string|null
The code stored on the item. On a barcode run this is what the bars carry and scanning the label returns it; on a QR run it is informational and may be null.
qr_token
string
Present on a QR run only: the token this item's stickers carry, stable for the life of the item.
qr_payload
string
Present on a QR run only: exactly what the square encodes. Scanning the printed label returns this string.
code_text
string|null
The line printed under the code, or null when there is none. A barcode prints its digits so they can be keyed in by hand; a QR prints nothing, because nobody retypes a token.
symbology
string
How the code is encoded: ean_13, ean_8, upc_a, itf_14, code_128, or qr. A GS1 number whose check digit adds up prints as its retail symbology; everything else prints as Code 128.
symbology_name
string
The symbology's printed name, for showing on screen.
copies
integer
How many of this label the run prints.
svg
string
Inline SVG of the code alone, with no text. For a barcode, give it barcode_width_mm by barcode_height_mm and leave quiet_zone_mm clear on each side or it will not scan. For a QR, give it qr_size_mm square — the quiet zone is already drawn inside it, and stretching it to a rectangle stops it decoding.
meta
object
code_kind
string
Which code the run printed: barcode or qr.
label_count
integer
Total stickers the run prints, copies included.
sheet_count
integer
Pages the run fills on the chosen stationery.
layout
object
The stationery's measurements in millimetres: label width and height, barcode height, QR side, page size and margins, columns and rows, and labels per page.
show
object
Which optional lines the sheet was built with.
skipped
object
item_id
integer
An item that could not be labelled.
reason
string
Why: no_barcode, unsupported_barcode, or encoding_failed. The first two cannot occur on a QR run.
Barcode labels for one item or for many.
requires authentication
Two shapes of the same job. GET items/{item}/barcode-labels prints one item's sticker —
what an app offers from an item screen after assigning a barcode. GET items/barcode-labels?items[]=
prints a run of them, which is what a shop actually does: a delivery arrives and forty
lines need shelf labels before opening. Both are this one endpoint, so a client that
learns the options once can print either.
format=json, the default, returns the code as an inline SVG per item together with the
chosen stationery's measurements, so an app can lay the sheet out itself or send one
label to a Bluetooth label printer. format=pdf returns the whole sheet already laid out
on the chosen stationery, for handing to the platform print service.
code_kind chooses what the sticker carries, and the two are not interchangeable. A
barcode reprints a number somebody else issued for a pack they made, so an item that
holds no barcode cannot have one invented for it and is skipped. A QR carries a token
Dukanam mints for the item, so it labels the loose rice, the re-packed dal and everything
else no manufacturer ever numbered — a QR run skips nothing. Scan them back with
items/by-barcode/{code} and items/by-qr?code= respectively.
Copies are per item, because a delivery is never uniform: copies[12]=6&copies[19]=2
prints six of one line and two of another. A single copies=10 prints ten of everything.
Items not named in the map print once.
An item that cannot be labelled is never silently dropped from the run. It comes back in
meta.skipped with a reason, so a shopkeeper counting stickers against a delivery note is
told which two are missing and why, rather than finding out at the shelf.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/items/1/barcode-labels?items[]=12&items[]=19&copies=10&layout=a4-65&width_mm=50&height_mm=30&code_kind=qr&show_name=1&show_price=1&show_mrp=&show_sku=&show_business=&format=json" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"items\": [
16
],
\"copies\": [
22
],
\"layout\": \"a4-65\",
\"width_mm\": 25,
\"height_mm\": 21,
\"code_kind\": \"barcode\",
\"show_business\": false,
\"show_name\": false,
\"show_sku\": false,
\"show_price\": false,
\"show_mrp\": false,
\"format\": \"json\",
\"search\": \"m\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/barcode-labels"
);
const params = {
"items[0]": "12",
"items[1]": "19",
"copies": "10",
"layout": "a4-65",
"width_mm": "50",
"height_mm": "30",
"code_kind": "qr",
"show_name": "1",
"show_price": "1",
"show_mrp": "0",
"show_sku": "0",
"show_business": "0",
"format": "json",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"items": [
16
],
"copies": [
22
],
"layout": "a4-65",
"width_mm": 25,
"height_mm": 21,
"code_kind": "barcode",
"show_business": false,
"show_name": false,
"show_sku": false,
"show_price": false,
"show_mrp": false,
"format": "json",
"search": "m"
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/barcode-labels';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'items[0]' => '12',
'items[1]' => '19',
'copies' => '10',
'layout' => 'a4-65',
'width_mm' => '50',
'height_mm' => '30',
'code_kind' => 'qr',
'show_name' => '1',
'show_price' => '1',
'show_mrp' => '0',
'show_sku' => '0',
'show_business' => '0',
'format' => 'json',
],
'json' => [
'items' => [16],
'copies' => [22],
'layout' => 'a4-65',
'width_mm' => 25,
'height_mm' => 21,
'code_kind' => 'barcode',
'show_business' => false,
'show_name' => false,
'show_sku' => false,
'show_price' => false,
'show_mrp' => false,
'format' => 'json',
'search' => 'm',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Example response (422):
{
"message": "That would print 720 labels. Print at most 500 at a time.",
"errors": {
"copies": [
"That would print 720 labels. Print at most 500 at a time."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
item_id
integer
The catalogue item this label belongs to.
barcode
string|null
The code stored on the item. On a barcode run this is what the bars carry and scanning the label returns it; on a QR run it is informational and may be null.
qr_token
string
Present on a QR run only: the token this item's stickers carry, stable for the life of the item.
qr_payload
string
Present on a QR run only: exactly what the square encodes. Scanning the printed label returns this string.
code_text
string|null
The line printed under the code, or null when there is none. A barcode prints its digits so they can be keyed in by hand; a QR prints nothing, because nobody retypes a token.
symbology
string
How the code is encoded: ean_13, ean_8, upc_a, itf_14, code_128, or qr. A GS1 number whose check digit adds up prints as its retail symbology; everything else prints as Code 128.
symbology_name
string
The symbology's printed name, for showing on screen.
copies
integer
How many of this label the run prints.
svg
string
Inline SVG of the code alone, with no text. For a barcode, give it barcode_width_mm by barcode_height_mm and leave quiet_zone_mm clear on each side or it will not scan. For a QR, give it qr_size_mm square — the quiet zone is already drawn inside it, and stretching it to a rectangle stops it decoding.
meta
object
code_kind
string
Which code the run printed: barcode or qr.
label_count
integer
Total stickers the run prints, copies included.
sheet_count
integer
Pages the run fills on the chosen stationery.
layout
object
The stationery's measurements in millimetres: label width and height, barcode height, QR side, page size and margins, columns and rows, and labels per page.
show
object
Which optional lines the sheet was built with.
skipped
object
item_id
integer
An item that could not be labelled.
reason
string
Why: no_barcode, unsupported_barcode, or encoding_failed. The first two cannot occur on a QR run.
Take an item out of circulation, or put it back.
requires authentication
Its own endpoint rather than a field on update, which reconciles the submitted quantity against the shelf and can record a stock adjustment and a journal with it. Retiring a line is a one-word decision and must not be able to move stock. An inactive item keeps its stock and every document it already sits on; it is simply no longer offered at the counter or when raising a new document.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/items/1/active" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"is_active\": false
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1/active"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"is_active": false
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1/active';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'is_active' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Assign classifications to multiple items atomically.
requires authentication
Inventory permission required. Omitted fields stay unchanged; null clears that field. Changing category clears the old subcategory unless a matching new subcategory is supplied.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/items/organisation" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"item_ids\": [
1,
2
],
\"category_id\": 1,
\"subcategory_id\": 2,
\"brand_id\": 3,
\"manufacturer_id\": 4
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/organisation"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"item_ids": [
1,
2
],
"category_id": 1,
"subcategory_id": 2,
"brand_id": 3,
"manufacturer_id": 4
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/organisation';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'item_ids' => [1, 2],
'category_id' => 1,
'subcategory_id' => 2,
'brand_id' => 3,
'manufacturer_id' => 4,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"updated": 2
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List items.
requires authentication
Item resources include sale_tax (enabled, tax_rate_basis_points, cess_rate_basis_points, price_includes_tax). Use this for new sale previews. Raw catalogue tax metadata is retained for history and future registration; it never authorizes GST collection. Unregistered businesses have sale_tax.enabled=false, zero sale rates and price_includes_tax=false.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/items?category_id=1&subcategory_id=2&brand_id=3&manufacturer_id=4&gst_rate_id=1&attention=warranty&contact=42&supplier=17&status=active" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"search\": \"b\",
\"low_stock\": false,
\"status\": \"all\",
\"contact\": 22,
\"per_page\": 7
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items"
);
const params = {
"category_id": "1",
"subcategory_id": "2",
"brand_id": "3",
"manufacturer_id": "4",
"gst_rate_id": "1",
"attention": "warranty",
"contact": "42",
"supplier": "17",
"status": "active",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"search": "b",
"low_stock": false,
"status": "all",
"contact": 22,
"per_page": 7
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'category_id' => '1',
'subcategory_id' => '2',
'brand_id' => '3',
'manufacturer_id' => '4',
'gst_rate_id' => '1',
'attention' => 'warranty',
'contact' => '42',
'supplier' => '17',
'status' => 'active',
],
'json' => [
'search' => 'b',
'low_stock' => false,
'status' => 'all',
'contact' => 22,
'per_page' => 7,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
sale_tax
object
Effective tax settings for a new sale: enabled, tax_rate_basis_points, cess_rate_basis_points, price_includes_tax. Disabled with zero rates for unregistered businesses.
category_id
integer|null
Category ID. The same ID/name pairs are returned for subcategory, brand and manufacturer.
category_name
string|null
Category display name.
subcategory_id
integer|null
Subcategory ID belonging to category_id.
subcategory_name
string|null
Subcategory display name.
brand_id
integer|null
Brand ID.
brand_name
string|null
Brand display name.
manufacturer_id
integer|null
Manufacturer ID.
manufacturer_name
string|null
Manufacturer display name.
gst_rate_id
integer|null
Business GST percentage master; the applied tax percentage remains tax_rate_basis_points.
warranty_duration
integer|null
Whole-number duration from the buyer's invoice date; null means no warranty.
warranty_unit
string|null
days, months or years. Maximum 10 years.
warranty_provider
string|null
brand, manufacturer, shop or both.
warranty_terms
string|null
Recorded coverage conditions.
warranty_provider_name
string|null
Brand/manufacturer support name.
warranty_provider_phone
string|null
Support telephone.
warranty_provider_email
string|null
Support email.
warranty_provider_website
string|null
HTTP(S) support website.
warranty_provider_address
string|null
Service centre/postal address.
warranty_provider_notes
string|null
Claim instructions.
requires_expiry
boolean
When true, every received lot needs expiry and missing-expiry stock is not sellable.
is_active
boolean
Whether the shop still offers this item. An inactive item keeps every document it already sits on and still counts in stock and reports, but is no longer offered when raising a new document or at the counter.
image_url
string|null
The item's first photo, ready to show in a list row or a POS tile; null when the item has no photo. Same URL as photos[0].url.
photos
object
id
integer
Photo id, for remove_photos on update.
url
string
Temporary signed object-storage URL from the photo’s recorded disk. New production uploads use private S3. Refresh the item after it expires.
original_name
string
File name the photo was uploaded under.
mime_type
string
Image media type, for example image/jpeg.
size
integer
Photo size in bytes.
supplier_price
object|null
Purchase rate for the supplier named by supplier, absent when supplier is not sent and null when no rate is known. Carries price_paise, source (agreed or last_purchase), supplier_id, and supplier_name. With supplier=preferred it is the item's preferred supplier.
party_price
object
Rate for the contact named by contact, absent otherwise. Carries unit_price_paise, list_price_paise, price_includes_tax, source (item, price_list, or party_discount), price_list_id, price_list_name, party_discount_basis_points, the quantity break tiers, and the exceeds_mrp and below_purchase_price warnings.
meta
object
item_allowance
object
used
integer
Number of catalogue items currently stored.
limit
integer|null
Maximum item count; null means unlimited.
remaining
integer|null
Item slots still available; null means unlimited.
can_create
boolean
Whether another item can be created on the current plan.
upgrade_plan
object|null
Recommended plan name and slug when the allowance is limited.
name
string
Recommended upgrade plan display name.
slug
string
Recommended upgrade plan slug for billing selection.
Create an item and record opening inventory.
requires authentication
Tracked opening stock creates a balanced Inventory / Owner equity journal at purchase cost. Stock and reorder quantities accept up to three decimal places. Optional category_id/subcategory_id/brand_id/manufacturer_id must belong to this tenant and have the matching kind. Alternatively use their *_name fields to create or reuse names. Warranty duration requires warranty_unit and warranty_provider and is capped at 10 years. New/changed brand, manufacturer or both policies require a support name and at least one phone, email or HTTP(S) website. Omitted fields are retained on update; null clears optional fields. Shop/no-warranty policies clear external contact fields. Issued warranties retain contact snapshots. GST master selection is authoritative for taxable items. Numeric tax_rate inputs create/reuse a business percentage master for compatibility. Omitted rate inputs preserve the item rate on update. requires_expiry needs enabled batch tracking; expiry is stored per physical batch.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/items" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "category_id=16"\
--form "category_name=Electrical"\
--form "subcategory_id=16"\
--form "subcategory_name=LED lighting"\
--form "brand_id=16"\
--form "brand_name=Sample Brand"\
--form "manufacturer_id=16"\
--form "manufacturer_name=Sample Manufacturer"\
--form "gst_rate_id=1"\
--form "warranty_duration=6"\
--form "warranty_unit=months"\
--form "warranty_provider=shop"\
--form "warranty_terms=Replacement for manufacturing defects."\
--form "warranty_provider_name=Sample Brand Support"\
--form "warranty_provider_phone=+91 1800 123 4567"\
--form "[email protected]"\
--form "warranty_provider_website=https://example.test/support"\
--form "warranty_provider_address=Example Service Centre, Hyderabad"\
--form "warranty_provider_notes=Keep your invoice and product serial number ready."\
--form "requires_expiry="\
--form "name=k"\
--form "sku=h"\
--form "barcode=w"\
--form "item_type=goods"\
--form "hsn_sac=aykcmyuw"\
--form "unit=pwlvqwrsitcpscql"\
--form "uqc=dzsnrwtujwvlxjkl"\
--form "gst_taxability=taxable"\
--form "sale_price=8"\
--form "purchase_price=10"\
--form "mrp=3"\
--form "tax_rate=14"\
--form "cess_rate=0"\
--form "stock_quantity=4"\
--form "warehouse_id=35"\
--form "reorder_level=9"\
--form "is_active="\
--form "track_inventory="\
--form "track_batches="\
--form "min_shelf_life_days=6"\
--form "track_serials="\
--form "opening_batch_number=n"\
--form "opening_batch_expiry_date=2026-01-15"\
--form "opening_serial_numbers[]=n"\
--form "opening_warranty_until=2026-01-15"\
--form "price_includes_tax="\
--form "remove_photos[]=16"\
--form "photos[]=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('category_id', '16');
body.append('category_name', 'Electrical');
body.append('subcategory_id', '16');
body.append('subcategory_name', 'LED lighting');
body.append('brand_id', '16');
body.append('brand_name', 'Sample Brand');
body.append('manufacturer_id', '16');
body.append('manufacturer_name', 'Sample Manufacturer');
body.append('gst_rate_id', '1');
body.append('warranty_duration', '6');
body.append('warranty_unit', 'months');
body.append('warranty_provider', 'shop');
body.append('warranty_terms', 'Replacement for manufacturing defects.');
body.append('warranty_provider_name', 'Sample Brand Support');
body.append('warranty_provider_phone', '+91 1800 123 4567');
body.append('warranty_provider_email', '[email protected]');
body.append('warranty_provider_website', 'https://example.test/support');
body.append('warranty_provider_address', 'Example Service Centre, Hyderabad');
body.append('warranty_provider_notes', 'Keep your invoice and product serial number ready.');
body.append('requires_expiry', '');
body.append('name', 'k');
body.append('sku', 'h');
body.append('barcode', 'w');
body.append('item_type', 'goods');
body.append('hsn_sac', 'aykcmyuw');
body.append('unit', 'pwlvqwrsitcpscql');
body.append('uqc', 'dzsnrwtujwvlxjkl');
body.append('gst_taxability', 'taxable');
body.append('sale_price', '8');
body.append('purchase_price', '10');
body.append('mrp', '3');
body.append('tax_rate', '14');
body.append('cess_rate', '0');
body.append('stock_quantity', '4');
body.append('warehouse_id', '35');
body.append('reorder_level', '9');
body.append('is_active', '');
body.append('track_inventory', '');
body.append('track_batches', '');
body.append('min_shelf_life_days', '6');
body.append('track_serials', '');
body.append('opening_batch_number', 'n');
body.append('opening_batch_expiry_date', '2026-01-15');
body.append('opening_serial_numbers[]', 'n');
body.append('opening_warranty_until', '2026-01-15');
body.append('price_includes_tax', '');
body.append('remove_photos[]', '16');
body.append('photos[]', document.querySelector('input[name="photos[]"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'category_id',
'contents' => '16'
],
[
'name' => 'category_name',
'contents' => 'Electrical'
],
[
'name' => 'subcategory_id',
'contents' => '16'
],
[
'name' => 'subcategory_name',
'contents' => 'LED lighting'
],
[
'name' => 'brand_id',
'contents' => '16'
],
[
'name' => 'brand_name',
'contents' => 'Sample Brand'
],
[
'name' => 'manufacturer_id',
'contents' => '16'
],
[
'name' => 'manufacturer_name',
'contents' => 'Sample Manufacturer'
],
[
'name' => 'gst_rate_id',
'contents' => '1'
],
[
'name' => 'warranty_duration',
'contents' => '6'
],
[
'name' => 'warranty_unit',
'contents' => 'months'
],
[
'name' => 'warranty_provider',
'contents' => 'shop'
],
[
'name' => 'warranty_terms',
'contents' => 'Replacement for manufacturing defects.'
],
[
'name' => 'warranty_provider_name',
'contents' => 'Sample Brand Support'
],
[
'name' => 'warranty_provider_phone',
'contents' => '+91 1800 123 4567'
],
[
'name' => 'warranty_provider_email',
'contents' => '[email protected]'
],
[
'name' => 'warranty_provider_website',
'contents' => 'https://example.test/support'
],
[
'name' => 'warranty_provider_address',
'contents' => 'Example Service Centre, Hyderabad'
],
[
'name' => 'warranty_provider_notes',
'contents' => 'Keep your invoice and product serial number ready.'
],
[
'name' => 'requires_expiry',
'contents' => ''
],
[
'name' => 'name',
'contents' => 'k'
],
[
'name' => 'sku',
'contents' => 'h'
],
[
'name' => 'barcode',
'contents' => 'w'
],
[
'name' => 'item_type',
'contents' => 'goods'
],
[
'name' => 'hsn_sac',
'contents' => 'aykcmyuw'
],
[
'name' => 'unit',
'contents' => 'pwlvqwrsitcpscql'
],
[
'name' => 'uqc',
'contents' => 'dzsnrwtujwvlxjkl'
],
[
'name' => 'gst_taxability',
'contents' => 'taxable'
],
[
'name' => 'sale_price',
'contents' => '8'
],
[
'name' => 'purchase_price',
'contents' => '10'
],
[
'name' => 'mrp',
'contents' => '3'
],
[
'name' => 'tax_rate',
'contents' => '14'
],
[
'name' => 'cess_rate',
'contents' => '0'
],
[
'name' => 'stock_quantity',
'contents' => '4'
],
[
'name' => 'warehouse_id',
'contents' => '35'
],
[
'name' => 'reorder_level',
'contents' => '9'
],
[
'name' => 'is_active',
'contents' => ''
],
[
'name' => 'track_inventory',
'contents' => ''
],
[
'name' => 'track_batches',
'contents' => ''
],
[
'name' => 'min_shelf_life_days',
'contents' => '6'
],
[
'name' => 'track_serials',
'contents' => ''
],
[
'name' => 'opening_batch_number',
'contents' => 'n'
],
[
'name' => 'opening_batch_expiry_date',
'contents' => '2026-01-15'
],
[
'name' => 'opening_serial_numbers[]',
'contents' => 'n'
],
[
'name' => 'opening_warranty_until',
'contents' => '2026-01-15'
],
[
'name' => 'price_includes_tax',
'contents' => ''
],
[
'name' => 'remove_photos[]',
'contents' => '16'
],
[
'name' => 'photos[]',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "Your current plan includes up to 5 items. Upgrade to Smart Books to add more.",
"errors": {
"plan": [
"Your current plan includes up to 5 items. Upgrade to Smart Books to add more."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
GET api/v1/businesses/{business}/items/{id}
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/items/1?contact=42&supplier=17" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"contact\": 16
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1"
);
const params = {
"contact": "42",
"supplier": "17",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"contact": 16
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'contact' => '42',
'supplier' => '17',
],
'json' => [
'contact' => 16,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
warehouse_stocks
object[]
Stock split by warehouse, for items that track stock: warehouse_id, name, location, is_default, quantity. Stock not yet placed in a warehouse counts toward the default.
supplier_price
object|null
Purchase rate for the supplier named by supplier: price_paise, source (agreed or last_purchase), supplier_id, supplier_name. Null when no rate is known; absent when supplier is not sent.
image_url
string|null
The item's first photo, ready to show on the item screen; null when the item has no photo. Same URL as photos[0].url.
photos
object
id
integer
Photo id, for remove_photos on update.
url
string
Temporary signed object-storage URL from the photo’s recorded disk. New production uploads use private S3. Refresh the item after it expires.
original_name
string
File name the photo was uploaded under.
mime_type
string
Image media type, for example image/jpeg.
size
integer
Photo size in bytes.
party_price
object
Rate for the contact named by contact, absent otherwise.
Update an item and account for stock corrections.
requires authentication
Quantity increases record inventory-adjustment income; decreases record an operating expense. Stock and reorder quantities accept up to three decimal places.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/businesses/1/items/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "category_id=16"\
--form "category_name=n"\
--form "subcategory_id=16"\
--form "subcategory_name=n"\
--form "brand_id=16"\
--form "brand_name=n"\
--form "manufacturer_id=16"\
--form "manufacturer_name=n"\
--form "gst_rate_id=16"\
--form "warranty_duration=22"\
--form "warranty_unit=days"\
--form "warranty_provider=brand"\
--form "warranty_terms=g"\
--form "warranty_provider_name=z"\
--form "warranty_provider_phone=m"\
--form "[email protected]"\
--form "warranty_provider_website=j"\
--form "warranty_provider_address=n"\
--form "warranty_provider_notes=i"\
--form "requires_expiry="\
--form "name=k"\
--form "sku=h"\
--form "barcode=w"\
--form "item_type=goods"\
--form "hsn_sac=aykcmyuw"\
--form "unit=pwlvqwrsitcpscql"\
--form "uqc=dzsnrwtujwvlxjkl"\
--form "gst_taxability=taxable"\
--form "sale_price=8"\
--form "purchase_price=10"\
--form "mrp=3"\
--form "tax_rate=14"\
--form "cess_rate=7"\
--form "stock_quantity=4"\
--form "warehouse_id=35"\
--form "reorder_level=9"\
--form "is_active="\
--form "track_inventory="\
--form "track_batches="\
--form "min_shelf_life_days=6"\
--form "track_serials="\
--form "opening_batch_number=n"\
--form "opening_batch_expiry_date=2026-01-15"\
--form "opening_serial_numbers[]=n"\
--form "opening_warranty_until=2026-01-15"\
--form "price_includes_tax="\
--form "remove_photos[]=16"\
--form "photos[]=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('category_id', '16');
body.append('category_name', 'n');
body.append('subcategory_id', '16');
body.append('subcategory_name', 'n');
body.append('brand_id', '16');
body.append('brand_name', 'n');
body.append('manufacturer_id', '16');
body.append('manufacturer_name', 'n');
body.append('gst_rate_id', '16');
body.append('warranty_duration', '22');
body.append('warranty_unit', 'days');
body.append('warranty_provider', 'brand');
body.append('warranty_terms', 'g');
body.append('warranty_provider_name', 'z');
body.append('warranty_provider_phone', 'm');
body.append('warranty_provider_email', '[email protected]');
body.append('warranty_provider_website', 'j');
body.append('warranty_provider_address', 'n');
body.append('warranty_provider_notes', 'i');
body.append('requires_expiry', '');
body.append('name', 'k');
body.append('sku', 'h');
body.append('barcode', 'w');
body.append('item_type', 'goods');
body.append('hsn_sac', 'aykcmyuw');
body.append('unit', 'pwlvqwrsitcpscql');
body.append('uqc', 'dzsnrwtujwvlxjkl');
body.append('gst_taxability', 'taxable');
body.append('sale_price', '8');
body.append('purchase_price', '10');
body.append('mrp', '3');
body.append('tax_rate', '14');
body.append('cess_rate', '7');
body.append('stock_quantity', '4');
body.append('warehouse_id', '35');
body.append('reorder_level', '9');
body.append('is_active', '');
body.append('track_inventory', '');
body.append('track_batches', '');
body.append('min_shelf_life_days', '6');
body.append('track_serials', '');
body.append('opening_batch_number', 'n');
body.append('opening_batch_expiry_date', '2026-01-15');
body.append('opening_serial_numbers[]', 'n');
body.append('opening_warranty_until', '2026-01-15');
body.append('price_includes_tax', '');
body.append('remove_photos[]', '16');
body.append('photos[]', document.querySelector('input[name="photos[]"]').files[0]);
fetch(url, {
method: "PUT",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'category_id',
'contents' => '16'
],
[
'name' => 'category_name',
'contents' => 'n'
],
[
'name' => 'subcategory_id',
'contents' => '16'
],
[
'name' => 'subcategory_name',
'contents' => 'n'
],
[
'name' => 'brand_id',
'contents' => '16'
],
[
'name' => 'brand_name',
'contents' => 'n'
],
[
'name' => 'manufacturer_id',
'contents' => '16'
],
[
'name' => 'manufacturer_name',
'contents' => 'n'
],
[
'name' => 'gst_rate_id',
'contents' => '16'
],
[
'name' => 'warranty_duration',
'contents' => '22'
],
[
'name' => 'warranty_unit',
'contents' => 'days'
],
[
'name' => 'warranty_provider',
'contents' => 'brand'
],
[
'name' => 'warranty_terms',
'contents' => 'g'
],
[
'name' => 'warranty_provider_name',
'contents' => 'z'
],
[
'name' => 'warranty_provider_phone',
'contents' => 'm'
],
[
'name' => 'warranty_provider_email',
'contents' => '[email protected]'
],
[
'name' => 'warranty_provider_website',
'contents' => 'j'
],
[
'name' => 'warranty_provider_address',
'contents' => 'n'
],
[
'name' => 'warranty_provider_notes',
'contents' => 'i'
],
[
'name' => 'requires_expiry',
'contents' => ''
],
[
'name' => 'name',
'contents' => 'k'
],
[
'name' => 'sku',
'contents' => 'h'
],
[
'name' => 'barcode',
'contents' => 'w'
],
[
'name' => 'item_type',
'contents' => 'goods'
],
[
'name' => 'hsn_sac',
'contents' => 'aykcmyuw'
],
[
'name' => 'unit',
'contents' => 'pwlvqwrsitcpscql'
],
[
'name' => 'uqc',
'contents' => 'dzsnrwtujwvlxjkl'
],
[
'name' => 'gst_taxability',
'contents' => 'taxable'
],
[
'name' => 'sale_price',
'contents' => '8'
],
[
'name' => 'purchase_price',
'contents' => '10'
],
[
'name' => 'mrp',
'contents' => '3'
],
[
'name' => 'tax_rate',
'contents' => '14'
],
[
'name' => 'cess_rate',
'contents' => '7'
],
[
'name' => 'stock_quantity',
'contents' => '4'
],
[
'name' => 'warehouse_id',
'contents' => '35'
],
[
'name' => 'reorder_level',
'contents' => '9'
],
[
'name' => 'is_active',
'contents' => ''
],
[
'name' => 'track_inventory',
'contents' => ''
],
[
'name' => 'track_batches',
'contents' => ''
],
[
'name' => 'min_shelf_life_days',
'contents' => '6'
],
[
'name' => 'track_serials',
'contents' => ''
],
[
'name' => 'opening_batch_number',
'contents' => 'n'
],
[
'name' => 'opening_batch_expiry_date',
'contents' => '2026-01-15'
],
[
'name' => 'opening_serial_numbers[]',
'contents' => 'n'
],
[
'name' => 'opening_warranty_until',
'contents' => '2026-01-15'
],
[
'name' => 'price_includes_tax',
'contents' => ''
],
[
'name' => 'remove_photos[]',
'contents' => '16'
],
[
'name' => 'photos[]',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Soft delete an item.
requires authentication
Items used on documents can be deleted. The item is removed from the catalogue and new item selections; existing document lines, stock history, quantities, and accounting entries are retained. Existing documents can keep their deleted items when edited, and returns still use the original item. Requires inventory permission. Subsequent item reads, updates, or deletions return 404, as do foreign-tenant items.
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/items/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/items/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/items/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204):
Empty response
Example response (403):
{
"message": "Your role does not allow this action."
}
Example response (404):
{
"message": "Not Found"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List business GST percentage masters for item lookups.
requires authentication
Returns an empty list for unregistered and composition businesses; historical masters remain stored. These are configured percentages, not a tax-advice or statutory-rate catalogue.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/gst-rates" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst-rates"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst-rates';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 1,
"percentage": 18,
"basis_points": 1800,
"label": "18%"
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create or reuse a business GST percentage.
requires authentication
Regular GST registration is required. Unregistered and composition businesses return 422.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/gst-rates" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"percentage\": 18
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst-rates"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"percentage": 18
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst-rates';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'percentage' => 18.0,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 1,
"percentage": 18,
"basis_points": 1800,
"label": "18%"
}
}
Example response (201):
{
"data": {
"id": 1,
"percentage": 18,
"basis_points": 1800,
"label": "18%"
}
}
Example response (422):
{
"message": "Only a business with Regular GST registration can add or edit GST percentages.",
"errors": {
"percentage": [
"Only a business with Regular GST registration can add or edit GST percentages."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Change an unused GST percentage.
requires authentication
Requires Regular GST registration; unregistered and composition businesses return 422. A rate referenced by any item (including deleted items) cannot change. Create a new master instead.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/gst-rates/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"percentage\": 12
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst-rates/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"percentage": 12
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst-rates/1';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'percentage' => 12.0,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (409):
{
"message": "This rate is used by an item. Add a new percentage instead."
}
Example response (422):
{
"message": "Only a business with Regular GST registration can add or edit GST percentages.",
"errors": {
"percentage": [
"Only a business with Regular GST registration can add or edit GST percentages."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Delete an unused GST percentage. Used rates return 409.
requires authentication
Requires Regular GST registration; unregistered and composition businesses return 422.
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/gst-rates/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst-rates/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst-rates/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204):
Empty response
Example response (409):
{
"message": "This rate is still used by an item."
}
Example response (422):
{
"message": "Only a business with Regular GST registration can add or edit GST percentages.",
"errors": {
"percentage": [
"Only a business with Regular GST registration can add or edit GST percentages."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List categories, subcategories, brands and manufacturers.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/item-classifications?kind=category&parent_id=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"parent_id\": 16
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/item-classifications"
);
const params = {
"kind": "category",
"parent_id": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"parent_id": 16
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/item-classifications';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'kind' => 'category',
'parent_id' => '1',
],
'json' => [
'parent_id' => 16,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create or reuse a classification.
requires authentication
Names ignore case and repeated spaces. Subcategories require a tenant-owned category parent.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/item-classifications" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"kind\": \"category\",
\"name\": \"Lighting\",
\"parent_id\": 1
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/item-classifications"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"kind": "category",
"name": "Lighting",
"parent_id": 1
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/item-classifications';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'kind' => 'category',
'name' => 'Lighting',
'parent_id' => 1,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Rename a classification without changing its hierarchy.
requires authentication
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/item-classifications/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Electrical lighting\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/item-classifications/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Electrical lighting"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/item-classifications/1';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Electrical lighting',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Delete an unused classification. Used classifications and categories with children return 409.
requires authentication
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/item-classifications/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/item-classifications/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/item-classifications/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204):
Empty response
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List warranty records for this workspace.
requires authentication
Contact details are frozen when coverage is issued, including after invoice corrections.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/item-warranties?search=LED&item_id=1&contact_id=1&per_page=25" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"search\": \"b\",
\"item_id\": 22,
\"contact_id\": 67,
\"per_page\": 16
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/item-warranties"
);
const params = {
"search": "LED",
"item_id": "1",
"contact_id": "1",
"per_page": "25",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"search": "b",
"item_id": 22,
"contact_id": 67,
"per_page": 16
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/item-warranties';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'search' => 'LED',
'item_id' => '1',
'contact_id' => '1',
'per_page' => '25',
],
'json' => [
'search' => 'b',
'item_id' => 22,
'contact_id' => 67,
'per_page' => 16,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
provider_label
string
Display label for shop/brand/manufacturer/both.
provider_contact
object|null
Original external support details (name, phone, email, website, address, notes).
support_contacts
object[]
Contact routes for the selected provider, plus the shop for combined cover.
Read a warranty, including coverage, its private buyer link and issued support contacts.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/item-warranties/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/item-warranties/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/item-warranties/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
provider_contact
object|null
Frozen external provider name, phone, email, website, address and notes.
support_contacts
object[]
Provider contact routes; shop details included for shop/both policies.
Download the product import template.
requires authentication
The workbook carries the Products sheet and header row the upload expects. Row 1 must
survive unedited — a renamed or reordered header is reported as a file-level error.
Optional product columns include category, subcategory, brand, manufacturer, warranty
duration/unit/provider/terms and batch/serial/expiry flags. The original 16-column template
remains supported. Stock batches and Serial units sheets link by SKU; quantities/counts
must equal Opening Stock. Each optional stock sheet allows up to 2,000 rows. Required
expiry, normalised serial uniqueness, tracked-goods constraints and plan features apply.
Create category, subcategory, brand and manufacturer masters first. This business-specific
workbook includes a Masters reference sheet and per-item dropdowns for existing records.
Subcategory uses a plain saved-master list, without dependent INDIRECT formulas.
The Masters sheet shows each subcategory's parent; upload validates that it matches the category.
Spreadsheet apps that do not preserve dropdowns can copy the saved names from Masters.
Regular GST businesses also receive
GST treatment choices and existing GST percentage masters; other businesses have tax columns hidden.
Download again after changing masters. Organisation columns may be left blank and assigned later.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/item-imports/template" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/item-imports/template"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/item-imports/template';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
Binary data - The product import template workbook.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Upload a product file and stage it for review.
requires authentication
Creates no items. The file is stored, checksummed, and validated row by row.
Imports never create masters. Nonblank classification names must match existing records
in this business (case and repeated spaces are ignored); a subcategory must match its category.
Taxable GST percentages supplied by a Regular GST business must match existing GST masters.
Blank organisation/rate cells remain unassigned. Unknown records produce row validation errors.
Optional warranty units are days/months/years and providers brand/manufacturer/shop/both.
External policies need Warranty provider name and at least one Warranty provider phone/email/website.
Optional Warranty provider address and Warranty provider notes appear on issued slips.
The response reports how many rows passed, which rows failed and why, and a sample of the normalized
rows that would be created. Call the confirm endpoint once error_count is zero.
Re-uploading a file this workspace has already uploaded does not stage it a second time:
the existing record is returned with duplicate set to true and HTTP 200. Unconfirmed
files are revalidated against current masters; imported files remain unchanged.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/item-imports" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "file=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/businesses/1/item-imports"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('file', document.querySelector('input[name="file"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/item-imports';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'file',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "The file must be a file of type: xlsx.",
"errors": {
"file": [
"The file must be a file of type: xlsx."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
duplicate
boolean
Whether this file had already been uploaded; unconfirmed records refresh validation, imported records remain unchanged.
data
object
preview
object[]
Up to the first 20 normalized rows, including resolved category_id, subcategory_id, brand_id, manufacturer_id and gst_rate_id (nullable); valid_count reports the true total.
validation_errors
string[]
Every rejected row, each naming its row number in the sheet.
Read a staged import.
requires authentication
Returns the current status, the row counts, every validation error, and the preview sample.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/item-imports/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/item-imports/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/item-imports/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create every validated row.
requires authentication
All rows are created inside one transaction, so the catalogue is either wholly imported or untouched. Rows carrying opening stock also record an opening stock movement and its inventory journal, exactly as the setup wizard does.
The workbook is checked again before writing. Conflicts introduced since preview (including
SKUs or barcodes reserved by deleted items) return 422 under file and create nothing.
Malformed numeric and boolean values are rejected instead of being converted to zero or false.
Existing classification and GST masters are revalidated at confirmation; renamed/deleted masters return 422 under file. No masters are created during preview or confirmation. Unregistered/composition imports retain product tax metadata without creating new masters; that metadata never authorizes collecting GST on new sales.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/item-imports/1/confirm" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/item-imports/1/confirm"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/item-imports/1/confirm';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (409):
{
"message": "This file has already been imported."
}
Example response (409):
{
"message": "This file has not passed validation."
}
Example response (422):
{
"message": "Your current plan includes up to 5 items. Upgrade to Smart Books to add more.",
"errors": {
"plan": [
"Your current plan includes up to 5 items. Upgrade to Smart Books to add more."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
imported_count
integer
Number of items created by this call.
Discard a staged import.
requires authentication
Deletes the stored file and its staged rows. An import that has already created items cannot be discarded — its checksum is what stops the same file being imported twice.
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/item-imports/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/item-imports/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/item-imports/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (409):
{
"message": "An imported file cannot be discarded."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Download the warranty slip PDF for an invoice.
requires authentication
Requires sales permission. Includes current warranty records, their terms and buyer tracking links.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/invoices/1/warranty.pdf" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/1/warranty.pdf"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/1/warranty.pdf';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
binary application/pdf
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Send or resend the invoice and warranty slip to the buyer's primary email.
requires authentication
Requires sales permission, configured mail and a valid customer email. Queued or sending deliveries are reused. Explicit resends may duplicate a message already accepted by mail.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/invoices/1/warranty/send" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/1/warranty/send"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/1/warranty/send';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (202):
{
"data": {
"status": "queued",
"sent_at": null
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Sales invoices
Successful sales create purchase-date warranties from each covered item's policy, including non-serialised goods. Invoice responses expose warranties and warranty_delivery. Automatic primary-customer email includes invoice and warranty PDFs when mail and the recipient are available. Issued policies are snapshots; item edits do not change existing coverage. Returns reduce covered quantities, voiding cancels cover, and coverage corrections supersede old slips.
List saved sales invoices.
requires authentication
Resources include show_gst_details, based on the registration frozen at issue. When false, hide GSTINs, HSN/tax columns, place of supply, reverse charge, GST amounts and tax summaries in invoice and document UI. New unregistered documents force GST/cess to zero and tax-inclusive/reverse-charge flags false. Previously issued registered documents keep their original tax treatment.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/invoices?status=saved&outstanding=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"status\": \"saved\",
\"outstanding\": false,
\"contact_id\": 16,
\"from\": \"2026-01-15\",
\"to\": \"2026-01-15\",
\"search\": \"n\",
\"per_page\": 7
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices"
);
const params = {
"status": "saved",
"outstanding": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"status": "saved",
"outstanding": false,
"contact_id": 16,
"from": "2026-01-15",
"to": "2026-01-15",
"search": "n",
"per_page": 7
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'status' => 'saved',
'outstanding' => '1',
],
'json' => [
'status' => 'saved',
'outstanding' => false,
'contact_id' => 16,
'from' => '2026-01-15',
'to' => '2026-01-15',
'search' => 'n',
'per_page' => 7,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
show_gst_details
boolean
Whether GST details should appear in the issued invoice UI; false for invoices issued as unregistered, based on the frozen registration snapshot.
warranties
object[]
Current sale-linked records with original item/buyer snapshots, duration, provider, terms, starts_on, ends_on, quantity, remaining_quantity, status and buyer_url.
warranty_delivery
object|null
Latest delivery status and sent_at. sent means mail transport acceptance, not inbox delivery.
status_label
string
Display label; the legacy saved status is labelled Saved.
revision
integer
Current invoice revision required when saving an edit.
customer_pdf_url
string
Signed public invoice link showing the complete document with print and PDF sharing actions. The link expires after 90 days.
Save one or many sales invoices.
requires authentication
Send a single invoice payload, or wrap up to 25 of them in an invoices array to save a batch in one atomic request. A batch either saves completely or not at all, and returns its invoices in request order under data with meta.count.
Monthly invoice allowances count all invoices created in the application-calendar month, including trial
and voided invoices. A downgrade does not reset usage. At the limit, new invoices return HTTP 422
against plan with an upgrade/reset message. Existing idempotency keys can still be retried at the limit.
A batch that exceeds the remaining allowance is rolled back completely. Existing invoices remain accessible.
A customer carrying a credit limit is refused with HTTP 422 against contact_id when the invoice would take what it owes past that limit. A batch is weighed cumulatively: each entry sees the exposure the earlier entries created.
GST heads follow the place of supply: CGST and SGST (UTGST in Chandigarh, Dadra and Nagar Haveli and Daman and Diu, Lakshadweep, Andaman and Nicobar Islands and Ladakh) when it is the business's own state, otherwise IGST. A customer with gst_treatment sez is always charged IGST, and an overseas customer defaults to place of supply 96.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/invoices" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"line_layout\": [
{
\"type\": \"header\",
\"title\": \"Kitchen essentials\"
},
{
\"type\": \"item\",
\"line_index\": 0
},
{
\"type\": \"subtotal\"
}
],
\"show_section_totals\": true,
\"contact_id\": 16,
\"issue_date\": \"2026-01-15\",
\"due_date\": \"2026-01-15\",
\"notes\": \"n\",
\"place_of_supply_state_code\": \"29\",
\"reverse_charge\": false,
\"prices_include_tax\": false,
\"channel\": \"backoffice\",
\"idempotency_key\": \"6b72fe4a-5b40-307c-bc24-f79acf9a1bb9\",
\"lines\": [
{
\"details\": \"Deliver in sealed packs.\",
\"item_id\": 16,
\"warehouse_id\": 22,
\"item_batch_id\": 16,
\"serial_numbers\": [
\"n\"
],
\"description\": \"Animi quos velit et fugiat.\",
\"hsn_sac\": \"100630\",
\"quantity\": 18,
\"unit_price\": 22,
\"tax_rate\": 24,
\"cess_rate\": 18,
\"discount\": 8,
\"price_includes_tax\": false
}
],
\"workers\": [
{
\"worker_id\": 66,
\"commission_kind\": \"percentage\",
\"rate_basis_points\": 0,
\"fixed_amount_paise\": 0,
\"attribution_basis_points\": 1
}
]
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"line_layout": [
{
"type": "header",
"title": "Kitchen essentials"
},
{
"type": "item",
"line_index": 0
},
{
"type": "subtotal"
}
],
"show_section_totals": true,
"contact_id": 16,
"issue_date": "2026-01-15",
"due_date": "2026-01-15",
"notes": "n",
"place_of_supply_state_code": "29",
"reverse_charge": false,
"prices_include_tax": false,
"channel": "backoffice",
"idempotency_key": "6b72fe4a-5b40-307c-bc24-f79acf9a1bb9",
"lines": [
{
"details": "Deliver in sealed packs.",
"item_id": 16,
"warehouse_id": 22,
"item_batch_id": 16,
"serial_numbers": [
"n"
],
"description": "Animi quos velit et fugiat.",
"hsn_sac": "100630",
"quantity": 18,
"unit_price": 22,
"tax_rate": 24,
"cess_rate": 18,
"discount": 8,
"price_includes_tax": false
}
],
"workers": [
{
"worker_id": 66,
"commission_kind": "percentage",
"rate_basis_points": 0,
"fixed_amount_paise": 0,
"attribution_basis_points": 1
}
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => \deepclone_from_array([
'classes' => 'stdClass',
'objectMeta' => 3,
'prepared' => [
'line_layout' => [0, 1, 2],
'show_section_totals' => true,
'contact_id' => 16,
'issue_date' => '2026-01-15',
'due_date' => '2026-01-15',
'notes' => 'n',
'place_of_supply_state_code' => '29',
'reverse_charge' => false,
'prices_include_tax' => false,
'channel' => 'backoffice',
'idempotency_key' => '6b72fe4a-5b40-307c-bc24-f79acf9a1bb9',
'lines' => [
[
'details' => 'Deliver in sealed packs.',
'item_id' => 16,
'warehouse_id' => 22,
'item_batch_id' => 16,
'serial_numbers' => ['n'],
'description' => 'Animi quos velit et fugiat.',
'hsn_sac' => '100630',
'quantity' => 18,
'unit_price' => 22,
'tax_rate' => 24,
'cess_rate' => 18,
'discount' => 8,
'price_includes_tax' => false,
],
],
'workers' => [
['worker_id' => 66, 'commission_kind' => 'percentage', 'rate_basis_points' => 0, 'fixed_amount_paise' => 0, 'attribution_basis_points' => 1],
],
],
'mask' => [
'line_layout' => [true, true, true],
],
'properties' => [
'stdClass' => [
'type' => ['header', 'item', 'subtotal'],
'title' => ['Kitchen essentials'],
'line_index' => [1 => 0],
],
],
], null, true),
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "The selected record was not found for this business.",
"errors": {
"invoices.1.contact_id": [
"The selected record was not found for this business."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
document_kind
string
Tax document type. Enum: invoice, tax_invoice, bill_of_supply. A regular business issues a bill of supply when every line is nil-rated, exempt, or non-GST.
data
object
state_tax_label
string
Label for the sgst_* amounts: SGST, or UTGST for a union territory without a legislature.
hsn_summary
object[]
HSN-wise tax summary on a tax invoice, one row per hsn_sac and tax_rate_basis_points with taxable_paise, cgst_paise, sgst_paise, igst_paise, cess_paise, and tax_paise. Empty on a bill of supply or plain invoice.
customer_pdf_url
string
Signed public invoice link showing the complete document with print and PDF sharing actions. The link expires after 90 days.
line_layout
object[]
Display rows, including headers and calculated subtotal markers. Headers and subtotals do not create billable lines or change tax, stock, or accounting totals.
show_section_totals
boolean
Whether to display each header's calculated total.
lines
object
details
string|null
Additional description printed below the item name.
sort_order
integer
Zero-based line position in the returned lines array.
Record one or many customer payments.
requires authentication
Post a single payment to invoices/{invoice}/payments, or send up to 25 payments — each naming its own invoice_id — as a payments array to invoices/payments to settle several invoices in one atomic request.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/invoices/payments" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"payments\": [
{
\"invoice_id\": 16,
\"amount\": 22,
\"method\": \"cash\",
\"payment_account_id\": 16,
\"paid_on\": \"2026-01-15\",
\"reference\": \"n\",
\"notes\": \"g\"
}
]
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/payments"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"payments": [
{
"invoice_id": 16,
"amount": 22,
"method": "cash",
"payment_account_id": 16,
"paid_on": "2026-01-15",
"reference": "n",
"notes": "g"
}
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/payments';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'payments' => [
['invoice_id' => 16, 'amount' => 22, 'method' => 'cash', 'payment_account_id' => 16, 'paid_on' => '2026-01-15', 'reference' => 'n', 'notes' => 'g'],
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "Enter an amount up to the current outstanding balance.",
"errors": {
"payments.1.amount": [
"Enter an amount up to the current outstanding balance."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
customer_pdf_url
string
Signed public invoice link showing the complete document with print and PDF sharing actions. The link expires after 90 days.
Void one or many unpaid invoices that have no returns.
requires authentication
Invoices with a payment or a sales return must be settled through the corresponding payment or return workflow and cannot be voided.
Void a single invoice at invoices/{invoice}/void, or send up to 25 entries — each naming its own invoice_id — as an invoices array to invoices/void. A batch may carry one shared request-level reason instead of repeating it per entry, and either voids completely or not at all.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/invoices/void" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"invoices\": [
{
\"invoice_id\": 16,
\"reason\": \"n\"
}
],
\"reason\": \"b\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/void"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"invoices": [
{
"invoice_id": 16,
"reason": "n"
}
],
"reason": "b"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/void';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'invoices' => [
['invoice_id' => 16, 'reason' => 'n'],
],
'reason' => 'b',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "Only unpaid, active invoices without returns can be voided.",
"errors": {
"invoice": [
"Only unpaid, active invoices without returns can be voided."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
customer_pdf_url
string
Signed public invoice link showing the complete document with print and PDF sharing actions. The link expires after 90 days.
Read an invoice and its change history.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/invoices/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"customer_pdf_url": "https://dukanam.com/invoices/1/shared?expires=1790000000&signature=example",
"send": {
"share_url": "https://dukanam.com/shared/invoices/1?expires=1790000000&signature=example",
"message": "Hi Ravi Kumar, please find invoice INV-1001 for ₹2,500.00 from Sri Lakshmi Traders. Thank you.\n\nView invoice PDF: https://dukanam.com/shared/invoices/1?expires=1790000000&signature=example",
"email_subject": "Invoice INV-1001 from Sri Lakshmi Traders",
"email_available": true,
"payment_reminder": {
"message": "Hi Ravi Kumar, a payment of ₹2,500.00 is pending on invoice INV-1001. Pay to Sri Lakshmi Traders: Account number: 001234567890, IFSC: HDFC0000123. View invoice PDF: https://dukanam.com/shared/invoices/1?expires=1790000000&signature=example",
"email_subject": "Sri Lakshmi Traders | Payment reminder: INV-1001"
},
"recipients": [
{
"key": "primary",
"name": "Ravi Kumar",
"designation": "Primary contact",
"phone": "98765 43210",
"whatsapp_phone": "919876543210",
"email": "[email protected]"
},
{
"key": "person-12",
"name": "Priya",
"designation": "Accounts",
"phone": null,
"whatsapp_phone": null,
"email": "[email protected]"
}
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
revision
integer
Current invoice revision required when saving an edit.
status_label
string
Display label; the legacy saved status is labelled Saved.
history
object[]
Invoice audit history, newest first, scoped to this business and invoice.
user
object
Actor identifier and name, or null if unavailable.
old_values
object
Complete invoice and line snapshot before a saved edit.
new_values
object
Complete invoice and line snapshot after a saved edit.
created_at
string
ISO-8601 timestamp of the action.
lines
object
cess_rate_basis_points
integer
Cess percentage multiplied by 100.
price_includes_tax
boolean
Whether the line unit price includes tax.
gst_taxability
string
Stored GST taxability for this line.
state_tax_label
string
Label for the sgst_* amounts: SGST, or UTGST for a union territory without a legislature.
hsn_summary
object[]
HSN-wise tax summary on a tax invoice, one row per hsn_sac and tax_rate_basis_points with taxable_paise, cgst_paise, sgst_paise, igst_paise, cess_paise, and tax_paise. Empty on a bill of supply or plain invoice.
customer_pdf_url
string
Signed public invoice link showing the complete document with print and PDF sharing actions. The link expires after 90 days.
send
object
What to send and who can receive it. Returned on this endpoint only.
share_url
string
Signed public invoice link to include in a WhatsApp or SMS message.
message
string
Suggested message, ending with the public link. Uses the business's invoice sharing template.
payment_reminder
object|null
Suggested customer payment reminder with message and email_subject, including the outstanding amount, due date, visible receiving accounts and invoice link. Null for settled or void invoices. Clients let the shop review and send through the existing email endpoint or WhatsApp/SMS; scheduled push alerts still go to workspace staff.
email_subject
string
Suggested email subject.
email_available
boolean
Whether this app can send email; when false, offer WhatsApp and SMS only.
recipients
object[]
People at the customer with a phone or email: the primary contact (key primary) then each additional contact (key person-{id}), each with name, designation, phone, whatsapp_phone (digits with country code, or null) and email.
Save edits to an invoice at any time.
requires authentication
PUT and PATCH both replace the editable invoice fields and lines in full. Send the latest revision from GET and include each existing line's id to retain it; omit id for a new line. Missing existing lines are removed. A stale revision returns 422 on revision. The invoice keeps its identifier, number, payment records, and return records. All statuses, including paid, returned, and void, remain editable with no time limit; a void invoice remains void.
The customer cannot change when payments or returns exist. Returned lines cannot be removed, assigned another item, or reduced below their returned quantity or recorded amounts. Reducing the total below payments retains customer credit. Totals, stock and accounting reconcile atomically, and each save records complete before/after snapshots with the actor and timestamp. Accounting corrections retain original entries and dated reversals/replacements; report date ranges honor each entry's date. Original business tax registration and stored line metadata are preserved.
Invoice edits keep the original invoice date unless the owner explicitly changes issue_date. Revision reversals use the previous transaction dates; replacements use the submitted invoice date, restating that period rather than moving the correction to today. The edit timestamp remains in audit history. There is no invoice-age limit on changing the date.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/businesses/1/invoices/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"line_layout\": [
{
\"type\": \"header\",
\"title\": \"Kitchen essentials\"
},
{
\"type\": \"item\",
\"line_index\": 0
},
{
\"type\": \"subtotal\"
}
],
\"show_section_totals\": true,
\"worker_attribution_basis_points\": 10000,
\"worker_id\": 16,
\"worker_rate_basis_points\": 1,
\"contact_id\": 16,
\"issue_date\": \"2026-01-15\",
\"due_date\": \"2026-01-15\",
\"notes\": \"n\",
\"place_of_supply_state_code\": 1,
\"reverse_charge\": false,
\"prices_include_tax\": false,
\"lines\": [
{
\"details\": \"Deliver in sealed packs.\",
\"item_id\": 16,
\"warehouse_id\": 22,
\"item_batch_id\": 16,
\"serial_numbers\": [
\"n\"
],
\"description\": \"Animi quos velit et fugiat.\",
\"hsn_sac\": \"100630\",
\"quantity\": 18,
\"unit_price\": 22,
\"tax_rate\": 24,
\"cess_rate\": 18,
\"discount\": 8,
\"price_includes_tax\": false,
\"id\": 1
}
],
\"revision\": 1,
\"workers\": [
{
\"worker_id\": 27,
\"commission_kind\": \"percentage\",
\"rate_basis_points\": 0,
\"fixed_amount_paise\": 0,
\"attribution_basis_points\": 2
}
]
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"line_layout": [
{
"type": "header",
"title": "Kitchen essentials"
},
{
"type": "item",
"line_index": 0
},
{
"type": "subtotal"
}
],
"show_section_totals": true,
"worker_attribution_basis_points": 10000,
"worker_id": 16,
"worker_rate_basis_points": 1,
"contact_id": 16,
"issue_date": "2026-01-15",
"due_date": "2026-01-15",
"notes": "n",
"place_of_supply_state_code": 1,
"reverse_charge": false,
"prices_include_tax": false,
"lines": [
{
"details": "Deliver in sealed packs.",
"item_id": 16,
"warehouse_id": 22,
"item_batch_id": 16,
"serial_numbers": [
"n"
],
"description": "Animi quos velit et fugiat.",
"hsn_sac": "100630",
"quantity": 18,
"unit_price": 22,
"tax_rate": 24,
"cess_rate": 18,
"discount": 8,
"price_includes_tax": false,
"id": 1
}
],
"revision": 1,
"workers": [
{
"worker_id": 27,
"commission_kind": "percentage",
"rate_basis_points": 0,
"fixed_amount_paise": 0,
"attribution_basis_points": 2
}
]
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/1';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => \deepclone_from_array([
'classes' => 'stdClass',
'objectMeta' => 3,
'prepared' => [
'line_layout' => [0, 1, 2],
'show_section_totals' => true,
'worker_attribution_basis_points' => 10000,
'worker_id' => 16,
'worker_rate_basis_points' => 1,
'contact_id' => 16,
'issue_date' => '2026-01-15',
'due_date' => '2026-01-15',
'notes' => 'n',
'place_of_supply_state_code' => 1,
'reverse_charge' => false,
'prices_include_tax' => false,
'lines' => [
[
'details' => 'Deliver in sealed packs.',
'item_id' => 16,
'warehouse_id' => 22,
'item_batch_id' => 16,
'serial_numbers' => ['n'],
'description' => 'Animi quos velit et fugiat.',
'hsn_sac' => '100630',
'quantity' => 18,
'unit_price' => 22,
'tax_rate' => 24,
'cess_rate' => 18,
'discount' => 8,
'price_includes_tax' => false,
'id' => 1,
],
],
'revision' => 1,
'workers' => [
['worker_id' => 27, 'commission_kind' => 'percentage', 'rate_basis_points' => 0, 'fixed_amount_paise' => 0, 'attribution_basis_points' => 2],
],
],
'mask' => [
'line_layout' => [true, true, true],
],
'properties' => [
'stdClass' => [
'type' => ['header', 'item', 'subtotal'],
'title' => ['Kitchen essentials'],
'line_index' => [1 => 0],
],
],
], null, true),
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 1,
"number": "INV-001",
"status": "posted",
"status_label": "Saved",
"revision": 2,
"total_paise": 20000,
"history": [
{
"id": 2,
"action": "invoice.updated",
"user": {
"id": 1,
"name": "Shop owner"
},
"old_values": {
"revision": 1,
"total_paise": 10000,
"lines": [
{
"id": 1,
"quantity": "1.000"
}
]
},
"new_values": {
"revision": 2,
"total_paise": 20000,
"lines": [
{
"id": 1,
"quantity": "2.000"
}
]
},
"reason": null,
"created_at": "2026-09-13T10:00:00.000000Z"
}
]
}
}
Example response (422):
{
"message": "This invoice was changed by someone else. Reload it before saving your changes.",
"errors": {
"revision": [
"This invoice was changed by someone else. Reload it before saving your changes."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
revision
integer
New revision after the edit is saved.
status_label
string
Display label; the legacy saved status is labelled Saved.
history
object[]
Invoice audit records with id, action, user (id and name), old_values, new_values, reason, and created_at. IP addresses, user agents, and internal audit metadata are omitted.
customer_pdf_url
string
Signed public link showing the current invoice document.
line_layout
object[]
Display rows, including headers and calculated subtotal markers. Headers and subtotals do not create billable lines or change tax, stock, or accounting totals.
show_section_totals
boolean
Whether to display each header's calculated total.
lines
object
details
string|null
Additional description printed below the item name.
sort_order
integer
Zero-based line position in the returned lines array.
Download a sales invoice PDF.
requires authentication
Returns the same tenant-scoped invoice document used by the web sharing flow.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/invoices/1/pdf" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/1/pdf"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/1/pdf';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
Binary data - The generated invoice PDF.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get a sales invoice as a thermal printer receipt.
requires authentication
Returns the receipt as ESC/POS printer commands for a 58mm or 80mm thermal printer, so the app can print over Bluetooth, USB or the network with no print dialog. Decode content_base64 and write the bytes to the printer unchanged. The same bytes are served to the web counter. The supermarket layout includes bill/date/cashier, aligned description/quantity/rate/amount columns, per-line discounts, item/quantity counts, payment methods, savings and a GST-rate summary using stored invoice values. Long descriptions wrap and large amounts retain all digits.
Pass cash_register_id to use that counter's saved printer settings. The cash drawer opens only when the counter allows it and the sale has a cash payment. A UPI QR prints only while a balance is due, for that balance. Text prints in the printer's built-in ASCII font; the rupee sign prints as Rs. and other characters are transliterated.
Available to members who can make sales or use the POS.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/invoices/1/receipt?cash_register_id=1&paper=58mm&cut=&open_drawer=" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/1/receipt"
);
const params = {
"cash_register_id": "1",
"paper": "58mm",
"cut": "0",
"open_drawer": "0",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/1/receipt';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'cash_register_id' => '1',
'paper' => '58mm',
'cut' => '0',
'open_drawer' => '0',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"format": "escpos",
"invoice_id": 1,
"invoice_number": "INV-0001",
"cash_register_id": 1,
"paper": "80mm",
"characters_per_line": 48,
"cut": true,
"opens_drawer": true,
"auto_print": false,
"content_base64": "G0AbcAAZ+htHYQE...",
"byte_length": 812,
"text": "Sri Lakshmi Traders\nTAX INVOICE\nBill: INV-0001 28 Sep 2026"
}
}
Example response (422):
{
"message": "Choose 58mm or 80mm paper.",
"errors": {
"paper": [
"Choose 58mm or 80mm paper."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
format
string
Always escpos.
paper
string
Paper width used: 58mm or 80mm.
characters_per_line
integer
32 on 58mm, 48 on 80mm.
cut
boolean
Whether the bytes end with a paper cut.
opens_drawer
boolean
Whether the bytes open the cash drawer.
auto_print
boolean
The counter's setting to print as soon as a sale is saved. False without a counter.
content_base64
string
ESC/POS bytes, base64-encoded. Send to the printer as-is.
byte_length
integer
Length of the decoded bytes.
text
string
Plain-text copy of what prints, including customer-visible bank/payment instructions, for an on-screen preview.
Email an invoice to people at the customer.
requires authentication
Sends one email to each chosen person with the invoice PDF attached and a button to the signed public invoice page. Choose people by the key values in send.recipients from GET invoices/{invoice}; each must have an email address. To send on WhatsApp or SMS instead, open the chat on the device with send.message and a recipient's whatsapp_phone or phone. Every email send is recorded in the audit log.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/invoices/1/email" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"recipients\": [
\"primary\",
\"person-12\"
],
\"recipient_emails\": {
\"primary\": \"[email protected]\"
},
\"subject\": \"Sri Lakshmi Traders | Purchase order: PO-0007\",
\"message\": \"Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.\",
\"from\": \"2026-04-01\",
\"to\": \"2027-03-31\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/1/email"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"recipients": [
"primary",
"person-12"
],
"recipient_emails": {
"primary": "[email protected]"
},
"subject": "Sri Lakshmi Traders | Purchase order: PO-0007",
"message": "Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.",
"from": "2026-04-01",
"to": "2027-03-31"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/1/email';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'recipients' => ['primary', 'person-12'],
'recipient_emails' => ['primary' => '[email protected]'],
'subject' => 'Sri Lakshmi Traders | Purchase order: PO-0007',
'message' => 'Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.',
'from' => '2026-04-01',
'to' => '2027-03-31',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (202):
{
"message": "Emailed to Ravi Kumar and Priya.",
"data": {
"sent_to": [
{
"key": "primary",
"name": "Ravi Kumar",
"email": "[email protected]"
},
{
"key": "person-12",
"name": "Priya",
"email": "[email protected]"
}
]
}
}
Example response (422):
{
"message": "Priya has no email address.",
"errors": {
"recipients.1": [
"Priya has no email address."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Record one or many customer payments.
requires authentication
Post a single payment to invoices/{invoice}/payments, or send up to 25 payments — each naming its own invoice_id — as a payments array to invoices/payments to settle several invoices in one atomic request.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/invoices/1/payments" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"payments\": [
{
\"invoice_id\": 16,
\"amount\": 22,
\"method\": \"cash\",
\"payment_account_id\": 16,
\"paid_on\": \"2026-01-15\",
\"reference\": \"n\",
\"notes\": \"g\"
}
]
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/1/payments"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"payments": [
{
"invoice_id": 16,
"amount": 22,
"method": "cash",
"payment_account_id": 16,
"paid_on": "2026-01-15",
"reference": "n",
"notes": "g"
}
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/1/payments';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'payments' => [
['invoice_id' => 16, 'amount' => 22, 'method' => 'cash', 'payment_account_id' => 16, 'paid_on' => '2026-01-15', 'reference' => 'n', 'notes' => 'g'],
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "Enter an amount up to the current outstanding balance.",
"errors": {
"payments.1.amount": [
"Enter an amount up to the current outstanding balance."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
customer_pdf_url
string
Signed public invoice link showing the complete document with print and PDF sharing actions. The link expires after 90 days.
Void one or many unpaid invoices that have no returns.
requires authentication
Invoices with a payment or a sales return must be settled through the corresponding payment or return workflow and cannot be voided.
Void a single invoice at invoices/{invoice}/void, or send up to 25 entries — each naming its own invoice_id — as an invoices array to invoices/void. A batch may carry one shared request-level reason instead of repeating it per entry, and either voids completely or not at all.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/invoices/1/void" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"invoices\": [
{
\"invoice_id\": 16,
\"reason\": \"n\"
}
],
\"reason\": \"b\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/1/void"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"invoices": [
{
"invoice_id": 16,
"reason": "n"
}
],
"reason": "b"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/1/void';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'invoices' => [
['invoice_id' => 16, 'reason' => 'n'],
],
'reason' => 'b',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "Only unpaid, active invoices without returns can be voided.",
"errors": {
"invoice": [
"Only unpaid, active invoices without returns can be voided."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
customer_pdf_url
string
Signed public invoice link showing the complete document with print and PDF sharing actions. The link expires after 90 days.
Business documents
Get recurring schedule settings and generation history.
requires authentication
Requires sales permission and recurring-invoice entitlement. Returns the schedule, customer recipients (including contacts missing an email) and the latest 25 occurrences. Owner notification preference defaults on; a missing owner email is not configured.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/documents/architecto/recurring-settings" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/architecto/recurring-settings"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/architecto/recurring-settings';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
recurring_settings
object
Frequency multiplier, due days, email preferences, selected recipient keys, owner email and original date anchor.
status_label
string
Readable schedule status, such as Active or Paused.
recurring_runs
object[]
Latest occurrences with invoice_id, scheduled_on, creation status/error and per-recipient email status. Sent means accepted by the mail transport, not inbox delivery.
Update a recurring schedule and automatic email preferences.
requires authentication
Changes affect future invoices only. Start/next date must follow the last generated date. Monthly dates stay anchored to their original day, clamped to shorter months. Creation uses the business timezone. Customer auto-email requires selected valid emails; missing addresses can be saved with recipient_emails. Existing emails cannot be overwritten. Owner notifications default on but missing email never blocks invoice creation. Pause with schedule_status=paused, resume with active; set a future next date to skip backlog. Requires sales permission and recurring-invoice entitlement; foreign schedules return 404.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/businesses/1/documents/architecto/recurring-settings" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"frequency\": \"monthly\",
\"next_issue_date\": \"2026-10-31\",
\"end_date\": \"2027-10-31\",
\"repeat_every\": 1,
\"due_after_days\": 7,
\"auto_email\": true,
\"email_recipients\": [
\"primary\"
],
\"recipient_emails\": {
\"primary\": \"[email protected]\"
},
\"notify_owner\": true,
\"owner_email\": \"[email protected]\",
\"schedule_status\": \"active\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/architecto/recurring-settings"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"frequency": "monthly",
"next_issue_date": "2026-10-31",
"end_date": "2027-10-31",
"repeat_every": 1,
"due_after_days": 7,
"auto_email": true,
"email_recipients": [
"primary"
],
"recipient_emails": {
"primary": "[email protected]"
},
"notify_owner": true,
"owner_email": "[email protected]",
"schedule_status": "active"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/architecto/recurring-settings';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'frequency' => 'monthly',
'next_issue_date' => '2026-10-31',
'end_date' => '2027-10-31',
'repeat_every' => 1,
'due_after_days' => 7,
'auto_email' => true,
'email_recipients' => ['primary'],
'recipient_emails' => ['primary' => '[email protected]'],
'notify_owner' => true,
'owner_email' => '[email protected]',
'schedule_status' => 'active',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
status_label
string
Readable schedule status, such as Active or Paused.
Retry failed emails for an existing recurring invoice.
requires authentication
Never creates another invoice or resends recipients already marked sent. Missing-address skips require updating the contact and sending that invoice manually. Queue retries are automatic (three attempts); this endpoint restarts exhausted pending/failed delivery. Requires sales permission and recurring-invoice entitlement; foreign runs return 404.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/documents/architecto/recurring-runs/architecto/retry" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/architecto/recurring-runs/architecto/retry"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/architecto/recurring-runs/architecto/retry';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (202):
{
"message": "Unsent emails queued for retry."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List business documents.
requires authentication
Saving a bill updates stock and account balances automatically. There is no extra confirmation step. Display status_label in screens; raw status values are retained for existing integrations.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/documents" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"type\": \"quote\",
\"status\": \"b\",
\"contact_id\": 16,
\"per_page\": 22
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"type": "quote",
"status": "b",
"contact_id": 16,
"per_page": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'type' => 'quote',
'status' => 'b',
'contact_id' => 16,
'per_page' => 22,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
status_label
string
Display status: Saved for posted, Voided for void, or the readable document status.
show_gst_details
boolean
Whether to display GST details, based on the registration snapshot frozen at issue. False for unregistered documents.
Create a business document.
requires authentication
Document resources include show_gst_details, based on the registration frozen at issue. When false, hide GST identifiers, HSN/tax columns, supply and reverse-charge details, GST amounts and tax summaries. Previously issued registered documents keep their original tax treatment.
Create a business document or recurring invoice schedule.
Tax is split by place of supply as on sales invoices: CGST and SGST (or UTGST) within the business's state, IGST across states and for SEZ parties. Responses carry place_of_supply_state_code, supply_type, state_tax_label, the cgst_total_paise, sgst_total_paise, igst_total_paise and cess_total_paise totals, and per-line hsn_sac and tax heads.
Sales and purchase returns retain the source line's item even after it has been soft deleted,
including its inventory, batch, and serial handling. Deleted items cannot be selected on new documents.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/documents/quote|purchase_order|purchase_invoice|recurring_invoice|sales_return|purchase_return|delivery_challan" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"line_layout\": [
{
\"type\": \"header\",
\"title\": \"Kitchen essentials\"
},
{
\"type\": \"item\",
\"line_index\": 0
},
{
\"type\": \"subtotal\"
}
],
\"show_section_totals\": true,
\"contact_id\": 16,
\"source_invoice_id\": 16,
\"source_document_id\": 16,
\"issue_date\": \"2026-01-15\",
\"due_date\": \"2026-01-15\",
\"frequency\": \"weekly\",
\"next_issue_date\": \"2026-01-15\",
\"end_date\": \"2026-01-15\",
\"notes\": \"n\",
\"external_reference\": \"g\",
\"challan_reason\": \"job_work\",
\"destination_warehouse_id\": 66,
\"expected_return_on\": \"2026-01-15\",
\"place_of_supply_state_code\": \"29\",
\"reverse_charge\": false,
\"prices_include_tax\": false,
\"lines\": [
{
\"details\": \"Deliver in sealed packs.\",
\"item_id\": 16,
\"warehouse_id\": 22,
\"item_batch_id\": 16,
\"batch_number\": \"n\",
\"batch_expiry_date\": \"2026-01-15\",
\"batch_manufactured_on\": \"2026-01-15\",
\"batch_mrp\": 7,
\"serial_numbers\": [
\"z\"
],
\"warranty_until\": \"2026-01-15\",
\"source_invoice_line_id\": 16,
\"source_document_line_id\": 16,
\"description\": \"Et animi quos velit et fugiat.\",
\"hsn_sac\": \"998719\\n\\nRecurring schedules support the options below, with the same validation as the\\nrecurring-settings endpoint. Owner notification defaults on; without an owner email,\\nhistory remains available and invoice creation continues. Customer auto-email defaults off.\",
\"quantity\": 18,
\"unit_price\": 22,
\"tax_rate\": 24,
\"cess_rate\": 18,
\"discount\": 8,
\"price_includes_tax\": false
}
],
\"repeat_every\": 1,
\"due_after_days\": 7,
\"auto_email\": false,
\"email_recipients\": [
\"primary\"
],
\"recipient_emails\": {
\"primary\": \"[email protected]\"
},
\"notify_owner\": true,
\"owner_email\": \"[email protected]\",
\"schedule_status\": \"active\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/quote|purchase_order|purchase_invoice|recurring_invoice|sales_return|purchase_return|delivery_challan"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"line_layout": [
{
"type": "header",
"title": "Kitchen essentials"
},
{
"type": "item",
"line_index": 0
},
{
"type": "subtotal"
}
],
"show_section_totals": true,
"contact_id": 16,
"source_invoice_id": 16,
"source_document_id": 16,
"issue_date": "2026-01-15",
"due_date": "2026-01-15",
"frequency": "weekly",
"next_issue_date": "2026-01-15",
"end_date": "2026-01-15",
"notes": "n",
"external_reference": "g",
"challan_reason": "job_work",
"destination_warehouse_id": 66,
"expected_return_on": "2026-01-15",
"place_of_supply_state_code": "29",
"reverse_charge": false,
"prices_include_tax": false,
"lines": [
{
"details": "Deliver in sealed packs.",
"item_id": 16,
"warehouse_id": 22,
"item_batch_id": 16,
"batch_number": "n",
"batch_expiry_date": "2026-01-15",
"batch_manufactured_on": "2026-01-15",
"batch_mrp": 7,
"serial_numbers": [
"z"
],
"warranty_until": "2026-01-15",
"source_invoice_line_id": 16,
"source_document_line_id": 16,
"description": "Et animi quos velit et fugiat.",
"hsn_sac": "998719\n\nRecurring schedules support the options below, with the same validation as the\nrecurring-settings endpoint. Owner notification defaults on; without an owner email,\nhistory remains available and invoice creation continues. Customer auto-email defaults off.",
"quantity": 18,
"unit_price": 22,
"tax_rate": 24,
"cess_rate": 18,
"discount": 8,
"price_includes_tax": false
}
],
"repeat_every": 1,
"due_after_days": 7,
"auto_email": false,
"email_recipients": [
"primary"
],
"recipient_emails": {
"primary": "[email protected]"
},
"notify_owner": true,
"owner_email": "[email protected]",
"schedule_status": "active"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/quote|purchase_order|purchase_invoice|recurring_invoice|sales_return|purchase_return|delivery_challan';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => \deepclone_from_array([
'classes' => 'stdClass',
'objectMeta' => 3,
'prepared' => [
'line_layout' => [0, 1, 2],
'show_section_totals' => true,
'contact_id' => 16,
'source_invoice_id' => 16,
'source_document_id' => 16,
'issue_date' => '2026-01-15',
'due_date' => '2026-01-15',
'frequency' => 'weekly',
'next_issue_date' => '2026-01-15',
'end_date' => '2026-01-15',
'notes' => 'n',
'external_reference' => 'g',
'challan_reason' => 'job_work',
'destination_warehouse_id' => 66,
'expected_return_on' => '2026-01-15',
'place_of_supply_state_code' => '29',
'reverse_charge' => false,
'prices_include_tax' => false,
'lines' => [
[
'details' => 'Deliver in sealed packs.',
'item_id' => 16,
'warehouse_id' => 22,
'item_batch_id' => 16,
'batch_number' => 'n',
'batch_expiry_date' => '2026-01-15',
'batch_manufactured_on' => '2026-01-15',
'batch_mrp' => 7,
'serial_numbers' => ['z'],
'warranty_until' => '2026-01-15',
'source_invoice_line_id' => 16,
'source_document_line_id' => 16,
'description' => 'Et animi quos velit et fugiat.',
'hsn_sac' => '998719'."\n"
."\n"
.'Recurring schedules support the options below, with the same validation as the'."\n"
.'recurring-settings endpoint. Owner notification defaults on; without an owner email,'."\n"
.'history remains available and invoice creation continues. Customer auto-email defaults off.',
'quantity' => 18,
'unit_price' => 22,
'tax_rate' => 24,
'cess_rate' => 18,
'discount' => 8,
'price_includes_tax' => false,
],
],
'repeat_every' => 1,
'due_after_days' => 7,
'auto_email' => false,
'email_recipients' => ['primary'],
'recipient_emails' => ['primary' => '[email protected]'],
'notify_owner' => true,
'owner_email' => '[email protected]',
'schedule_status' => 'active',
],
'mask' => [
'line_layout' => [true, true, true],
],
'properties' => [
'stdClass' => [
'type' => ['header', 'item', 'subtotal'],
'title' => ['Kitchen essentials'],
'line_index' => [1 => 0],
],
],
], null, true),
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
show_gst_details
boolean
Whether GST details should appear in the issued document UI, based on the frozen registration snapshot.
status_label
string
Display status: Saved for posted, Voided for void, or the readable document status.
line_layout
object[]
Display rows, including headers and calculated subtotal markers. Headers and subtotals do not create billable lines or change tax, stock, or accounting totals.
show_section_totals
boolean
Whether to display each header's calculated total.
lines
object
details
string|null
Additional description printed below the item name.
sort_order
integer
Zero-based line position in the returned lines array.
GET api/v1/businesses/{business}/documents/{document_id}
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/documents/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
attachments
object[]
Attached supplier invoice files on this detail endpoint: id, name, mime_type, size_bytes, created_at and authenticated download_url. Empty when none are attached.
send
object
Present on quotations, purchase orders, delivery challans, sales returns and purchase returns that are not voided: what to send and who at the party can receive it. Returned on this endpoint only.
share_url
string
Signed public link to the document, valid for 90 days. For a quotation it is the approval page, the same as customer_approval_url.
message
string
Suggested WhatsApp, SMS or email message, ending with the public link.
email_subject
string
Suggested email subject.
email_available
boolean
Whether this app can send email; when false, offer WhatsApp and SMS only.
recipients
object[]
People at the party with a phone or email: the primary contact (key primary) then each additional contact (key person-{id}), each with name, designation, phone, whatsapp_phone (digits with country code, or null) and email.
status_label
string
Display status: Saved for posted, Voided for void, or the readable document status.
Update a quotation, purchase order, purchase invoice, or delivery challan.
requires authentication
Replaces the party, dates, note, and every line of a quotation, purchase order, or delivery challan that
has not been converted yet. A quotation that the customer already answered returns to open and its
recorded decision is cleared, so the same approval link asks for a fresh decision on the updated figures.
A delivery challan that had already taken goods off the shelf returns them before the replacement lines
take out their own, so stock is never held out twice for one challan.
Purchase invoices replace stock, supplier ledger, and accounting entries atomically, retaining their number.
Invoice edits keep the original invoice date unless the owner explicitly changes issue_date. Purchase revision reversals use the previous transaction dates and replacements use the submitted invoice date, restating that period. The audit timestamp records when the edit occurred. There is no invoice-age limit on changing the date; existing stock, return, and payment safeguards still apply.
Returns, batch or serial tracked goods, and later stock activity prevent purchase invoice edits (422).
Paid invoices retain payments: the supplier cannot change and the total cannot fall below applied payments.
The complete contact_id, issue_date, and lines payload is required; omitted optional fields are cleared.
Items already on this document can be retained after soft deletion. Other deleted items
and foreign-tenant items return 404; deletion never bypasses the existing edit restrictions.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/documents/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"line_layout\": [
{
\"type\": \"header\",
\"title\": \"Kitchen essentials\"
},
{
\"type\": \"item\",
\"line_index\": 0
},
{
\"type\": \"subtotal\"
}
],
\"show_section_totals\": true,
\"contact_id\": 16,
\"issue_date\": \"2026-01-15\",
\"due_date\": \"2026-01-15\",
\"notes\": \"n\",
\"external_reference\": \"g\",
\"challan_reason\": \"job_work\",
\"destination_warehouse_id\": 66,
\"expected_return_on\": \"2026-01-15\",
\"place_of_supply_state_code\": 1,
\"reverse_charge\": false,
\"prices_include_tax\": false,
\"lines\": [
{
\"details\": \"Deliver in sealed packs.\",
\"item_id\": 16,
\"warehouse_id\": 22,
\"item_batch_id\": 16,
\"serial_numbers\": [
\"n\"
],
\"description\": \"Animi quos velit et fugiat.\",
\"hsn_sac\": \"5593:14):23)\",
\"quantity\": 18,
\"unit_price\": 22,
\"tax_rate\": 24,
\"cess_rate\": 18,
\"discount\": 8,
\"price_includes_tax\": false
}
]
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"line_layout": [
{
"type": "header",
"title": "Kitchen essentials"
},
{
"type": "item",
"line_index": 0
},
{
"type": "subtotal"
}
],
"show_section_totals": true,
"contact_id": 16,
"issue_date": "2026-01-15",
"due_date": "2026-01-15",
"notes": "n",
"external_reference": "g",
"challan_reason": "job_work",
"destination_warehouse_id": 66,
"expected_return_on": "2026-01-15",
"place_of_supply_state_code": 1,
"reverse_charge": false,
"prices_include_tax": false,
"lines": [
{
"details": "Deliver in sealed packs.",
"item_id": 16,
"warehouse_id": 22,
"item_batch_id": 16,
"serial_numbers": [
"n"
],
"description": "Animi quos velit et fugiat.",
"hsn_sac": "5593:14):23)",
"quantity": 18,
"unit_price": 22,
"tax_rate": 24,
"cess_rate": 18,
"discount": 8,
"price_includes_tax": false
}
]
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/1';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => \deepclone_from_array([
'classes' => 'stdClass',
'objectMeta' => 3,
'prepared' => [
'line_layout' => [0, 1, 2],
'show_section_totals' => true,
'contact_id' => 16,
'issue_date' => '2026-01-15',
'due_date' => '2026-01-15',
'notes' => 'n',
'external_reference' => 'g',
'challan_reason' => 'job_work',
'destination_warehouse_id' => 66,
'expected_return_on' => '2026-01-15',
'place_of_supply_state_code' => 1,
'reverse_charge' => false,
'prices_include_tax' => false,
'lines' => [
[
'details' => 'Deliver in sealed packs.',
'item_id' => 16,
'warehouse_id' => 22,
'item_batch_id' => 16,
'serial_numbers' => ['n'],
'description' => 'Animi quos velit et fugiat.',
'hsn_sac' => '5593:14):23)',
'quantity' => 18,
'unit_price' => 22,
'tax_rate' => 24,
'cess_rate' => 18,
'discount' => 8,
'price_includes_tax' => false,
],
],
],
'mask' => [
'line_layout' => [true, true, true],
],
'properties' => [
'stdClass' => [
'type' => ['header', 'item', 'subtotal'],
'title' => ['Kitchen essentials'],
'line_index' => [1 => 0],
],
],
], null, true),
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
status_label
string
Display status: Saved for posted, Voided for void, or the readable document status.
line_layout
object[]
Display rows, including headers and calculated subtotal markers. Headers and subtotals do not create billable lines or change tax, stock, or accounting totals.
show_section_totals
boolean
Whether to display each header's calculated total.
lines
object
details
string|null
Additional description printed below the item name.
sort_order
integer
Zero-based line position in the returned lines array.
Delete a purchase invoice.
requires authentication
Requires purchases permission. Reverses stock, supplier ledger and accounting atomically,
retaining the document with status void for audit. Outstanding becomes zero.
Payments, applied advances, purchase returns, batch or serial tracked goods, later stock
activity, and already voided invoices prevent deletion (422). Other document types return 404.
Any failed deletion leaves the invoice and all balances unchanged.
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/documents/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"message": "Purchase invoice deleted. Audit record retained."
}
Example response (422):
{
"message": "This invoice has payments or applied advances and cannot be deleted.",
"errors": {
"status": [
"This invoice has payments or applied advances and cannot be deleted."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Convert a document into the bill it becomes.
requires authentication
A quotation and a delivery challan both become a tax invoice; a purchase order becomes a purchase
invoice. A delivery challan may be converted exactly once: a second attempt answers 422, leaving the
first invoice as the only bill for those goods. GST on a converted challan is computed at the conversion
date, because the price is frequently not settled when the goods leave the shop.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/documents/1/convert" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/1/convert"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/1/convert';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
status_label
string
Readable bill status, such as Saved or Partially paid.
Download a document PDF.
requires authentication
The same A4 paper the party receives, for quotations, purchase orders, delivery challans, sales returns and purchase returns. Other document types answer 404.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/documents/1/pdf" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/1/pdf"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/1/pdf';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
Binary data - The generated document PDF.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Email a document to people at the party.
requires authentication
Sends one email to each chosen person with the document PDF attached and a button to its signed public page. Works for quotations, purchase orders, purchase invoices, recurring invoice schedules, delivery challans, sales returns and purchase returns that are not voided. Delivery is queued through platform Cloudflare email with the shop’s branding. Choose people by the key values in send.recipients from GET documents/{document}; each must have an email address. To send on WhatsApp or SMS instead, open the chat on the device with send.message and a recipient's whatsapp_phone or phone. Every email send is recorded in the audit log.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/documents/1/email" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"recipients\": [
\"primary\",
\"person-12\"
],
\"recipient_emails\": {
\"primary\": \"[email protected]\"
},
\"subject\": \"Sri Lakshmi Traders | Purchase order: PO-0007\",
\"message\": \"Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.\",
\"from\": \"2026-04-01\",
\"to\": \"2027-03-31\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/1/email"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"recipients": [
"primary",
"person-12"
],
"recipient_emails": {
"primary": "[email protected]"
},
"subject": "Sri Lakshmi Traders | Purchase order: PO-0007",
"message": "Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.",
"from": "2026-04-01",
"to": "2027-03-31"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/1/email';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'recipients' => ['primary', 'person-12'],
'recipient_emails' => ['primary' => '[email protected]'],
'subject' => 'Sri Lakshmi Traders | Purchase order: PO-0007',
'message' => 'Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.',
'from' => '2026-04-01',
'to' => '2027-03-31',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (202):
{
"message": "Emailed to Ravi Kumar.",
"data": {
"sent_to": [
{
"key": "primary",
"name": "Ravi Kumar",
"email": "[email protected]"
}
]
}
}
Example response (422):
{
"message": "Choose people listed on this party.",
"errors": {
"recipients.0": [
"Choose people listed on this party."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Attach a supplier invoice.
requires authentication
Upload one PDF, JPEG or PNG using multipart/form-data. Maximum 10 MB per file and 10 attachments per purchase invoice. Requires purchases permission. Files are private to this business. Voided invoices are read-only (422); other document types return 404. Attachments do not change stock, balances or the invoice's accounting entries.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/documents/1/attachments" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "file=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/1/attachments"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('file', document.querySelector('input[name="file"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/1/attachments';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'file',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 1,
"name": "supplier-invoice.pdf",
"mime_type": "application/pdf",
"size_bytes": 2048,
"created_at": "2026-09-28T05:00:00.000000Z",
"download_url": "https://dukanam.com/api/v1/businesses/1/documents/1/attachments/1"
}
}
Example response (422):
{
"message": "A purchase invoice can have up to 10 attachments.",
"errors": {
"file": [
"A purchase invoice can have up to 10 attachments."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Download an attached supplier invoice.
requires authentication
Requires authentication and purchases permission in the owning business, including when following download_url. Available on voided invoices for audit. Missing files, foreign-tenant invoices and attachments belonging to a different invoice return 404. No public storage URL is exposed.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/documents/1/attachments/1?preview=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"preview\": false
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/1/attachments/1"
);
const params = {
"preview": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"preview": false
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/1/attachments/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'preview' => '1',
],
'json' => [
'preview' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
Binary data - The original supplier invoice file, inline when preview=1 or downloaded otherwise.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Remove an attached supplier invoice.
requires authentication
Requires purchases permission. Removes the attachment without changing the invoice or its balances. Voided invoices are read-only (422). Tenant and invoice mismatches return 404.
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/documents/1/attachments/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/1/attachments/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/1/attachments/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204):
Empty response
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Payments
List payments.
requires authentication
Payments received from customers or made to suppliers, newest first. Advances — money taken
or paid before there was a bill — appear here too, with is_advance set and the part not
yet put towards a bill in unapplied_paise.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/payments?direction=received&from=2026-09-01&to=2026-09-30&contact_id=42&is_advance=1&unapplied=1&per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"direction\": \"received\",
\"from\": \"2026-01-15\",
\"to\": \"2026-01-15\",
\"contact_id\": 16,
\"is_advance\": false,
\"unapplied\": false,
\"per_page\": 22
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/payments"
);
const params = {
"direction": "received",
"from": "2026-09-01",
"to": "2026-09-30",
"contact_id": "42",
"is_advance": "1",
"unapplied": "1",
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"direction": "received",
"from": "2026-01-15",
"to": "2026-01-15",
"contact_id": 16,
"is_advance": false,
"unapplied": false,
"per_page": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/payments';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'direction' => 'received',
'from' => '2026-09-01',
'to' => '2026-09-30',
'contact_id' => '42',
'is_advance' => '1',
'unapplied' => '1',
'per_page' => '20',
],
'json' => [
'direction' => 'received',
'from' => '2026-01-15',
'to' => '2026-01-15',
'contact_id' => 16,
'is_advance' => false,
'unapplied' => false,
'per_page' => 22,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
is_advance
boolean
Whether the payment was taken or paid before any bill, rather than against one.
applied_paise
integer
How much of an advance has been put towards bills. Always 0 for a payment made against a bill.
unapplied_paise
integer
How much of an advance is still available to apply. Always 0 for a payment made against a bill.
Record a payment.
requires authentication
Settles a bill, or — with is_advance — records money taken from a customer or paid to a
supplier before any bill exists. An advance moves the account balance, the cash register,
the party's khata and the books the day it is recorded, so the party carries a credit
balance until a bill takes it up through POST /payments/{payment}/apply.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/payments" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"is_advance\": false,
\"contact_id\": 42,
\"invoice_id\": 16,
\"business_document_id\": 16,
\"amount\": 22,
\"paid_on\": \"2026-01-15\",
\"method\": \"cash\",
\"payment_account_id\": 16,
\"reference\": \"n\",
\"notes\": \"g\",
\"direction\": \"received\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/payments"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"is_advance": false,
"contact_id": 42,
"invoice_id": 16,
"business_document_id": 16,
"amount": 22,
"paid_on": "2026-01-15",
"method": "cash",
"payment_account_id": 16,
"reference": "n",
"notes": "g",
"direction": "received"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/payments';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'is_advance' => false,
'contact_id' => 42,
'invoice_id' => 16,
'business_document_id' => 16,
'amount' => 22,
'paid_on' => '2026-01-15',
'method' => 'cash',
'payment_account_id' => 16,
'reference' => 'n',
'notes' => 'g',
'direction' => 'received',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 9,
"direction": "received",
"is_advance": true,
"amount_paise": 500000,
"applied_paise": 0,
"unapplied_paise": 500000,
"paid_on": "2026-09-25",
"method": "upi",
"invoice_id": null,
"business_document_id": null
}
}
Example response (422):
{
"message": "Choose the customer who paid this advance.",
"errors": {
"contact_id": [
"Choose the customer who paid this advance."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
is_advance
boolean
Whether this is an advance.
unapplied_paise
integer
How much of an advance is still available to apply.
Get a payment and its email/share options.
requires authentication
Requires sales permission for received payments and purchases permission for made payments. Includes advances. Foreign business payments return 404.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/payments/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/payments/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/payments/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
send
object
Recipient keys, default message, email subject, email availability and a signed receipt PDF link valid for 90 days.
Edit an allocated payment.
requires authentication
Correct a payment allocated directly to a sales or purchase invoice, including a POS receipt. Requires sales permission for received payments or purchases for made payments. The party, direction, advance designation and invoice allocation cannot change. Advances and voided invoices return 422. Amount is capped at the current payment plus the bill's outstanding balance (an existing overpayment may be retained or reduced). Updates bill balances/status, khata, account journals and cash movements atomically; original financial entries are reversed and replacements saved, with payment.updated audit values. Reconciled payments cannot change amount, date, method or account. Payments in closed cash sessions cannot change amount, method or account; reference and notes can still be corrected. Moving to cash requires an open register. Foreign records return 404. Omitted fields retain their values; null reference/notes clear them.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/payments/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"is_advance\": false,
\"amount\": 1000,
\"paid_on\": \"2026-09-30\",
\"method\": \"upi\",
\"payment_account_id\": 3,
\"reference\": \"UTR-123\",
\"notes\": \"Corrected receipt\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/payments/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"is_advance": false,
"amount": 1000,
"paid_on": "2026-09-30",
"method": "upi",
"payment_account_id": 3,
"reference": "UTR-123",
"notes": "Corrected receipt"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/payments/1';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'is_advance' => false,
'amount' => 1000.0,
'paid_on' => '2026-09-30',
'method' => 'upi',
'payment_account_id' => 3,
'reference' => 'UTR-123',
'notes' => 'Corrected receipt',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 9,
"direction": "received",
"is_advance": false,
"amount_paise": 100000,
"paid_on": "2026-09-30",
"method": "upi",
"payment_account_id": 3,
"reference": "UTR-123",
"notes": "Corrected receipt",
"invoice_id": 16
}
}
Example response (422):
{
"message": "This payment is reconciled. Its amount, date, method and account cannot be changed.",
"errors": {
"payment": [
"This payment is reconciled. Its amount, date, method and account cannot be changed."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Email a payment receipt or advice.
requires authentication
Queues a separate branded Dukanam email for each selected contact through the platform's Cloudflare SMTP transport, with the shop's branding and receipt PDF attached. Requires sales permission for received payments or purchases for made payments. Includes advances. Foreign tenant payments return 404, invalid or foreign recipient keys and disabled platform email return 422. No payment or ledger values are changed.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/payments/1/email" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"recipients\": [
\"primary\",
\"person-12\"
],
\"recipient_emails\": {
\"primary\": \"[email protected]\"
},
\"subject\": \"Sri Lakshmi Traders | Purchase order: PO-0007\",
\"message\": \"Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.\",
\"from\": \"2026-04-01\",
\"to\": \"2027-03-31\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/payments/1/email"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"recipients": [
"primary",
"person-12"
],
"recipient_emails": {
"primary": "[email protected]"
},
"subject": "Sri Lakshmi Traders | Purchase order: PO-0007",
"message": "Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.",
"from": "2026-04-01",
"to": "2027-03-31"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/payments/1/email';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'recipients' => ['primary', 'person-12'],
'recipient_emails' => ['primary' => '[email protected]'],
'subject' => 'Sri Lakshmi Traders | Purchase order: PO-0007',
'message' => 'Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.',
'from' => '2026-04-01',
'to' => '2027-03-31',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (202):
{
"message": "Emailed to Ravi.",
"data": {
"sent_to": [
{
"key": "primary",
"name": "Ravi",
"email": "[email protected]"
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Apply an advance to a bill.
requires authentication
Puts part or all of an advance towards one unpaid bill of the same party: a sales invoice
for an advance received, a purchase invoice for an advance made. The bill's paid_paise
rises and the advance's unapplied_paise falls; the account, cash register, khata and
books do not move again, because they already did when the advance was recorded. Leave
amount out to apply as much as both the advance and the bill balance allow.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/payments/1/apply" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"invoice_id\": 16,
\"business_document_id\": 16,
\"amount\": 1500,
\"applied_on\": \"2026-09-25\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/payments/1/apply"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"invoice_id": 16,
"business_document_id": 16,
"amount": 1500,
"applied_on": "2026-09-25"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/payments/1/apply';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'invoice_id' => 16,
'business_document_id' => 16,
'amount' => 1500.0,
'applied_on' => '2026-09-25',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 9,
"direction": "received",
"is_advance": true,
"amount_paise": 500000,
"applied_paise": 150000,
"unapplied_paise": 350000,
"applications": [
{
"id": 1,
"payment_id": 9,
"amount_paise": 150000,
"applied_on": "2026-09-25",
"invoice_id": 16,
"invoice_number": "INV-016",
"business_document_id": null
}
]
}
}
Example response (422):
{
"message": "Enter an amount up to ₹3,500.00, the smaller of the unused advance and the bill balance.",
"errors": {
"amount": [
"Enter an amount up to ₹3,500.00, the smaller of the unused advance and the bill balance."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
applications
object[]
Every bill this advance has been put towards, with amount_paise, applied_on, and the invoice or document it settled.
Expenses
Expense records are a core Purchases capability on every plan. Workspace purchase permissions still apply.
List expenses.
requires authentication
Search and filter business expenses. The response includes totals for the current result, the current month, and the retained voided audit trail.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/expenses?q=internet&category=Utilities&payment_account_id=9&from=2026-08-01&to=2026-08-31&status=active&per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/expenses"
);
const params = {
"q": "internet",
"category": "Utilities",
"payment_account_id": "9",
"from": "2026-08-01",
"to": "2026-08-31",
"status": "active",
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/expenses';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'q' => 'internet',
'category' => 'Utilities',
'payment_account_id' => '9',
'from' => '2026-08-01',
'to' => '2026-08-31',
'status' => 'active',
'per_page' => '20',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 41,
"category": "Utilities",
"payee": "City Internet Services",
"amount_paise": 249900,
"occurred_on": "2026-08-12",
"payment_method": "bank",
"payment_account_id": 9,
"payment_account_name": "HDFC Current Account",
"note": "August internet bill for the main shop",
"voided_at": null,
"void_reason": null,
"created_at": "2026-08-12T10:30:00.000000Z"
}
],
"links": {
"first": "https://example.com/api/v1/businesses/1/expenses?page=1",
"last": "https://example.com/api/v1/businesses/1/expenses?page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 1,
"path": "https://example.com/api/v1/businesses/1/expenses",
"per_page": 20,
"to": 1,
"total": 1
},
"summary": {
"filtered_total_paise": 249900,
"month_total_paise": 384900,
"month_count": 3,
"voided_count": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
summary
object
filtered_total_paise
integer
Total of non-voided expenses matching the current filters.
month_total_paise
integer
Total of all non-voided expenses in the current calendar month.
month_count
integer
Count of all non-voided expenses in the current calendar month.
voided_count
integer
Count of all retained voided expenses.
Save an expense.
requires authentication
The expense, general-ledger journal, cash-drawer movement, and audit record either all commit or all roll back.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/expenses" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"category\": \"b\",
\"payee\": \"n\",
\"amount\": 7,
\"occurred_on\": \"2026-01-15\",
\"payment_method\": \"cash\",
\"payment_account_id\": 16,
\"note\": \"n\",
\"idempotency_key\": \"6d61f406-f07d-482d-a284-3e06edfd7f55\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/expenses"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"category": "b",
"payee": "n",
"amount": 7,
"occurred_on": "2026-01-15",
"payment_method": "cash",
"payment_account_id": 16,
"note": "n",
"idempotency_key": "6d61f406-f07d-482d-a284-3e06edfd7f55"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/expenses';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'category' => 'b',
'payee' => 'n',
'amount' => 7,
'occurred_on' => '2026-01-15',
'payment_method' => 'cash',
'payment_account_id' => 16,
'note' => 'n',
'idempotency_key' => '6d61f406-f07d-482d-a284-3e06edfd7f55',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
GET api/v1/businesses/{business}/expenses/{expense_id}
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/expenses/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/expenses/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/expenses/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Void an expense atomically.
requires authentication
The void marker, journal reversal, cash-drawer reversal, and audit record either all commit or all roll back.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/expenses/1/void" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"reason\": \"b\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/expenses/1/void"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"reason": "b"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/expenses/1/void';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'reason' => 'b',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
POS
POS and cash-register endpoints remain available while onboarding is on the first_work step so a shop can complete its first sale. Other business endpoints continue to return onboarding_incomplete until setup finishes.
A barcode is matched whole, and read as GS1 reads it, so a wedge scanner emitting the 12-digit UPC-A of a pack stored by its 13-digit EAN-13 still finds it. A name is matched on any part of it. For a camera scan use `items/by-barcode/{code}`, which answers with one item or a 404 rather than a list.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/pos/items?q=atta&contact=42" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"q\": \"b\",
\"contact\": 22
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/pos/items"
);
const params = {
"q": "atta",
"contact": "42",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"q": "b",
"contact": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/pos/items';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'q' => 'atta',
'contact' => '42',
],
'json' => [
'q' => 'b',
'contact' => 22,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
GET api/v1/businesses/{business}/pos/upi
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/pos/upi" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"amount\": 1,
\"payment_account_id\": 16
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/pos/upi"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"amount": 1,
"payment_account_id": 16
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/pos/upi';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'amount' => 1,
'payment_account_id' => 16,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List held POS carts for the business.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/pos/carts" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/pos/carts"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/pos/carts';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Hold a POS cart for later checkout.
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/pos/carts" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"b\",
\"contact_id\": 16,
\"lines\": [
{
\"item_id\": 16,
\"item_batch_id\": 16,
\"serial_numbers\": [
\"n\"
],
\"quantity\": 4326.41688
}
]
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/pos/carts"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "b",
"contact_id": 16,
"lines": [
{
"item_id": 16,
"item_batch_id": 16,
"serial_numbers": [
"n"
],
"quantity": 4326.41688
}
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/pos/carts';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'b',
'contact_id' => 16,
'lines' => [
[
'item_id' => 16,
'item_batch_id' => 16,
'serial_numbers' => ['n'],
'quantity' => 4326.41688,
],
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
POST api/v1/businesses/{business}/pos/checkout
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/pos/checkout" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"contact_id\": 16,
\"idempotency_key\": \"a4855dc5-0acb-33c3-b921-f4291f719ca0\",
\"warehouse_id\": 66,
\"cart_id\": 16,
\"lines\": [
{
\"item_id\": 16,
\"item_batch_id\": 16,
\"serial_numbers\": [
\"n\"
],
\"quantity\": 4326.41688,
\"discount\": 77
}
],
\"payments\": [
{
\"method\": \"cash\",
\"payment_account_id\": 16,
\"amount\": 4326.41688
}
]
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/pos/checkout"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"contact_id": 16,
"idempotency_key": "a4855dc5-0acb-33c3-b921-f4291f719ca0",
"warehouse_id": 66,
"cart_id": 16,
"lines": [
{
"item_id": 16,
"item_batch_id": 16,
"serial_numbers": [
"n"
],
"quantity": 4326.41688,
"discount": 77
}
],
"payments": [
{
"method": "cash",
"payment_account_id": 16,
"amount": 4326.41688
}
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/pos/checkout';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'contact_id' => 16,
'idempotency_key' => 'a4855dc5-0acb-33c3-b921-f4291f719ca0',
'warehouse_id' => 66,
'cart_id' => 16,
'lines' => [
[
'item_id' => 16,
'item_batch_id' => 16,
'serial_numbers' => ['n'],
'quantity' => 4326.41688,
'discount' => 77,
],
],
'payments' => [
['method' => 'cash', 'payment_account_id' => 16, 'amount' => 4326.41688],
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Cash register
Cash-register and POS endpoints remain available while onboarding is on the first_work step so a shop can complete its first POS sale. Other business endpoints continue to return onboarding_incomplete until setup finishes.
GET api/v1/businesses/{business}/cash-register
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/cash-register" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"cash_register_id\": 16
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/cash-register"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"cash_register_id": 16
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/cash-register';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'cash_register_id' => 16,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
POST api/v1/businesses/{business}/cash-register/open
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/cash-register/open" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"cash_register_id\": 16,
\"opening_float\": 22
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/cash-register/open"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"cash_register_id": 16,
"opening_float": 22
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/cash-register/open';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'cash_register_id' => 16,
'opening_float' => 22,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
POST api/v1/businesses/{business}/cash-register/movements
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/cash-register/movements" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"type\": \"cash_in\",
\"cash_register_id\": 16,
\"amount\": 22,
\"notes\": \"g\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/cash-register/movements"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"type": "cash_in",
"cash_register_id": 16,
"amount": 22,
"notes": "g"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/cash-register/movements';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'type' => 'cash_in',
'cash_register_id' => 16,
'amount' => 22,
'notes' => 'g',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
POST api/v1/businesses/{business}/cash-register/close
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/cash-register/close" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"counted_cash\": 1,
\"cash_register_id\": 16,
\"closing_notes\": \"n\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/cash-register/close"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"counted_cash": 1,
"cash_register_id": 16,
"closing_notes": "n"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/cash-register/close';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'counted_cash' => 1,
'cash_register_id' => 16,
'closing_notes' => 'n',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Save a counter's receipt printer settings.
requires authentication
Every device billing at the counter reads these from printer on GET cash-register and from cash_register.printer on payment accounts, and GET invoices/{invoice}/receipt applies them. Send only the settings that change. The printer itself is paired on each device and is not stored.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/cash-register/printer" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"cash_register_id\": 1,
\"paper\": \"80mm\",
\"auto_print\": true,
\"cut\": true,
\"open_drawer\": true
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/cash-register/printer"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"cash_register_id": 1,
"paper": "80mm",
"auto_print": true,
"cut": true,
"open_drawer": true
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/cash-register/printer';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'cash_register_id' => 1,
'paper' => '80mm',
'auto_print' => true,
'cut' => true,
'open_drawer' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"cash_register_id": 1,
"cash_register_name": "Main counter",
"paper": "58mm",
"auto_print": true,
"cut": false,
"open_drawer": true
}
}
Example response (422):
{
"message": "Choose 58mm or 80mm paper.",
"errors": {
"paper": [
"Choose 58mm or 80mm paper."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get a printer test slip.
requires authentication
ESC/POS bytes for a short slip showing the counter, paper width, cutter and drawer settings, with a numbered line that fills exactly one row when the paper width is right. Use it to check a newly paired printer before the first sale. The drawer opens only when open_drawer=1 is sent. The response has the same shape as GET invoices/{invoice}/receipt, with invoice_id and invoice_number null.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/cash-register/printer/test?cash_register_id=1&paper=58mm&cut=&open_drawer=" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/cash-register/printer/test"
);
const params = {
"cash_register_id": "1",
"paper": "58mm",
"cut": "0",
"open_drawer": "0",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/cash-register/printer/test';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'cash_register_id' => '1',
'paper' => '58mm',
'cut' => '0',
'open_drawer' => '0',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"format": "escpos",
"invoice_id": null,
"invoice_number": null,
"cash_register_id": 1,
"paper": "80mm",
"characters_per_line": 48,
"cut": true,
"opens_drawer": false,
"auto_print": false,
"content_base64": "G0AbYQEbRQEd...",
"byte_length": 402,
"text": "API Shop\nPrinter test"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Reports
View a business report.
requires authentication
Reports update automatically when bills, returns, expenses and other transactions are saved. No extra confirmation step is required. The P&L description is "Income and expenses for the selected period". Report descriptions use plain billing language. General ledger labels its entry-number column Reference.
Unregistered businesses have no Taxes category in catalog; tax-summary returns 403.
warehouse narrows stock summary, stock valuation, and low stock to one warehouse (low stock then compares that warehouse's quantity with each item's reorder level). The stock-by-warehouse report lists every item's stock in every warehouse.
Inventory values use recorded carrying costs independently of editable catalogue prices;
incomplete legacy movement history retains catalogue estimates. Warehouse allocations
use cumulative rounding so their values sum to the whole inventory value.
Cash flow includes actual expense settlements and worker payouts with dated payment
reversals; unpaid invoice expenses and earned-but-unpaid commissions are not outflows.
Financial reports include original journal entries and their dated reversals, so invoice edits and cancellations retain history while producing the correct net balance. Each entry is included according to its own accounting date. Invoice edits restate the invoice period: reversals use the previous invoice date and replacements use the submitted invoice date. Saving an edit does not move the invoice to today's date. Owners may explicitly change issue_date; cancellation reversals retain their cancellation date.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/reports" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"from\": \"2026-01-15\",
\"to\": \"2026-01-15\",
\"warehouse\": 22
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/reports"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"from": "2026-01-15",
"to": "2026-01-15",
"warehouse": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/reports';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'from' => '2026-01-15',
'to' => '2026-01-15',
'warehouse' => 22,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Download a report as PDF or Excel.
requires authentication
Supports every report in the report catalog, including profit-loss, balance-sheet, trial-balance, general-ledger, cash-flow and tax-summary. Uses the same rows and totals as the web and JSON report, excluding the separate summary cards. Money in Excel is numeric rupees, with signed amounts and two decimal places; dates are native Excel dates. Profit is green, loss is red, and zero is neutral in both formats. Both formats include the current shop/legal name, address, contact details and GSTIN where registered, plus the saved invoice logo (when enabled), accent and header style. Headers include the report dates and generation timestamp in the business timezone. Missing or unreadable logos are omitted without preventing download.
Requires reports and exports permissions, reports.basic and data_export features. Advanced reports also require reports.advanced. Unregistered businesses cannot export tax-summary. Tenant and warehouse isolation are enforced.
Dates are inclusive. Balance-sheet and trial-balance use to as their as-at date. Current-stock, payment-account and party-balance reports remain current snapshots, matching the JSON report, and are labelled accordingly. Aging reports use the requested document period with aging as at today. Downloads are synchronous and online-only.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/reports/export?report=profit-loss&format=pdf&from=2026-04-01&to=2026-10-03&warehouse=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/reports/export"
);
const params = {
"report": "profit-loss",
"format": "pdf",
"from": "2026-04-01",
"to": "2026-10-03",
"warehouse": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/reports/export';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'report' => 'profit-loss',
'format' => 'pdf',
'from' => '2026-04-01',
'to' => '2026-10-03',
'warehouse' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
Binary data - The report file, with attachment filename dukanam-{report}-{from}-{to}.{pdf|xlsx}.
Example response (401):
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "Your role does not allow this action."
}
Example response (404):
{
"message": "Not Found"
}
Example response (422):
{
"message": "The selected format is invalid.",
"errors": {
"format": [
"The selected format is invalid."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Stock that is about to go out of date.
requires authentication
Batches still holding stock whose expiry falls within within_days, soonest first.
Already-expired lots are included and flagged — the shop has to see and clear them — and
can never be billed. Requires a plan with advanced reports, like every other stock report.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/reports/expiring?within_days=60" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"within_days\": 1
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/reports/expiring"
);
const params = {
"within_days": "60",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"within_days": 1
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/reports/expiring';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'within_days' => '60',
],
'json' => [
'within_days' => 1,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
report
object
rows
string[]
Item, batch, expiry date, status, quantity, and value at cost.
within_days
integer
The horizon the response was built for.
Business compliance guidance
Get the compliance profile, registrations, evidence metadata and assessed obligations.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/compliance" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/compliance"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/compliance';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"business_id": 16,
"state_code": "29",
"profile": {
"constitution": "proprietorship",
"masked_pan": "ABC****34F",
"district": "Bengaluru Urban",
"local_body": "BBMP",
"premises_type": "rented",
"supply_type": "both",
"gst_registration_status": "not_registered",
"previous_year_turnover_paise": 180000000,
"other_pan_turnover_paise": 25000000,
"employee_count": 4,
"contract_worker_count": 0,
"female_employee_count": 2,
"interstate_sales": false,
"ecommerce_sales": true,
"imports": false,
"exports": false,
"manufactures": false,
"food_business": true,
"uses_weighing_scale": true,
"packs_or_imports_goods": false,
"pharmacy": false,
"serves_alcohol": false,
"fire_risk": false,
"pollution_activity": false,
"reminder_days": [
90,
60,
30,
7,
0
],
"assessment_completed_at": "2026-08-20T12:00:00+05:30",
"next_review_at": "2026-11-20T12:00:00+05:30"
},
"categories": [
{
"id": 13,
"slug": "food-service",
"name": "Restaurant, café and food service",
"nic_section": "I"
}
],
"gst_registrations": [],
"obligations": [
{
"id": 81,
"rule_code": "fssai-registration-licence",
"rule_version": 1,
"title": "FSSAI registration or food licence",
"applicability_reason": "Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.",
"applicability_level": "required",
"obligation_type": "licence",
"authority_name": "Food Safety and Standards Authority of India",
"source_url": "https://fssai.gov.in/business/registration",
"application_url": "https://foscos.fssai.gov.in/",
"status": "action_required",
"reference_number": null,
"issued_on": null,
"due_on": null,
"expires_on": null,
"notes": null,
"manual_override": false,
"last_assessed_at": "2026-08-20T12:00:00+05:30",
"documents": []
}
],
"guidance_notice": "Guidance is generated from configurable rules and recorded business facts. Review items marked professional review and verify official sources before relying on them.",
"options": {
"constitutions": {
"proprietorship": "Proprietorship",
"partnership": "Partnership firm",
"llp": "Limited Liability Partnership",
"private_limited": "Private limited company",
"public_limited": "Public limited company",
"opc": "One Person Company",
"huf": "Hindu Undivided Family",
"trust_society": "Trust / society",
"cooperative": "Co-operative society",
"government": "Government entity",
"other": "Other"
},
"supply_types": {
"goods": "Goods only",
"services": "Services only",
"both": "Goods and services"
},
"gst_statuses": {
"not_assessed": "Not assessed",
"not_registered": "Not registered",
"pending": "Application pending",
"active": "Registered / active",
"suspended": "Suspended",
"cancelled": "Cancelled / surrendered"
},
"gst_registration_types": {
"normal": "Normal taxpayer",
"composition": "Composition taxpayer",
"casual": "Casual taxable person",
"non_resident": "Non-resident taxable person",
"isd": "Input Service Distributor (ISD)",
"tds": "Tax Deductor (TDS)",
"tcs": "Tax Collector / e-commerce operator (TCS)",
"sez_unit": "SEZ unit",
"sez_developer": "SEZ developer",
"oidar": "OIDAR / online money gaming supplier",
"uin": "Unique Identity Number holder",
"temporary": "Temporary registration"
}
}
},
"meta": {
"available_categories": [
{
"id": 13,
"slug": "food-service",
"name": "Restaurant, café and food service",
"nic_section": "I",
"description": "Restaurants, cafés, caterers, cloud kitchens and mobile food service."
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update business facts and immediately run the deterministic compliance assessment.
requires authentication
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/businesses/1/compliance/profile" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"state_code\": \"29\",
\"category_ids\": [
13
],
\"constitution\": \"proprietorship\",
\"pan\": \"ABCDE1234F\",
\"district\": \"Bengaluru Urban\",
\"local_body\": \"BBMP\",
\"premises_type\": \"rented\",
\"supply_type\": \"both\",
\"gst_registration_status\": \"not_registered\",
\"previous_year_turnover\": 1800000,
\"other_pan_turnover\": 250000,
\"employee_count\": 4,
\"contract_worker_count\": 0,
\"female_employee_count\": 2,
\"interstate_sales\": false,
\"ecommerce_sales\": true,
\"imports\": false,
\"exports\": false,
\"manufactures\": false,
\"food_business\": true,
\"uses_weighing_scale\": true,
\"packs_or_imports_goods\": false,
\"pharmacy\": false,
\"serves_alcohol\": false,
\"fire_risk\": false,
\"pollution_activity\": false,
\"reminder_days\": [
90,
60,
30,
7,
0
]
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/compliance/profile"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"state_code": "29",
"category_ids": [
13
],
"constitution": "proprietorship",
"pan": "ABCDE1234F",
"district": "Bengaluru Urban",
"local_body": "BBMP",
"premises_type": "rented",
"supply_type": "both",
"gst_registration_status": "not_registered",
"previous_year_turnover": 1800000,
"other_pan_turnover": 250000,
"employee_count": 4,
"contract_worker_count": 0,
"female_employee_count": 2,
"interstate_sales": false,
"ecommerce_sales": true,
"imports": false,
"exports": false,
"manufactures": false,
"food_business": true,
"uses_weighing_scale": true,
"packs_or_imports_goods": false,
"pharmacy": false,
"serves_alcohol": false,
"fire_risk": false,
"pollution_activity": false,
"reminder_days": [
90,
60,
30,
7,
0
]
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/compliance/profile';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'state_code' => '29',
'category_ids' => [13],
'constitution' => 'proprietorship',
'pan' => 'ABCDE1234F',
'district' => 'Bengaluru Urban',
'local_body' => 'BBMP',
'premises_type' => 'rented',
'supply_type' => 'both',
'gst_registration_status' => 'not_registered',
'previous_year_turnover' => 1800000,
'other_pan_turnover' => 250000,
'employee_count' => 4,
'contract_worker_count' => 0,
'female_employee_count' => 2,
'interstate_sales' => false,
'ecommerce_sales' => true,
'imports' => false,
'exports' => false,
'manufactures' => false,
'food_business' => true,
'uses_weighing_scale' => true,
'packs_or_imports_goods' => false,
'pharmacy' => false,
'serves_alcohol' => false,
'fire_risk' => false,
'pollution_activity' => false,
'reminder_days' => [90, 60, 30, 7, 0],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"business_id": 16,
"state_code": "29",
"profile": {
"constitution": "proprietorship",
"masked_pan": "ABC****34F",
"district": "Bengaluru Urban",
"local_body": "BBMP",
"premises_type": "rented",
"supply_type": "both",
"gst_registration_status": "not_registered",
"previous_year_turnover_paise": 180000000,
"other_pan_turnover_paise": 25000000,
"employee_count": 4,
"contract_worker_count": 0,
"female_employee_count": 2,
"interstate_sales": false,
"ecommerce_sales": true,
"imports": false,
"exports": false,
"manufactures": false,
"food_business": true,
"uses_weighing_scale": true,
"packs_or_imports_goods": false,
"pharmacy": false,
"serves_alcohol": false,
"fire_risk": false,
"pollution_activity": false,
"reminder_days": [
90,
60,
30,
7,
0
],
"assessment_completed_at": "2026-08-20T12:00:00+05:30",
"next_review_at": "2026-11-20T12:00:00+05:30"
},
"categories": [
{
"id": 13,
"slug": "food-service",
"name": "Restaurant, café and food service",
"nic_section": "I"
}
],
"gst_registrations": [],
"obligations": [
{
"id": 81,
"rule_code": "fssai-registration-licence",
"rule_version": 1,
"title": "FSSAI registration or food licence",
"applicability_reason": "Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.",
"applicability_level": "required",
"obligation_type": "licence",
"authority_name": "Food Safety and Standards Authority of India",
"source_url": "https://fssai.gov.in/business/registration",
"application_url": "https://foscos.fssai.gov.in/",
"status": "action_required",
"reference_number": null,
"issued_on": null,
"due_on": null,
"expires_on": null,
"notes": null,
"manual_override": false,
"last_assessed_at": "2026-08-20T12:00:00+05:30",
"documents": []
}
],
"guidance_notice": "Guidance is generated from configurable rules and recorded business facts. Review items marked professional review and verify official sources before relying on them.",
"options": {
"constitutions": {
"proprietorship": "Proprietorship",
"partnership": "Partnership firm",
"llp": "Limited Liability Partnership",
"private_limited": "Private limited company",
"public_limited": "Public limited company",
"opc": "One Person Company",
"huf": "Hindu Undivided Family",
"trust_society": "Trust / society",
"cooperative": "Co-operative society",
"government": "Government entity",
"other": "Other"
},
"supply_types": {
"goods": "Goods only",
"services": "Services only",
"both": "Goods and services"
},
"gst_statuses": {
"not_assessed": "Not assessed",
"not_registered": "Not registered",
"pending": "Application pending",
"active": "Registered / active",
"suspended": "Suspended",
"cancelled": "Cancelled / surrendered"
},
"gst_registration_types": {
"normal": "Normal taxpayer",
"composition": "Composition taxpayer",
"casual": "Casual taxable person",
"non_resident": "Non-resident taxable person",
"isd": "Input Service Distributor (ISD)",
"tds": "Tax Deductor (TDS)",
"tcs": "Tax Collector / e-commerce operator (TCS)",
"sez_unit": "SEZ unit",
"sez_developer": "SEZ developer",
"oidar": "OIDAR / online money gaming supplier",
"uin": "Unique Identity Number holder",
"temporary": "Temporary registration"
}
}
},
"meta": {
"available_categories": [
{
"id": 13,
"slug": "food-service",
"name": "Restaurant, café and food service",
"nic_section": "I",
"description": "Restaurants, cafés, caterers, cloud kitchens and mobile food service."
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Re-run assessment against the latest published rule revisions.
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/compliance/assess" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/compliance/assess"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/compliance/assess';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"matched": 8,
"closed": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
POST api/v1/businesses/{business_id}/compliance/gst-registrations
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/compliance/gst-registrations" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"registration_type\": \"composition\",
\"registration_status\": \"active\",
\"gstin\": \"29ABCDE1234F1Z5\",
\"uin\": null,
\"state_code\": \"29\",
\"is_primary\": true,
\"valid_from\": \"2026-04-01\",
\"valid_until\": null
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/compliance/gst-registrations"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"registration_type": "composition",
"registration_status": "active",
"gstin": "29ABCDE1234F1Z5",
"uin": null,
"state_code": "29",
"is_primary": true,
"valid_from": "2026-04-01",
"valid_until": null
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/compliance/gst-registrations';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'registration_type' => 'composition',
'registration_status' => 'active',
'gstin' => '29ABCDE1234F1Z5',
'uin' => null,
'state_code' => '29',
'is_primary' => true,
'valid_from' => '2026-04-01',
'valid_until' => null,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 32,
"business_id": 16,
"registration_type": "composition",
"registration_status": "active",
"gstin": "29ABCDE1234F1Z5",
"uin": null,
"state_code": "29",
"is_primary": true,
"valid_from": "2026-04-01T00:00:00.000000Z",
"valid_until": null,
"created_at": "2026-08-20T06:30:00.000000Z",
"updated_at": "2026-08-20T06:30:00.000000Z"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
DELETE api/v1/businesses/{business_id}/compliance/gst-registrations/{gstRegistration_id}
requires authentication
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/compliance/gst-registrations/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/compliance/gst-registrations/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/compliance/gst-registrations/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204):
Empty response
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
GET api/v1/businesses/{business_id}/compliance/obligations/{complianceObligation_id}
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/compliance/obligations/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/compliance/obligations/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/compliance/obligations/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 81,
"rule_code": "fssai-registration-licence",
"rule_version": 1,
"title": "FSSAI registration or food licence",
"applicability_reason": "Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.",
"applicability_level": "required",
"obligation_type": "licence",
"authority_name": "Food Safety and Standards Authority of India",
"source_url": "https://fssai.gov.in/business/registration",
"application_url": "https://foscos.fssai.gov.in/",
"status": "obtained",
"reference_number": "FSSAI-10010022000123",
"issued_on": "2026-04-01",
"due_on": null,
"expires_on": "2027-03-31",
"notes": "Certificate verified.",
"manual_override": false,
"last_assessed_at": "2026-08-20T12:00:00+05:30",
"documents": [
{
"id": 44,
"original_name": "fssai-certificate.pdf",
"mime_type": "application/pdf",
"size": 102400,
"document_number": "FSSAI-10010022000123",
"issued_on": "2026-04-01",
"expires_on": "2027-03-31",
"is_current": true,
"download_url": "https://billing.example.com/api/v1/businesses/16/compliance/documents/44/download"
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
PATCH api/v1/businesses/{business_id}/compliance/obligations/{complianceObligation_id}
requires authentication
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/compliance/obligations/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"status\": \"obtained\",
\"reference_number\": \"LIC-2026-1001\",
\"issued_on\": \"2026-04-01\",
\"due_on\": null,
\"expires_on\": \"2027-03-31\",
\"notes\": \"Renewal filed by the accountant.\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/compliance/obligations/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"status": "obtained",
"reference_number": "LIC-2026-1001",
"issued_on": "2026-04-01",
"due_on": null,
"expires_on": "2027-03-31",
"notes": "Renewal filed by the accountant."
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/compliance/obligations/1';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'status' => 'obtained',
'reference_number' => 'LIC-2026-1001',
'issued_on' => '2026-04-01',
'due_on' => null,
'expires_on' => '2027-03-31',
'notes' => 'Renewal filed by the accountant.',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 81,
"rule_code": "fssai-registration-licence",
"rule_version": 1,
"title": "FSSAI registration or food licence",
"applicability_reason": "Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.",
"applicability_level": "required",
"obligation_type": "licence",
"authority_name": "Food Safety and Standards Authority of India",
"source_url": "https://fssai.gov.in/business/registration",
"application_url": "https://foscos.fssai.gov.in/",
"status": "obtained",
"reference_number": "FSSAI-10010022000123",
"issued_on": "2026-04-01",
"due_on": null,
"expires_on": "2027-03-31",
"notes": "Certificate verified.",
"manual_override": false,
"last_assessed_at": "2026-08-20T12:00:00+05:30",
"documents": [
{
"id": 44,
"original_name": "fssai-certificate.pdf",
"mime_type": "application/pdf",
"size": 102400,
"document_number": "FSSAI-10010022000123",
"issued_on": "2026-04-01",
"expires_on": "2027-03-31",
"is_current": true,
"download_url": "https://billing.example.com/api/v1/businesses/16/compliance/documents/44/download"
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
POST api/v1/businesses/{business_id}/compliance/obligations/{complianceObligation_id}/documents
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/compliance/obligations/1/documents" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "document_number=FSSAI-10010022000123"\
--form "issued_on=2026-04-01"\
--form "expires_on=2027-03-31"\
--form "document=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/businesses/1/compliance/obligations/1/documents"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('document_number', 'FSSAI-10010022000123');
body.append('issued_on', '2026-04-01');
body.append('expires_on', '2027-03-31');
body.append('document', document.querySelector('input[name="document"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/compliance/obligations/1/documents';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'document_number',
'contents' => 'FSSAI-10010022000123'
],
[
'name' => 'issued_on',
'contents' => '2026-04-01'
],
[
'name' => 'expires_on',
'contents' => '2027-03-31'
],
[
'name' => 'document',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 81,
"rule_code": "fssai-registration-licence",
"rule_version": 1,
"title": "FSSAI registration or food licence",
"applicability_reason": "Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.",
"applicability_level": "required",
"obligation_type": "licence",
"authority_name": "Food Safety and Standards Authority of India",
"source_url": "https://fssai.gov.in/business/registration",
"application_url": "https://foscos.fssai.gov.in/",
"status": "obtained",
"reference_number": "FSSAI-10010022000123",
"issued_on": "2026-04-01",
"due_on": null,
"expires_on": "2027-03-31",
"notes": "Certificate verified.",
"manual_override": false,
"last_assessed_at": "2026-08-20T12:00:00+05:30",
"documents": [
{
"id": 44,
"original_name": "fssai-certificate.pdf",
"mime_type": "application/pdf",
"size": 102400,
"document_number": "FSSAI-10010022000123",
"issued_on": "2026-04-01",
"expires_on": "2027-03-31",
"is_current": true,
"download_url": "https://billing.example.com/api/v1/businesses/16/compliance/documents/44/download"
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
GET api/v1/businesses/{business_id}/compliance/documents/{complianceDocument_id}/download
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/compliance/documents/1/download" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/compliance/documents/1/download"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/compliance/documents/1/download';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
GST compliance
View GST workspace.
requires authentication
Unregistered businesses cannot access this endpoint, regardless of plan.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/gst" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "GST features are unavailable for an unregistered business."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Export GSTR-1.
requires authentication
Unregistered businesses cannot access this endpoint, regardless of plan.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/gst/gstr1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst/gstr1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst/gstr1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "GST features are unavailable for an unregistered business."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Import GSTR-2B.
requires authentication
Unregistered businesses cannot access this endpoint, regardless of plan.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/gst/gstr2b" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst/gstr2b"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst/gstr2b';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (403):
{
"message": "GST features are unavailable for an unregistered business."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Unregistered businesses cannot access this endpoint, regardless of plan.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/gst/gstr3b?period=2026-08&revision=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst/gstr3b"
);
const params = {
"period": "2026-08",
"revision": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst/gstr3b';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'period' => '2026-08',
'revision' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Example response (403):
{"message":"GST features are unavailable for an unregistered business."}
Prepare GSTR-3B for a month.
Sections 3.1, 3.2, 4, 5 and 6.1 computed from saved documents — never from current
masters, so re-reading an old period reproduces it exactly. A locked revision answers with
the figures that were locked rather than recomputing; a draft recomputes on every read.
`itc_basis` says where section 4's other ITC came from: `gstr2b_reconciled` when a GSTR-2B
has been imported for the period, `books` when it has not. `not_derived` lists the rows of
the form Dukanam cannot compute, which the filer enters on the portal.
This is preparation data. Dukanam does not file returns.
**Offline:** online-only. It reads a month of recorded transactions, so it belongs in neither the
offline cache nor the write queue.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
status
string
draft or locked.
revision
integer
Which revision of the period this is.
itc_basis
string
gstr2b_reconciled or books.
file_hash
string
SHA-256 of the locked sections; null while draft.
sections
object
The form, keyed by section number.
not_derived
string[]
Rows the filer must supply on the portal.
Unregistered businesses cannot access this endpoint, regardless of plan.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/gst/gstr3b/2026-08/export?format=xlsx&revision=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"format\": \"json\",
\"revision\": 16
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst/gstr3b/2026-08/export"
);
const params = {
"format": "xlsx",
"revision": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"format": "json",
"revision": 16
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst/gstr3b/2026-08/export';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'format' => 'xlsx',
'revision' => '1',
],
'json' => [
'format' => 'json',
'revision' => 16,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, JSON export):
{
"period": "2026-08",
"status": "locked",
"sections": {}
}
Example response (403):
{"message":"GST features are unavailable for an unregistered business."}
Download a prepared GSTR-3B.
`format=json` returns the same payload as the summary endpoint as a file; `format=xlsx`
returns a workbook with one sheet per section, money as a number in rupees and a frozen
header row, for handing to the shop's accountant.
**Offline:** online-only. The file is built on the server from recorded transactions.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Unregistered businesses cannot access this endpoint, regardless of plan.
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/gst/gstr3b/2026-08/lock" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst/gstr3b/2026-08/lock"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst/gstr3b/2026-08/lock';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (403):
{"message":"GST features are unavailable for an unregistered business."}
Lock a GSTR-3B period after filing.
Computes the figures once more, stores them and hashes them. The period then answers from
that snapshot however the books move afterwards. Locking twice answers `422`; so does
locking a period that has not ended, because it cannot have been filed yet.
**Offline:** online-only. Nothing here may be replayed from the write queue — a lock
records a filing that has already happened, and a stale replay would freeze stale figures.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Unregistered businesses cannot access this endpoint, regardless of plan.
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/gst/gstr3b/2026-08/amend" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"reason\": \"Missed a purchase invoice\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst/gstr3b/2026-08/amend"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"reason": "Missed a purchase invoice"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst/gstr3b/2026-08/amend';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'reason' => 'Missed a purchase invoice',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (403):
{"message":"GST features are unavailable for an unregistered business."}
Open the next revision of a locked period.
The locked revision stays readable exactly as it was filed and the new revision starts as
a draft that recomputes from the corrected books. Amending a period that is not locked
answers `422` — an unlocked period is simply edited by correcting the documents.
**Offline:** online-only, for the same reason a lock is.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Unregistered businesses cannot access this endpoint, regardless of plan.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/gst/gstr9?financial_year=2026-27&revision=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"financial_year\": \"bngzmiy\",
\"revision\": 16
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst/gstr9"
);
const params = {
"financial_year": "2026-27",
"revision": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"financial_year": "bngzmiy",
"revision": 16
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst/gstr9';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'financial_year' => '2026-27',
'revision' => '1',
],
'json' => [
'financial_year' => 'bngzmiy',
'revision' => 16,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Example response (403):
{"message":"GST features are unavailable for an unregistered business."}
Prepare the GSTR-9 annual working for a financial year.
This is the **working data**, not the return: the year's outward supplies by nature
(tables 4 and 5), ITC availed and reversed (6 and 7), tax paid as it was declared (9), the
HSN summaries (17 and 18), an outward and inward summary by rate, and the month-by-month
reconciliation of the sales register against GSTR-3B against the journals. The filer reads
these onto the portal; Dukanam submits nothing.
Tables 4, 5, 6, 7, 17 and 18 are computed from the year's saved documents. Table 9 and the
reconciliation are rolled up from the twelve monthly GSTR-3B returns, using a month's
**locked** revision where it has one — that table reports what was declared, not what the
books say today. `sections.9.filed_months` says how many of the twelve were locked, and
every reconciliation row carries `gstr3b_source` of `filed` or `computed`.
`applicability` measures this workspace's turnover against the configured threshold. It is
never a gate: aggregate turnover is a PAN-wide figure Dukanam cannot see, and the threshold
itself moves. A locked revision answers with the figures that were locked; a draft
recomputes on every read.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
status
string
draft or locked.
revision
integer
Which revision of the year this is.
itc_basis
string
gstr2b_reconciled, partly_gstr2b_reconciled or books.
file_hash
string
SHA-256 of the locked sections; null while draft.
applicability
object
Measured turnover against the configured thresholds.
sections
object
The working, keyed by table number plus rate_summary and reconciliation.
not_derived
string[]
Tables of the form Dukanam cannot compute, which the filer supplies on the portal.
Unregistered businesses cannot access this endpoint, regardless of plan.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/gst/gstr9/2026-27/export?format=xlsx&revision=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"format\": \"json\",
\"revision\": 16
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst/gstr9/2026-27/export"
);
const params = {
"format": "xlsx",
"revision": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"format": "json",
"revision": 16
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst/gstr9/2026-27/export';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'format' => 'xlsx',
'revision' => '1',
],
'json' => [
'format' => 'json',
'revision' => 16,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, JSON export):
{
"financial_year": "2026-27",
"status": "locked",
"sections": {}
}
Example response (403):
{"message":"GST features are unavailable for an unregistered business."}
Download a prepared GSTR-9 working.
`format=json` returns the same payload as the summary endpoint as a file; `format=xlsx`
returns the workbook to hand to the shop's accountant — one sheet per table plus the
reconciliation, money as a number in rupees and a frozen header row.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Unregistered businesses cannot access this endpoint, regardless of plan.
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/gst/gstr9/2026-27/lock" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst/gstr9/2026-27/lock"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst/gstr9/2026-27/lock';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (403):
{"message":"GST features are unavailable for an unregistered business."}
Lock a GSTR-9 year after filing.
Computes the working once more, stores it and hashes it. The year then answers from that
snapshot however the books move afterwards. Locking twice answers `422`; so does locking a
year that has not ended, because its annual return cannot have been filed yet.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Unregistered businesses cannot access this endpoint, regardless of plan.
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/gst/gstr9/2026-27/amend" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"reason\": \"Credit note saved after filing\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/gst/gstr9/2026-27/amend"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"reason": "Credit note saved after filing"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/gst/gstr9/2026-27/amend';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'reason' => 'Credit note saved after filing',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (403):
{"message":"GST features are unavailable for an unregistered business."}
Open the next revision of a locked year.
The locked revision stays readable exactly as it was filed and the new revision starts as a
draft that recomputes from the corrected books. Amending a year that is not locked answers
`422` — an unlocked year is simply corrected by correcting the documents.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Billing
List plans and activation availability
requires authentication
Use razorpay_checkout_enabled for paid checkout. paid_plan_activation_enabled refers only to the
support-only manual activation fallback, while each plan's activation_available covers either path.
Ended paid subscriptions automatically fall back to Free Essentials, preserving paid history and trial ineligibility.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/billing" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/billing"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/billing';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Free fallback (excerpt)):
{
"data": {
"business": {
"id": 1,
"free_transition": {
"subscription_id": 3,
"reason": "trial_ended",
"requires_acknowledgement": true
},
"invoice_allowance": {
"limit": 50,
"used": 70,
"remaining": 0,
"can_create": false,
"resets_at": "2026-09-30T18:30:00.000000Z"
}
},
"paid_plan_activation_enabled": false,
"razorpay_checkout_enabled": true,
"plans": []
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
business
object
free_transition
object|null
Owner-only automatic fallback notice; null when no automatic fallback applies.
subscription_id
integer
Current Free subscription ID to acknowledge.
reason
string
Either trial_ended or subscription_ended.
requires_acknowledgement
boolean
Show a plan choice when true; never block access to existing invoices.
invoice_allowance
object
Calendar-month usage, including trial invoices. Downgrading never resets it.
limit
integer|null
Monthly limit; null means unlimited.
used
integer
Invoices created this application-calendar month, including voided invoices.
remaining
integer|null
Remaining allowance, clamped to zero; null means unlimited.
can_create
boolean
Whether another invoice fits in the allowance.
resets_at
string
ISO-8601 timestamp of the next monthly reset.
paid_plan_activation_enabled
boolean
Whether customers may self-activate paid plans.
razorpay_checkout_enabled
boolean
Whether Razorpay subscription checkout is configured.
plans
object
activation_available
boolean
Whether this plan may be selected through the API.
activation_mode
string
Use prorated_change with the change endpoint for an immediate paid upgrade; otherwise use checkout, direct_change, manual_change, or current.
trial_eligible
boolean
Whether this workspace may receive this plan's introductory trial.
checkout_trial_days
integer
Trial days that a new checkout would actually receive after applying workspace history.
monthly_mrp_paise
integer
Regular monthly price before an active offer.
monthly_price_paise
integer
Effective monthly price after any active offer.
yearly_mrp_paise
integer
Regular yearly price before an active offer.
yearly_price_paise
integer
Effective yearly price after any active offer.
offer
object|null
Active offer label, validity, and discount details.
features
string[]
Effective feature list, including the core expenses capability on every plan.
limits
object
items
integer
Maximum catalogue item count; 0 means unlimited.
List subscription payments
requires authentication
Returns successful subscription charges and mandate authorisations for the business owner. Only captured recurring charges expose an invoice download URL.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/billing/payments?per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/billing/payments"
);
const params = {
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/billing/payments';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '20',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Download a subscription invoice
requires authentication
Downloads the immutable PDF invoice for a captured recurring subscription charge.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/billing/payments/1/invoice" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/billing/payments/1/invoice"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/billing/payments/1/invoice';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Continue on Free after a subscription ends
requires authentication
Owner only. Acknowledges the current automatic Free fallback without changing plans, resetting invoice usage, or granting another trial. Repeating the same request is safe. A stale or unrelated subscription ID returns HTTP 422; another tenant returns HTTP 404. Existing invoices remain accessible before and after acknowledgement.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/billing/continue-free" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"subscription_id\": 3
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/billing/continue-free"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"subscription_id": 3
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/billing/continue-free';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'subscription_id' => 3,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Acknowledged Free fallback (excerpt)):
{
"data": {
"id": 1,
"free_transition": {
"subscription_id": 3,
"reason": "trial_ended",
"requires_acknowledgement": false
},
"invoice_allowance": {
"limit": 50,
"used": 70,
"remaining": 0,
"can_create": false,
"resets_at": "2026-09-30T18:30:00.000000Z"
}
}
}
Example response (422):
{
"message": "Your plan has changed. Refresh billing before continuing.",
"errors": {
"subscription_id": [
"Your plan has changed. Refresh billing before continuing."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
free_transition
object
requires_acknowledgement
boolean
False after acknowledgement.
Change the current plan
requires authentication
Paid plans return HTTP 422 while online paid-plan activation is disabled, except for an upgrade from an
active Razorpay paid plan. Smart Books to Business is updated in place immediately; Razorpay credits the
unused current-plan value and charges the prorated difference without creating another trial.
Changing away from a Razorpay-backed plan cancels that provider subscription immediately before the new
plan is activated, so mobile clients must confirm an immediate loss of paid access before submitting. If
Razorpay cannot confirm cancellation, the endpoint returns HTTP 422 and leaves the current plan unchanged.
Send an Idempotency-Key when applying a prorated upgrade. Retrying with the same key returns the completed
operation and never sends a second provider update; concurrent retries return HTTP 409 until it completes.
A conflicting target or interval also returns HTTP 409 while another transition owns the subscription.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/billing/change" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Idempotency-Key: upgrade-2026-08-25-01" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"plan_id\": 3,
\"billing_interval\": \"monthly\",
\"idempotency_key\": \"upgrade-2026-08-25-01\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/billing/change"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Idempotency-Key": "upgrade-2026-08-25-01",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"plan_id": 3,
"billing_interval": "monthly",
"idempotency_key": "upgrade-2026-08-25-01"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/billing/change';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Idempotency-Key' => 'upgrade-2026-08-25-01',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'plan_id' => 3,
'billing_interval' => 'monthly',
'idempotency_key' => 'upgrade-2026-08-25-01',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (409):
{
"message": "This plan change is already processing. Retry with the same idempotency key."
}
Example response (422):
{
"message": "Online paid-plan activation is not configured. Contact support to change this plan.",
"errors": {
"plan_id": [
"Online paid-plan activation is not configured. Contact support to change this plan."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Start Razorpay subscription checkout
requires authentication
Creates a price-versioned Razorpay subscription. Mobile clients must pass the returned subscription_id
to Razorpay Standard Checkout and then send the signed result to the confirm endpoint. The effective plan
price is calculated on the server and must be at least 100 paise; clients cannot submit an arbitrary amount.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/billing/checkout" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"plan_id\": 3,
\"billing_interval\": \"monthly\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/billing/checkout"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"plan_id": 3,
"billing_interval": "monthly"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/billing/checkout';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'plan_id' => 3,
'billing_interval' => 'monthly',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"checkout_id": 1,
"key": "rzp_test_example",
"subscription_id": "sub_example",
"name": "Dukanam",
"description": "Smart Books · Monthly",
"amount_paise": 99900,
"currency": "INR",
"trial_days": 14,
"prefill": {
"name": "Shop Owner",
"email": "[email protected]",
"contact": "9876543210"
}
}
}
Example response (401):
{
"message": "Razorpay rejected the configured API credentials."
}
Example response (422):
{
"message": "Razorpay checkout requires a charge of at least 100 paise.",
"errors": {
"plan_id": [
"Razorpay checkout requires a charge of at least 100 paise."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Confirm Razorpay subscription authorisation
requires authentication
The server verifies the Razorpay HMAC signature and fetches provider state before granting access. Trial access starts only after successful payment-method authorisation; a plan without a trial waits for Razorpay to report the subscription as active. When the checkout replaces another Razorpay plan, the previous provider subscription is cancelled immediately before the replacement is activated.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/billing/checkouts/1/confirm" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"razorpay_payment_id\": \"b\",
\"razorpay_subscription_id\": \"n\",
\"razorpay_signature\": \"gzmiyvdljnikhwaykcmyuwpwlvqwrsitcpscqldzsnrwtujwvlxjklqppwqbewtn\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/billing/checkouts/1/confirm"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"razorpay_payment_id": "b",
"razorpay_subscription_id": "n",
"razorpay_signature": "gzmiyvdljnikhwaykcmyuwpwlvqwrsitcpscqldzsnrwtujwvlxjklqppwqbewtn"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/billing/checkouts/1/confirm';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'razorpay_payment_id' => 'b',
'razorpay_subscription_id' => 'n',
'razorpay_signature' => 'gzmiyvdljnikhwaykcmyuwpwlvqwrsitcpscqldzsnrwtujwvlxjklqppwqbewtn',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Cancel the paid plan at the end of the current period.
requires authentication
Owner only. Access continues until the provider's current cycle ends. Free referral
time (subscription.provider is referral) ends on its own date and cannot be cancelled.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/billing/cancel" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/billing/cancel"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/billing/cancel';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "Free referral time ends on its own date and does not need to be cancelled."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Buy or give up POS logins.
requires authentication
A POS login costs the seat price per month and is added to the running subscription: Razorpay charges the prorated difference for the rest of the cycle immediately and every renewal after it bills the new total. Owner only. The workspace must be on an active paid plan billed online, and seats cannot drop below the logins already in use.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/billing/pos-seats" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"pos_seats\": 2
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/billing/pos-seats"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"pos_seats": 2
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/billing/pos-seats';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'pos_seats' => 2,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "POS logins are billed on an active paid subscription. Choose a plan first, then add counter logins.",
"errors": {
"pos_seats": [
"POS logins are billed on an active paid subscription. Choose a plan first, then add counter logins."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Referrals
Refer other shops and earn free subscription months. Referrals belong to the signed-in account, not to one workspace, so these routes are not business-scoped. The server owns eligibility, reward calculation and the free-period schedule; clients display what it returns.
Check a referral code.
For a registration screen opened from a shared link: tells the app whether the code
will be accepted and whose it is (first name only). The code may be a customer's
referral code or an influencer partner's link code; referrer_type says which.
Send the same code as referral_code on registration.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/referrals/codes/K7M2QX9A" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/referrals/codes/K7M2QX9A"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/referrals/codes/K7M2QX9A';
$response = $client->get(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"code": "K7M2QX9A",
"valid": true,
"program_enabled": true,
"referrer_type": "customer",
"referrer_first_name": "Priya"
}
}
Example response (200, Partner link code):
{
"data": {
"code": "RAVI7K",
"valid": true,
"program_enabled": true,
"referrer_type": "partner",
"referrer_first_name": "Ravi"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get the referral dashboard.
requires authentication
Returns the account's referral code and link (created on first call), a ready-to-send
WhatsApp message and wa.me URL, the counters for the Referral & Rewards screen, the
running reward and the projected dates of every waiting one. Waiting rewards start
only after all paid time ends; when paid_coverage.auto_renews is true those dates
move forward each time the paid plan renews. POS counter logins cannot refer (403).
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/referrals" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/referrals"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/referrals';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"program": {
"enabled": true,
"reward_months": 1,
"reward_plan": {
"id": 2,
"name": "Smart Books",
"slug": "smart"
},
"reward_plan_source": "program_default"
},
"referral_code": "K7M2QX9A",
"referral_link": "https://dukanam.com/register?ref=K7M2QX9A",
"share": {
"code": "K7M2QX9A",
"link": "https://dukanam.com/register?ref=K7M2QX9A",
"message": "Hey! Check out Dukanam for billing, stock and khata. You can register using my referral link: https://dukanam.com/register?ref=K7M2QX9A",
"whatsapp_url": "https://wa.me/?text=Hey%21%20Check%20out%20Dukanam"
},
"stats": {
"total_referrals": 3,
"pending_referrals": 1,
"successful_referrals": 2,
"rewards_earned": 2,
"months_earned": 2,
"available_rewards": 1,
"available_months": 1,
"available_extra_days": 0,
"active_rewards": 1,
"used_rewards": 0
},
"paid_coverage": {
"ends_at": null,
"auto_renews": false
},
"current_reward": {
"id": 7,
"status": "active",
"months": 1,
"remaining_days": null,
"plan": {
"id": 2,
"name": "Smart Books",
"slug": "smart"
},
"business": {
"id": 17,
"name": "Aarogya Medical Store"
},
"referred_name": "Ravi Kumar",
"earned_at": "2026-09-01T10:00:00.000000Z",
"activated_at": "2026-09-01T10:00:00.000000Z",
"starts_at": "2026-09-01T10:00:00.000000Z",
"ends_at": "2026-10-01T10:00:00.000000Z",
"consumed_at": null
},
"upcoming_rewards": [
{
"id": 8,
"status": "available",
"months": 1,
"remaining_days": null,
"plan": {
"id": 2,
"name": "Smart Books",
"slug": "smart"
},
"business": null,
"referred_name": "Meena S",
"earned_at": "2026-09-15T10:00:00.000000Z",
"activated_at": null,
"starts_at": null,
"ends_at": null,
"consumed_at": null,
"projected_starts_at": "2026-10-01T10:00:00.000000Z",
"projected_ends_at": "2026-11-01T10:00:00.000000Z",
"starts_after_paid_plan": false
}
]
}
}
Example response (403):
{
"message": "This action is unauthorized."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
program
object
reward_plan
object|null
The plan this account's next reward grants: the plan it last paid for, otherwise the program plan.
reward_plan_source
string|null
paid_plan when the reward matches the plan this account last paid for, program_default when it uses the admin-configured plan.
referral_code
string
The account's unique referral code.
referral_link
string
The registration link carrying the code as ref.
share
object
message
string
The admin-configured share message with the link filled in.
whatsapp_url
string
Opens WhatsApp (app on phones, WhatsApp Web or Desktop on computers) with the message pre-filled.
stats
object
pending_referrals
integer
Registered or subscription pending.
successful_referrals
integer
Bought a paid plan.
available_months
integer
Whole months waiting to start.
available_extra_days
integer
Days carried over from rewards a paid plan interrupted.
upcoming_rewards
object
projected_starts_at
string
When the reward is expected to start.
starts_after_paid_plan
boolean
True while a paid plan is still running ahead of it.
List referral history.
requires authentication
The people who registered with the account's code, newest first, with each one's
status (registered, subscription_pending, subscription_purchased,
reward_earned, reward_applied) and the reward it earned.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/referrals/history?status=reward_earned&per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"status\": \"architecto\",
\"per_page\": 22
}"
const url = new URL(
"https://dukanam.com/api/v1/referrals/history"
);
const params = {
"status": "reward_earned",
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"status": "architecto",
"per_page": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/referrals/history';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'status' => 'reward_earned',
'per_page' => '20',
],
'json' => [
'status' => 'architecto',
'per_page' => 22,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 12,
"status": "reward_earned",
"status_label": "Reward earned",
"steps": [
{
"status": "registered",
"label": "Registered",
"reached": true
},
{
"status": "subscription_pending",
"label": "Subscription pending",
"reached": true
},
{
"status": "subscription_purchased",
"label": "Subscription purchased",
"reached": true
},
{
"status": "reward_earned",
"label": "Reward earned",
"reached": true
},
{
"status": "reward_applied",
"label": "Reward applied",
"reached": false
}
],
"referred": {
"name": "Meena S"
},
"source": "api",
"registered_at": "2026-09-10T08:00:00.000000Z",
"subscription_pending_at": "2026-09-12T08:00:00.000000Z",
"subscription_purchased_at": "2026-09-15T10:00:00.000000Z",
"reward_earned_at": "2026-09-15T10:00:00.000000Z",
"reward_applied_at": null,
"reward_skipped_reason": null,
"reward": {
"id": 8,
"status": "available",
"months": 1,
"remaining_days": null,
"plan": {
"id": 2,
"name": "Smart Books",
"slug": "smart"
},
"business": null,
"referred_name": null,
"earned_at": "2026-09-15T10:00:00.000000Z",
"activated_at": null,
"starts_at": null,
"ends_at": null,
"consumed_at": null
}
}
],
"links": {},
"meta": {
"current_page": 1,
"per_page": 20,
"total": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List earned rewards.
requires authentication
Every reward the account has earned. available rewards are earned and waiting,
active is running now as free subscription time, and consumed ones are used up.
A reward a paid plan interrupted is available again with remaining_days set.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/referrals/rewards?status=available" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"status\": \"architecto\"
}"
const url = new URL(
"https://dukanam.com/api/v1/referrals/rewards"
);
const params = {
"status": "available",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"status": "architecto"
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/referrals/rewards';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'status' => 'available',
],
'json' => [
'status' => 'architecto',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 8,
"status": "available",
"months": 1,
"remaining_days": null,
"plan": {
"id": 2,
"name": "Smart Books",
"slug": "smart"
},
"business": null,
"referred_name": "Meena S",
"earned_at": "2026-09-15T10:00:00.000000Z",
"activated_at": null,
"starts_at": null,
"ends_at": null,
"consumed_at": null
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Apply waiting rewards.
requires authentication
Optionally chooses which owned workspace waiting rewards run on, then starts the next one immediately when no paid plan is running. With a paid plan running, nothing starts: rewards never interrupt paid time and begin on their own once it ends. Idempotent. Returns the refreshed referral dashboard.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/referrals/rewards/apply" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"business_id\": 17
}"
const url = new URL(
"https://dukanam.com/api/v1/referrals/rewards/apply"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"business_id": 17
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/referrals/rewards/apply';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'business_id' => 17,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"program": {
"enabled": true,
"reward_months": 1,
"reward_plan": {
"id": 2,
"name": "Smart Books",
"slug": "smart"
},
"reward_plan_source": "program_default"
},
"referral_code": "K7M2QX9A",
"referral_link": "https://dukanam.com/register?ref=K7M2QX9A",
"share": {
"code": "K7M2QX9A",
"link": "https://dukanam.com/register?ref=K7M2QX9A",
"message": "Hey! You can register using my referral link: https://dukanam.com/register?ref=K7M2QX9A",
"whatsapp_url": "https://wa.me/?text=Hey%21"
},
"stats": {
"total_referrals": 1,
"pending_referrals": 0,
"successful_referrals": 1,
"rewards_earned": 1,
"months_earned": 1,
"available_rewards": 0,
"available_months": 0,
"available_extra_days": 0,
"active_rewards": 1,
"used_rewards": 0
},
"paid_coverage": {
"ends_at": null,
"auto_renews": false
},
"current_reward": {
"id": 8,
"status": "active",
"months": 1,
"remaining_days": null,
"plan": {
"id": 2,
"name": "Smart Books",
"slug": "smart"
},
"business": {
"id": 17,
"name": "Aarogya Medical Store"
},
"referred_name": "Meena S",
"earned_at": "2026-09-15T10:00:00.000000Z",
"activated_at": "2026-09-24T10:00:00.000000Z",
"starts_at": "2026-09-24T10:00:00.000000Z",
"ends_at": "2026-10-24T10:00:00.000000Z",
"consumed_at": null
},
"upcoming_rewards": []
}
}
Example response (422):
{
"message": "The selected business id is invalid.",
"errors": {
"business_id": [
"The selected business id is invalid."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Referral program administration
Get referral program settings.
requires authentication
Super admin only. reward_plan_id is the configured plan, or the entry paid plan
when none is configured. It is the fallback: a referrer who has paid before is
rewarded with the plan they last paid for, and only a referrer who never paid gets
this plan. Program totals are included for the admin overview.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/referral-settings" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/admin/referral-settings"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/referral-settings';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"enabled": true,
"reward_plan_id": 2,
"reward_months": 1,
"whatsapp_message": "Hey! Check out Dukanam. You can register using my referral link: {link}",
"placeholders": [
"{link}",
"{code}",
"{name}"
],
"totals": {
"referrals": 42,
"successful": 9,
"rewards": 9,
"months_granted": 9,
"rewards_running": 3
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update referral program settings.
requires authentication
Super admin only. Takes effect for the next reward earned; rewards already earned keep the plan and months they were earned with. Turning the program off stops new sign-ups from being attributed and new rewards from being earned.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/admin/referral-settings" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"enabled\": true,
\"reward_plan_id\": 2,
\"reward_months\": 1,
\"whatsapp_message\": \"Hey! Check out Dukanam. You can register using my referral link: {link}\"
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/referral-settings"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"enabled": true,
"reward_plan_id": 2,
"reward_months": 1,
"whatsapp_message": "Hey! Check out Dukanam. You can register using my referral link: {link}"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/referral-settings';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'enabled' => true,
'reward_plan_id' => 2,
'reward_months' => 1,
'whatsapp_message' => 'Hey! Check out Dukanam. You can register using my referral link: {link}',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"enabled": true,
"reward_plan_id": 3,
"reward_months": 2,
"whatsapp_message": "Join me on Dukanam: {link}",
"placeholders": [
"{link}",
"{code}",
"{name}"
],
"totals": {
"referrals": 42,
"successful": 9,
"rewards": 9,
"months_granted": 9,
"rewards_running": 3
}
}
}
Example response (422):
{
"message": "Choose an active paid plan as the referral reward.",
"errors": {
"reward_plan_id": [
"Choose an active paid plan as the referral reward."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List all referrals.
requires authentication
Super admin only. Every referral with both accounts, its status and any reward.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/referrals?status=reward_earned&search=priya&per_page=25" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"status\": \"architecto\",
\"search\": \"n\",
\"per_page\": 7
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/referrals"
);
const params = {
"status": "reward_earned",
"search": "priya",
"per_page": "25",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"status": "architecto",
"search": "n",
"per_page": 7
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/referrals';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'status' => 'reward_earned',
'search' => 'priya',
'per_page' => '25',
],
'json' => [
'status' => 'architecto',
'search' => 'n',
'per_page' => 7,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 12,
"code": "K7M2QX9A",
"status": "reward_earned",
"status_label": "Reward earned",
"source": "web",
"referrer": {
"id": 4,
"name": "Priya Rao",
"email": "[email protected]"
},
"referred": {
"id": 31,
"name": "Meena S",
"email": "[email protected]"
},
"qualifying_business": {
"id": 40,
"name": "Meena Stores"
},
"registered_at": "2026-09-10T08:00:00.000000Z",
"subscription_pending_at": "2026-09-12T08:00:00.000000Z",
"subscription_purchased_at": "2026-09-15T10:00:00.000000Z",
"reward_earned_at": "2026-09-15T10:00:00.000000Z",
"reward_applied_at": null,
"reward_skipped_reason": null,
"reward": {
"id": 8,
"status": "available",
"months": 1,
"remaining_days": null,
"plan": {
"id": 2,
"name": "Smart Books",
"slug": "smart"
},
"business": null,
"referred_name": null,
"earned_at": "2026-09-15T10:00:00.000000Z",
"activated_at": null,
"starts_at": null,
"ends_at": null,
"consumed_at": null
}
}
],
"links": {},
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List rewards granted.
requires authentication
Super admin only. Who received referral rewards, on which workspace and plan, and whether each is waiting, running or used.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/referral-rewards?status=active&search=priya&per_page=25" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"status\": \"architecto\",
\"search\": \"n\",
\"per_page\": 7
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/referral-rewards"
);
const params = {
"status": "active",
"search": "priya",
"per_page": "25",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"status": "architecto",
"search": "n",
"per_page": 7
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/referral-rewards';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'status' => 'active',
'search' => 'priya',
'per_page' => '25',
],
'json' => [
'status' => 'architecto',
'search' => 'n',
'per_page' => 7,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 8,
"status": "active",
"months": 1,
"remaining_days": null,
"plan": {
"id": 2,
"name": "Smart Books",
"slug": "smart"
},
"business": {
"id": 17,
"name": "Aarogya Medical Store"
},
"referred_name": "Meena S",
"earned_at": "2026-09-15T10:00:00.000000Z",
"activated_at": "2026-09-24T10:00:00.000000Z",
"starts_at": "2026-09-24T10:00:00.000000Z",
"ends_at": "2026-10-24T10:00:00.000000Z",
"consumed_at": null,
"user": {
"id": 4,
"name": "Priya Rao",
"email": "[email protected]"
},
"referral_id": 12,
"subscription_id": 95
}
],
"links": {},
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Data exports
An export too large to finish inside a request is generated on the queue. The export endpoint
answers 202 with the record below; poll it until status is completed, then fetch
download_url. Generated files are a convenience copy of data the workspace already owns and
are deleted once they expire.
Export business data
requires authentication
Reporting exports download as dukanam-{type}-YYYY-MM-DD.{csv|xlsx}. format=csv stays the default,
so an existing caller sees no change. format=xlsx returns a workbook with one sheet per requested
section, a frozen header row, real dates and money as numbers a spreadsheet can sum.
type accepts several sections at once — type=invoices,expenses or type[]=invoices&type[]=expenses —
which requires format=xlsx, since a CSV file holds one table. Several sections download as
dukanam-export-YYYY-MM-DD.xlsx.
from and to narrow ledger, invoice and expense exports to a period. Contact exports carry lifetime
balances and refuse a date range rather than quietly ignoring it.
An export larger than the queued-export threshold is generated on the queue: the response is 202
carrying the export record, which is polled until status is completed and then fetched from
download_url. Pass async=1 to ask for that regardless of size. The web workspace keeps streaming
its downloads inline, since a browser link has nowhere to poll.
Invoice CSV and Excel exports use readable status labels, such as Saved, instead of internal status codes.
Exports are online-only: the file is generated on the server from the recorded transactions, and nothing here belongs in the app's offline cache or its write queue.
The legacy business-backup type returns a dukanam-business-export-v2 owner/admin-only customer-data
portability export named dukanam-business-export-YYYY-MM-DD.json. It includes item photos and compliance
evidence content, item classifications, GST percentage masters, sale warranty snapshots and warranty delivery records,
but is not a restorable platform backup. Warranty tokens are private buyer access links;
protect the archive accordingly.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/exports?type=invoices&format=xlsx&from=2026-04-01&to=2027-03-31&async=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"type\": [
\"architecto\"
],
\"format\": \"csv\",
\"from\": \"2026-01-15\",
\"to\": \"2026-01-15\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/exports"
);
const params = {
"type": "invoices",
"format": "xlsx",
"from": "2026-04-01",
"to": "2027-03-31",
"async": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"type": [
"architecto"
],
"format": "csv",
"from": "2026-01-15",
"to": "2026-01-15"
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/exports';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'type' => 'invoices',
'format' => 'xlsx',
'from' => '2026-04-01',
'to' => '2027-03-31',
'async' => '1',
],
'json' => [
'type' => ['architecto'],
'format' => 'csv',
'from' => '2026-01-15',
'to' => '2026-01-15',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (202):
{
"data": {
"uuid": "9b5f…",
"types": [
"invoices"
],
"format": "xlsx",
"status": "queued",
"download_url": null
}
}
Example response (422):
{
"message": "Several sections can only be exported together as a workbook. Use format=xlsx or export one type at a time.",
"errors": {
"format": [
"Several sections can only be exported together as a workbook. Use format=xlsx or export one type at a time."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Export the books for Tally Prime
requires authentication
Returns Tally-compatible XML: the shop's chart of accounts, parties, units and stock items as masters, and its accounting entries as accounting vouchers. The CA imports the file into a Tally company with Gateway of Tally → Import → Vouchers, which is how a shop hands its year over without keeping a second set of books.
mode chooses what the file carries — masters, vouchers, or both (the default). A voucher
export needs from and to and covers at most one financial year per call; a masters export is
the whole chart and refuses a date range.
Vouchers are built from the same accounting entries Dukanam's own trial balance is built from, so the
two agree. A voucher export starting after the books did also carries one opening journal dated the
day before from, holding every ledger and party balance as it stood — without it the CA would
receive a year of movement with no starting position.
Each voucher carries a REMOTEID derived from Dukanam's own ids, so re-importing an overlapping
date range updates the vouchers already there instead of writing a second copy of the same sales.
The file is accounting-only: stock items arrive as masters with their units and HSN codes, but the vouchers carry ledger entries rather than inventory allocations, so Tally's stock summary is not populated from this export. The mapping from Dukanam's chart of accounts onto Tally's groups is written into the top of the file as a comment, for the accountant to check.
A large export is generated on the queue exactly as the workbook export is: the response is 202
carrying the export record, polled until status is completed and then fetched from download_url.
Pass async=1 to ask for that whatever the size. This is online-only — nothing here belongs in the
app's offline cache or its write queue.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/exports/tally?mode=both&from=2026-04-01&to=2027-03-31&async=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"mode\": \"masters\",
\"from\": \"2026-01-15\",
\"to\": \"2026-01-15\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/exports/tally"
);
const params = {
"mode": "both",
"from": "2026-04-01",
"to": "2027-03-31",
"async": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"mode": "masters",
"from": "2026-01-15",
"to": "2026-01-15"
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/exports/tally';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'mode' => 'both',
'from' => '2026-04-01',
'to' => '2027-03-31',
'async' => '1',
],
'json' => [
'mode' => 'masters',
'from' => '2026-01-15',
'to' => '2026-01-15',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
Binary data - The generated Tally XML.
Example response (202):
{
"data": {
"uuid": "9b5f…",
"types": [
"tally-both"
],
"format": "tally",
"status": "queued",
"filename": "dukanam-tally-2026-09-15.xml",
"download_url": null
}
}
Example response (422):
{
"message": "A Tally voucher export needs both a start and an end date.",
"errors": {
"from": [
"A Tally voucher export needs both a start and an end date."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List queued exports.
requires authentication
Newest first, whatever their status, so a failed export is visible rather than silent.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/exports/queued" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/exports/queued"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/exports/queued';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Read one queued export.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/exports/queued/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/exports/queued/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/exports/queued/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
status
string
One of queued, processing, completed or failed.
download_url
string
Present only while a generated file is available to fetch.
Download a generated export.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/exports/queued/1/download" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/exports/queued/1/download"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/exports/queued/1/download';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
Binary data - The generated CSV or workbook.
Example response (409):
{
"message": "This export is not ready yet."
}
Example response (410):
{
"message": "This export has expired. Request it again."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Team access
Owners and administrators can manage workspace members. Smart Books supports one accountant seat in addition to the owner. Business supports all documented roles, with five total seats including the owner and pending invitations.
List team members, pending invitations, and the 25 most recent expired invitations.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/team" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/team"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/team';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"members": [
{
"id": 2,
"name": "Priya Rao",
"email": "[email protected]",
"role": "cashier",
"status": "active",
"joined_at": "2026-08-21T12:00:00.000000Z"
}
],
"invitations": [],
"roles": {
"cashier": {
"label": "Cashier",
"description": "POS, sales, and cash register access."
},
"accountant": {
"label": "Accountant",
"description": "Sales, purchases, inventory, accounting, reports, compliance, and exports."
}
},
"seats": {
"used": 2,
"limit": 5,
"active": 2,
"pending": 0
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
seats
object
limit
integer
Total workspace seat limit. A value of 0 means unlimited seats.
Invite a team member.
requires authentication
Pending invitations reserve a seat and expire after seven days.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/team/invitations" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"email\": \"[email protected]\",
\"role\": \"cashier\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/team/invitations"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"email": "[email protected]",
"role": "cashier"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/team/invitations';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'email' => '[email protected]',
'role' => 'cashier',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 12,
"email": "[email protected]",
"role": "cashier",
"status": "pending",
"expires_at": "2026-08-28T12:00:00.000000Z",
"accepted_at": null,
"invited_by": {
"id": 1,
"name": "Workspace Owner"
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Resend a pending or expired invitation with a new secure token.
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/team/invitations/1/resend" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/team/invitations/1/resend"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/team/invitations/1/resend';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 12,
"email": "[email protected]",
"role": "cashier",
"status": "pending",
"expires_at": "2026-08-28T12:00:00.000000Z",
"accepted_at": null,
"invited_by": {
"id": 1,
"name": "Workspace Owner"
}
}
}
Example response (422):
{
"message": "All 2 workspace seats are already assigned or reserved.",
"errors": {
"email": [
"All 2 workspace seats are already assigned or reserved."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Cancel a pending invitation and release its reserved seat.
requires authentication
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/team/invitations/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/team/invitations/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/team/invitations/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204):
Empty response
Example response (403):
{
"message": "Only the owner can manage administrator invitations."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Change a member's role or suspend/reactivate access.
requires authentication
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/team/members/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"role\": \"manager\",
\"status\": \"active\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/team/members/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"role": "manager",
"status": "active"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/team/members/1';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'role' => 'manager',
'status' => 'active',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 2,
"name": "Priya Rao",
"email": "[email protected]",
"role": "manager",
"status": "active",
"joined_at": "2026-08-21T12:00:00.000000Z"
}
}
Example response (422):
{
"message": "All 2 workspace seats are already assigned or reserved.",
"errors": {
"role": [
"All 2 workspace seats are already assigned or reserved."
],
"status": [
"All 2 workspace seats are already assigned or reserved."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Remove a member's workspace access without deleting their user account.
requires authentication
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/team/members/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/team/members/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/team/members/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204):
Empty response
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create a POS login against a paid counter seat.
requires authentication
A POS login can open the billing counter and its cash drawer and nothing else. Seats
are bought with POST /businesses/{business}/billing/pos-seats; creating a login
without a free paid seat returns 422. Unlike an invitation the password is set here
and handed to the counter staff directly.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/team/pos-logins" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Ravi Kumar\",
\"email\": \"[email protected]\",
\"password\": \"counter-password\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/team/pos-logins"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Ravi Kumar",
"email": "[email protected]",
"password": "counter-password"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/team/pos-logins';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Ravi Kumar',
'email' => '[email protected]',
'password' => 'counter-password',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 7,
"name": "Ravi Kumar",
"email": "[email protected]",
"role": "pos",
"status": "active",
"joined_at": "2026-09-02T12:00:00.000000Z"
}
}
Example response (422):
{
"message": "Buy a POS login before creating counter staff.",
"errors": {
"email": [
"Buy a POS login before creating counter staff."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Replace a POS login's password and sign its devices out.
requires authentication
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/team/pos-logins/1/password" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"password\": \"new-counter-password\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/team/pos-logins/1/password"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"password": "new-counter-password"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/team/pos-logins/1/password';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'password' => 'new-counter-password',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 7,
"name": "Ravi Kumar",
"email": "[email protected]",
"role": "pos",
"status": "active",
"joined_at": "2026-09-02T12:00:00.000000Z"
}
}
Example response (422):
{
"message": "That member is not a POS login."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Team invitations
Accept a team invitation as an existing user.
requires authentication
Send a valid Sanctum bearer token belonging to the invited email. This endpoint does not create a new token.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/team-invitations/4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa/accept" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/team-invitations/4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa/accept"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/team-invitations/4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa/accept';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"user": {
"id": 2,
"name": "Priya Rao",
"email": "[email protected]"
},
"business": {
"id": 1,
"name": "Anika Stores",
"role": "cashier"
}
}
}
Example response (404):
{
"message": "Not Found"
}
Example response (422):
{
"message": "The given data was invalid.",
"errors": {
"email": [
"Sign in with the email address that received this invitation."
],
"invitation": [
"This workspace is no longer available."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Inspect a team invitation.
This endpoint does not require authentication. The opaque token is supplied by the invitation email.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/team-invitations/4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/team-invitations/4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/team-invitations/4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa';
$response = $client->get(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"business": {
"id": 1,
"name": "Anika Stores"
},
"email": "[email protected]",
"role": "cashier",
"role_label": "Cashier",
"status": "pending",
"expires_at": "2026-08-28T12:00:00.000000Z",
"existing_account": false
}
}
Example response (404):
{
"message": "Not Found"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create an account and accept a team invitation.
Use this unauthenticated endpoint only when the invited email does not already belong to a Dukanam user. The response includes a new Sanctum device token.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/team-invitations/4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa/register-and-accept" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Priya Rao\",
\"password\": \"secret-pass-123\",
\"device_name\": \"Priya\'s phone\",
\"password_confirmation\": \"secret-pass-123\"
}"
const url = new URL(
"https://dukanam.com/api/v1/team-invitations/4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa/register-and-accept"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Priya Rao",
"password": "secret-pass-123",
"device_name": "Priya's phone",
"password_confirmation": "secret-pass-123"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/team-invitations/4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa/register-and-accept';
$response = $client->post(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Priya Rao',
'password' => 'secret-pass-123',
'device_name' => 'Priya\'s phone',
'password_confirmation' => 'secret-pass-123',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"user": {
"id": 2,
"name": "Priya Rao",
"email": "[email protected]"
},
"business": {
"id": 1,
"name": "Anika Stores",
"role": "cashier"
},
"token": "1|new-mobile-token"
}
}
Example response (403):
{
"message": "Sign in as the invited user to accept this invitation."
}
Example response (404):
{
"message": "Not Found"
}
Example response (422):
{
"message": "This invitation has expired or is no longer available.",
"errors": {
"invitation": [
"This invitation has expired or is no longer available."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Testimonials
List my workspace testimonials.
requires authentication
Returns only testimonials submitted by the signed-in user for this workspace. Includes admin-authored drafts linked to this business owner; this grants no workspace access. photo_url may be null for a text-only draft.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/17/testimonials?per_page=12" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/17/testimonials"
);
const params = {
"per_page": "12",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/17/testimonials';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '12',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": "53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29",
"store_type": "pharmacy",
"feedback": "Dukanam keeps our counter records together.",
"photo_url": "https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo",
"status": "pending",
"submitted_at": "2026-08-31T10:15:00.000000Z",
"published_at": null
}
],
"links": {},
"meta": {
"current_page": 1,
"per_page": 12,
"total": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Submit a testimonial from a subscribed workspace.
requires authentication
Requires a usable non-free subscription. Name, email and shop identity are taken from the authenticated user and workspace, not request fields.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/17/testimonials" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "store_type=pharmacy"\
--form "feedback=Dukanam made our daily medicine counter billing easier to review, and the purchase and stock records now stay together for closing."\
--form "consent=1"\
--form "website="\
--form "photo=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/businesses/17/testimonials"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('store_type', 'pharmacy');
body.append('feedback', 'Dukanam made our daily medicine counter billing easier to review, and the purchase and stock records now stay together for closing.');
body.append('consent', '1');
body.append('website', '');
body.append('photo', document.querySelector('input[name="photo"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/17/testimonials';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'store_type',
'contents' => 'pharmacy'
],
[
'name' => 'feedback',
'contents' => 'Dukanam made our daily medicine counter billing easier to review, and the purchase and stock records now stay together for closing.'
],
[
'name' => 'consent',
'contents' => '1'
],
[
'name' => 'website',
'contents' => ''
],
[
'name' => 'photo',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (202):
{
"message": "Thank you. Your testimonial is awaiting review.",
"data": {
"id": "53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29",
"store_type": "pharmacy",
"feedback": "Dukanam keeps our counter records together.",
"photo_url": "https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo",
"status": "pending",
"submitted_at": "2026-08-31T10:15:00.000000Z",
"published_at": null
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List approved public testimonials.
Includes only consented stories; internal uxcrafts.com owners and businesses are excluded. Admin-authored stories may be text-only.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/testimonials?store_type=electrical-store&per_page=12" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/testimonials"
);
const params = {
"store_type": "electrical-store",
"per_page": "12",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/testimonials';
$response = $client->get(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'store_type' => 'electrical-store',
'per_page' => '12',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": "53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29",
"store_type": "pharmacy",
"owner_name": "Dr. Kavitha Reddy",
"shop_name": "Aarogya Medical & General Store",
"feedback": "Dukanam made our daily medicine counter billing easier to review.",
"photo_url": "https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo",
"published_at": "2026-08-31T10:30:00.000000Z",
"location": null
}
],
"links": {
"first": "https://dukanam.com/api/v1/testimonials?page=1",
"last": "https://dukanam.com/api/v1/testimonials?page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 1,
"links": [],
"path": "https://dukanam.com/api/v1/testimonials",
"per_page": 12,
"to": 1,
"total": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
location
string|null
Optional city/region.
photo_url
string|null
Null for a text-only story.
Testimonial moderation
List testimonial submissions for moderation.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/testimonials?status=pending&store_type=electrical-store&per_page=24" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/admin/testimonials"
);
const params = {
"status": "pending",
"store_type": "electrical-store",
"per_page": "24",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/testimonials';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'status' => 'pending',
'store_type' => 'electrical-store',
'per_page' => '24',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": "53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29",
"store_type": "pharmacy",
"user_id": 42,
"business_id": 17,
"owner_name": "Dr. Kavitha Reddy",
"shop_name": "Aarogya Medical & General Store",
"contact_email": "[email protected]",
"feedback": "Dukanam made our daily medicine counter billing easier to review.",
"photo_url": "https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo",
"photo_mime_type": "image/webp",
"photo_size": 98214,
"status": "pending",
"consent_at": "2026-08-31T10:15:00.000000Z",
"review_note": null,
"reviewed_by": null,
"reviewed_at": null,
"published_at": null,
"submitted_at": "2026-08-31T10:15:00.000000Z",
"location": null,
"source": "customer",
"consent_note": null
}
],
"links": {
"first": "https://dukanam.com/api/v1/admin/testimonials?page=1",
"last": "https://dukanam.com/api/v1/admin/testimonials?page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 1,
"links": [],
"path": "https://dukanam.com/api/v1/admin/testimonials",
"per_page": 24,
"to": 1,
"total": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
store_type
string|null
Null for unassigned admin drafts.
contact_email
string
Private submitter contact; never returned by the public endpoint.
location
string|null
Optional public city/region.
source
string
customer or admin.
consent_note
string|null
Private permission evidence.
consent_at
string|null
Null on drafts awaiting permission.
photo_url
string|null
Null on text-only stories; photo metadata is also nullable.
Create a customer testimonial draft.
requires authentication
Super admins may draft on behalf of a customer on any plan. Identity and private contact email come from the selected business and its owner; display-name corrections are allowed. uxcrafts.com and its subdomains are excluded for both the owner and business email. Drafts always start pending. Photos are optional. Permission for the exact wording and attribution must be recorded with consent=true and a private consent_note before approval.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/admin/testimonials" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "business_id=17"\
--form "store_type=pharmacy"\
--form "owner_name=Kavitha Reddy"\
--form "shop_name=Aarogya Medical Store"\
--form "location=Hyderabad, Telangana"\
--form "feedback=We have had a positive experience using Dukanam for our pharmacy in Hyderabad."\
--form "remove_photo="\
--form "consent=1"\
--form "consent_note=Owner approved this wording and public attribution in our customer conversation."\
--form "photo=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/admin/testimonials"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('business_id', '17');
body.append('store_type', 'pharmacy');
body.append('owner_name', 'Kavitha Reddy');
body.append('shop_name', 'Aarogya Medical Store');
body.append('location', 'Hyderabad, Telangana');
body.append('feedback', 'We have had a positive experience using Dukanam for our pharmacy in Hyderabad.');
body.append('remove_photo', '');
body.append('consent', '1');
body.append('consent_note', 'Owner approved this wording and public attribution in our customer conversation.');
body.append('photo', document.querySelector('input[name="photo"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/testimonials';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'business_id',
'contents' => '17'
],
[
'name' => 'store_type',
'contents' => 'pharmacy'
],
[
'name' => 'owner_name',
'contents' => 'Kavitha Reddy'
],
[
'name' => 'shop_name',
'contents' => 'Aarogya Medical Store'
],
[
'name' => 'location',
'contents' => 'Hyderabad, Telangana'
],
[
'name' => 'feedback',
'contents' => 'We have had a positive experience using Dukanam for our pharmacy in Hyderabad.'
],
[
'name' => 'remove_photo',
'contents' => ''
],
[
'name' => 'consent',
'contents' => '1'
],
[
'name' => 'consent_note',
'contents' => 'Owner approved this wording and public attribution in our customer conversation.'
],
[
'name' => 'photo',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": "53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29",
"business_id": 17,
"user_id": 42,
"store_type": "pharmacy",
"owner_name": "Kavitha Reddy",
"shop_name": "Aarogya Medical Store",
"location": "Hyderabad",
"feedback": "We have had a positive experience using Dukanam for our pharmacy in Hyderabad.",
"source": "admin",
"status": "pending",
"photo_url": null,
"photo_mime_type": null,
"photo_size": null,
"consent_at": null,
"consent_note": null,
"published_at": null
}
}
Example response (422):
{
"message": "Choose a customer business with an owner outside uxcrafts.com and its subdomains.",
"errors": {
"business_id": [
"Choose a customer business with an owner outside uxcrafts.com and its subdomains."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
store_type
string|null
Optional industry page; required before approval.
source
string
customer for owner submissions, admin for administrator-authored drafts.
location
string|null
Optional public city or region.
photo_url
string|null
Null for text-only testimonials.
consent_at
string|null
Null when permission has not been recorded.
consent_note
string|null
Private permission evidence, never returned publicly.
Edit a testimonial and return it to pending review.
requires authentication
The business reference cannot be changed. Every edit unpublishes the old version from matching web pages and the public API. Omit photo to keep it, or set remove_photo=true to remove it. Confirm consent and supply its private note again for the revised wording; omitted/false consent clears permission. For multipart edits, POST this URL with _method=PUT.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/admin/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "store_type=pharmacy"\
--form "owner_name=Kavitha Reddy"\
--form "shop_name=Aarogya Medical Store"\
--form "location=Hyderabad, Telangana"\
--form "feedback=We have had a positive experience using Dukanam for our pharmacy in Hyderabad."\
--form "remove_photo="\
--form "consent=1"\
--form "consent_note=Owner approved this wording and public attribution in our customer conversation."\
--form "photo=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/admin/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('store_type', 'pharmacy');
body.append('owner_name', 'Kavitha Reddy');
body.append('shop_name', 'Aarogya Medical Store');
body.append('location', 'Hyderabad, Telangana');
body.append('feedback', 'We have had a positive experience using Dukanam for our pharmacy in Hyderabad.');
body.append('remove_photo', '');
body.append('consent', '1');
body.append('consent_note', 'Owner approved this wording and public attribution in our customer conversation.');
body.append('photo', document.querySelector('input[name="photo"]').files[0]);
fetch(url, {
method: "PUT",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'store_type',
'contents' => 'pharmacy'
],
[
'name' => 'owner_name',
'contents' => 'Kavitha Reddy'
],
[
'name' => 'shop_name',
'contents' => 'Aarogya Medical Store'
],
[
'name' => 'location',
'contents' => 'Hyderabad, Telangana'
],
[
'name' => 'feedback',
'contents' => 'We have had a positive experience using Dukanam for our pharmacy in Hyderabad.'
],
[
'name' => 'remove_photo',
'contents' => ''
],
[
'name' => 'consent',
'contents' => '1'
],
[
'name' => 'consent_note',
'contents' => 'Owner approved this wording and public attribution in our customer conversation.'
],
[
'name' => 'photo',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": "53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29",
"business_id": 17,
"user_id": 42,
"store_type": "pharmacy",
"owner_name": "Kavitha Reddy",
"shop_name": "Aarogya Medical Store",
"location": "Hyderabad",
"feedback": "We have had a positive experience using Dukanam for our pharmacy in Hyderabad.",
"source": "admin",
"status": "pending",
"photo_url": null,
"photo_mime_type": null,
"photo_size": null,
"consent_at": null,
"consent_note": null,
"published_at": null
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Approve or reject a testimonial.
requires authentication
Approval publishes the story on its matching industry page. Rejection removes it from public API and web responses while preserving the private moderation record. Approval returns 422 without recorded permission or for internal uxcrafts.com businesses/owners. Rejection is the reversible Unpublish action.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/admin/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"status\": \"approved\",
\"review_note\": \"Photo and first-hand statement verified.\"
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"status": "approved",
"review_note": "Photo and first-hand statement verified."
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'status' => 'approved',
'review_note' => 'Photo and first-hand statement verified.',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": "53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29",
"store_type": "pharmacy",
"user_id": 42,
"business_id": 17,
"owner_name": "Dr. Kavitha Reddy",
"shop_name": "Aarogya Medical & General Store",
"contact_email": "[email protected]",
"feedback": "Dukanam made our daily medicine counter billing easier to review.",
"photo_url": "https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo",
"photo_mime_type": "image/webp",
"photo_size": 98214,
"status": "approved",
"consent_at": "2026-08-31T10:15:00.000000Z",
"review_note": "Verified",
"reviewed_by": {
"id": 1,
"name": "Platform Admin"
},
"reviewed_at": "2026-08-31T10:30:00.000000Z",
"published_at": "2026-08-31T10:30:00.000000Z",
"submitted_at": "2026-08-31T10:15:00.000000Z",
"location": null,
"source": "customer",
"consent_note": null
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Feedback
List my private feedback.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/17/feedback?per_page=12" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/17/feedback"
);
const params = {
"per_page": "12",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/17/feedback';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '12',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": "82fdb616-df56-4547-9e09-5f58f4740acd",
"feedback_type": "suggestion",
"message": "Please add a daily counter export to the mobile dashboard.",
"photo_url": null,
"status": "pending",
"response": null,
"submitted_at": "2026-08-31T10:15:00.000000Z",
"reviewed_at": null
}
],
"links": {},
"meta": {
"current_page": 1,
"per_page": 12,
"total": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Submit private feedback.
requires authentication
Available to every authenticated workspace user, including free plans. Feedback is never added to public testimonial pages.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/17/feedback" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "feedback_type=suggestion"\
--form "message=Please add an option to export the daily counter summary directly from the mobile dashboard."\
--form "website="\
--form "photo=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/businesses/17/feedback"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('feedback_type', 'suggestion');
body.append('message', 'Please add an option to export the daily counter summary directly from the mobile dashboard.');
body.append('website', '');
body.append('photo', document.querySelector('input[name="photo"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/17/feedback';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'feedback_type',
'contents' => 'suggestion'
],
[
'name' => 'message',
'contents' => 'Please add an option to export the daily counter summary directly from the mobile dashboard.'
],
[
'name' => 'website',
'contents' => ''
],
[
'name' => 'photo',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (202):
{
"message": "Thank you. Your feedback has been sent to the Dukanam team.",
"data": {
"id": "82fdb616-df56-4547-9e09-5f58f4740acd",
"feedback_type": "suggestion",
"message": "Please add a daily counter export to the mobile dashboard.",
"photo_url": null,
"status": "pending",
"response": null,
"submitted_at": "2026-08-31T10:15:00.000000Z",
"reviewed_at": null
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Feedback moderation
List private customer feedback.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/feedback?status=pending&feedback_type=complaint&per_page=24" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"per_page\": 1
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/feedback"
);
const params = {
"status": "pending",
"feedback_type": "complaint",
"per_page": "24",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"per_page": 1
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/feedback';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'status' => 'pending',
'feedback_type' => 'complaint',
'per_page' => '24',
],
'json' => [
'per_page' => 1,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": "82fdb616-df56-4547-9e09-5f58f4740acd",
"feedback_type": "complaint",
"message": "The mobile report did not load after closing the counter.",
"photo_url": null,
"photo_mime_type": null,
"photo_size": null,
"status": "pending",
"submitter": {
"id": 42,
"name": "Kavitha Reddy",
"email": "[email protected]"
},
"business": {
"id": 17,
"name": "Aarogya Medical Store"
},
"response": null,
"reviewed_by": null,
"submitted_at": "2026-08-31T10:15:00.000000Z",
"reviewed_at": null
}
],
"links": {},
"meta": {
"current_page": 1,
"per_page": 24,
"total": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Mark customer feedback reviewed or resolved.
requires authentication
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/admin/feedback/82fdb616-df56-4547-9e09-5f58f4740acd" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"status\": \"reviewed\",
\"review_note\": \"Added to the mobile dashboard backlog.\"
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/feedback/82fdb616-df56-4547-9e09-5f58f4740acd"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"status": "reviewed",
"review_note": "Added to the mobile dashboard backlog."
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/feedback/82fdb616-df56-4547-9e09-5f58f4740acd';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'status' => 'reviewed',
'review_note' => 'Added to the mobile dashboard backlog.',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": "82fdb616-df56-4547-9e09-5f58f4740acd",
"feedback_type": "suggestion",
"message": "Please add a mobile daily summary export.",
"photo_url": null,
"photo_mime_type": null,
"photo_size": null,
"status": "reviewed",
"submitter": {
"id": 42,
"name": "Kavitha Reddy",
"email": "[email protected]"
},
"business": {
"id": 17,
"name": "Aarogya Medical Store"
},
"response": "Added to the mobile backlog.",
"reviewed_by": {
"id": 1,
"name": "Platform Admin"
},
"submitted_at": "2026-08-31T10:15:00.000000Z",
"reviewed_at": "2026-08-31T11:00:00.000000Z"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Apple subscriptions
Apple App Store subscription verification and authoritative Dukanam entitlement state.
Owner endpoints require the business's owner_user_id to match the signed-in user,
as with web billing and API onboarding. A pivot role alone does not confer ownership.
Verify an App Store subscription purchase
requires authentication
Send either StoreKit's signed transaction JWS or a transaction ID plus its environment. The business owner
must pass the app_account_token returned by the current-subscription endpoint to StoreKit when purchasing.
The plan, dates, and status always come from Apple's signed data.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/apple/subscription/verify" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"business_id\": 1,
\"signedTransactionInfo\": \"eyJhbGciOiJFUzI1NiIsIng1YyI6WyIuLi4iXX0.eyJ0cmFuc2FjdGlvbklkIjoiMjAwMDAwMTIzNDU2Nzg5MCJ9.signature\",
\"transactionId\": \"2000001234567890\",
\"environment\": \"sandbox\"
}"
const url = new URL(
"https://dukanam.com/api/v1/apple/subscription/verify"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"business_id": 1,
"signedTransactionInfo": "eyJhbGciOiJFUzI1NiIsIng1YyI6WyIuLi4iXX0.eyJ0cmFuc2FjdGlvbklkIjoiMjAwMDAwMTIzNDU2Nzg5MCJ9.signature",
"transactionId": "2000001234567890",
"environment": "sandbox"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/apple/subscription/verify';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'business_id' => 1,
'signedTransactionInfo' => 'eyJhbGciOiJFUzI1NiIsIng1YyI6WyIuLi4iXX0.eyJ0cmFuc2FjdGlvbklkIjoiMjAwMDAwMTIzNDU2Nzg5MCJ9.signature',
'transactionId' => '2000001234567890',
'environment' => 'sandbox',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"success": true,
"business_id": 1,
"app_account_token": "2ee48be5-b23e-46e8-b84d-5e21850d42d2",
"subscription": {
"provider": "apple",
"plan": "business",
"billing_period": "yearly",
"product_id": "com.dukanam.business.yearly",
"status": "active",
"is_active": true,
"expires_at": "2027-09-04T10:30:00.000000Z",
"auto_renew_status": true,
"environment": "production"
}
}
Example response (403):
{
"message": "Only the business owner can manage this subscription."
}
Example response (409):
{
"message": "The Apple transaction is not linked to this Dukanam business."
}
Example response (422):
{
"message": "This Apple product is not supported."
}
Example response (503):
{
"message": "Apple subscriptions are not enabled."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Synchronize an App Store subscription
requires authentication
Reconciles the linked subscription with the App Store Server API. Use after Restore Purchases or to recover from a missed server notification.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/apple/subscription/sync" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"business_id\": 1
}"
const url = new URL(
"https://dukanam.com/api/v1/apple/subscription/sync"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"business_id": 1
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/apple/subscription/sync';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'business_id' => 1,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (403):
{
"message": "Only the business owner can manage this subscription."
}
Example response (422):
{
"message": "No Apple subscription has been linked to this business yet."
}
Example response (503):
{
"message": "App Store Server API credentials are not configured."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get the current subscription entitlement
requires authentication
Returns the backend-authoritative plan and the stable app_account_token that Flutter must attach to every
StoreKit purchase for this business. Free Essentials is returned when there is no usable paid entitlement.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/me/subscription?business_id=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/me/subscription"
);
const params = {
"business_id": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/me/subscription';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'business_id' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"success": true,
"business_id": 1,
"app_account_token": "2ee48be5-b23e-46e8-b84d-5e21850d42d2",
"subscription": {
"provider": null,
"plan": "free_essentials",
"billing_period": null,
"product_id": null,
"status": "active",
"is_active": true,
"expires_at": null,
"auto_renew_status": null,
"environment": null
}
}
Example response (403):
{
"message": "Only the business owner can manage this subscription."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Receive App Store Server Notifications V2
Public Apple webhook. Every payload and nested transaction is cryptographically verified and processed idempotently by notification UUID. This endpoint does not use Dukanam bearer authentication.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/apple/notifications" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"signedPayload\": \"eyJhbGciOiJFUzI1NiIsIng1YyI6WyIuLi4iXX0.eyJub3RpZmljYXRpb25UeXBlIjoiVEVTVCJ9.signature\"
}"
const url = new URL(
"https://dukanam.com/api/v1/apple/notifications"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"signedPayload": "eyJhbGciOiJFUzI1NiIsIng1YyI6WyIuLi4iXX0.eyJub3RpZmljYXRpb25UeXBlIjoiVEVTVCJ9.signature"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/apple/notifications';
$response = $client->post(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'signedPayload' => 'eyJhbGciOiJFUzI1NiIsIng1YyI6WyIuLi4iXX0.eyJub3RpZmljYXRpb25UeXBlIjoiVEVTVCJ9.signature',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"success": true
}
Example response (400):
{
"message": "Apple could not verify the signed notification."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Account deletion
Request, inspect and cancel deletion of the signed-in account and its data, as required by the Google Play user data policy. The public web equivalent of these endpoints is published at /delete-account.
Show the deletion status and what a deletion would destroy.
requires authentication
owned_workspaces are erased entirely, including every team member's access.
joined_workspaces survive; only this user's membership in them is removed.
Apple billing is not cancelled by deletion. Display the warning and management URL
when present; active, trialing and billing-retry Apple records trigger the warning.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/account/deletion" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/account/deletion"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/account/deletion';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"pending": false,
"grace_days": 7,
"scheduled_for": null,
"owned_workspaces": [
{
"name": "Sri Balaji Stores",
"team_members": 2,
"has_billing_records": true,
"has_apple_subscription": true
}
],
"joined_workspaces": [
"Anand Traders"
],
"apple_subscription_warning": "Deleting your Dukanam account does not cancel your Apple subscription. Apple may continue billing you until you cancel it in your Apple subscription settings.",
"apple_subscription_management_url": "https://apps.apple.com/account/subscriptions"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Request deletion of the account and its data.
requires authentication
The account stays usable during the grace period so the request can be cancelled. Push notifications stop immediately. The confirmation email warns owners of outstanding Apple subscriptions and links to Apple's subscription settings. Deletion never cancels Apple billing.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/account/deletion" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"current_password\": \"correct-horse-battery\",
\"confirmation\": \"DELETE\",
\"reason\": \"Closing the shop\"
}"
const url = new URL(
"https://dukanam.com/api/v1/account/deletion"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"current_password": "correct-horse-battery",
"confirmation": "DELETE",
"reason": "Closing the shop"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/account/deletion';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'current_password' => 'correct-horse-battery',
'confirmation' => 'DELETE',
'reason' => 'Closing the shop',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (202):
{
"message": "Your account is scheduled for deletion.",
"data": {
"scheduled_for": "2026-09-06T10:15:00+00:00"
}
}
Example response (403):
{
"message": "Administrator accounts cannot be deleted from the app. Contact support."
}
Example response (422):
{
"message": "The given data was invalid.",
"errors": {
"confirmation": [
"Type DELETE to confirm that you want the account erased."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Cancel a pending deletion request.
requires authentication
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/account/deletion" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/account/deletion"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/account/deletion';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"message": "Account deletion cancelled."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
App link analytics
Opens of the promoted pages: the https://dukanam.com/app download link and the store-type pages (the /billing-software-for-retail-stores index and one page per store type, in every language). /app redirects phones to their store, so its opens are read from the request; store pages may be served from the CDN cache and report their own opens with a beacon, adding reading time, scroll depth, screen size and the first call to action used. Location is Cloudflare's IP-based estimate (city level, approximate). An open is one human visit; link previews and crawlers are counted separately. Dates are calendar days in Asia/Kolkata. Requires a super-admin token.
Summarise app link opens.
requires authentication
Totals, store-page engagement, per-page rows, a zero-filled daily series, a weekday × hour
grid (ISO weekday 1 = Monday, hours 0–23), top-15 breakdowns by source, medium, campaign, platform, device, operating
system, browser, in-app browser, referrer, country, region, language, page language,
call to action and screen size, the top 50
cities with approximate coordinates, and link-preview fetches. previous_period covers
the same number of days immediately before from, for comparison.
Source keys: whatsapp, instagram, facebook, meta (a Meta ad click whose app is unknown),
qr, sms, google, youtube, x, linkedin, telegram, email, referral (another website),
internal (a link on dukanam.com) and direct (untagged with no referrer). Any other utm_source value is kept as sent, normalised.
Outcomes: play_store and app_store are store redirects; landing_page means the page was shown.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/app-link/analytics?from=2026-01-15&to=2026-01-15&source=whatsapp&campaign=diwali_2026&platform=android&tracked_page=app&per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/admin/app-link/analytics"
);
const params = {
"from": "2026-01-15",
"to": "2026-01-15",
"source": "whatsapp",
"campaign": "diwali_2026",
"platform": "android",
"tracked_page": "app",
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/app-link/analytics';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'from' => '2026-01-15',
'to' => '2026-01-15',
'source' => 'whatsapp',
'campaign' => 'diwali_2026',
'platform' => 'android',
'tracked_page' => 'app',
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"range": {
"from": "2026-09-20",
"to": "2026-09-26",
"days": 7
},
"filters": {
"source": null,
"campaign": null,
"platform": null,
"tracked_page": null
},
"totals": {
"opens": 5,
"unique_visitors": 5,
"new_visitors": 4,
"returning_visitors": 1,
"store_redirects": 3,
"play_store": 2,
"app_store": 1,
"landing_page": 2,
"link_previews": 1
},
"engagement": {
"page_views": 2,
"average_seconds": 100,
"average_scroll_depth": 80,
"call_to_action_opens": 2,
"call_to_action_rate": 100,
"signup_or_app_opens": 1
},
"previous_period": {
"from": "2026-09-13",
"to": "2026-09-19",
"opens": 0,
"unique_visitors": 0,
"store_redirects": 0
},
"daily": [
{
"date": "2026-09-20",
"opens": 0,
"unique_visitors": 0,
"play_store": 0,
"app_store": 0,
"landing_page": 0
},
{
"date": "2026-09-21",
"opens": 0,
"unique_visitors": 0,
"play_store": 0,
"app_store": 0,
"landing_page": 0
},
{
"date": "2026-09-22",
"opens": 0,
"unique_visitors": 0,
"play_store": 0,
"app_store": 0,
"landing_page": 0
},
{
"date": "2026-09-23",
"opens": 1,
"unique_visitors": 1,
"play_store": 1,
"app_store": 0,
"landing_page": 0
},
{
"date": "2026-09-24",
"opens": 1,
"unique_visitors": 1,
"play_store": 1,
"app_store": 0,
"landing_page": 0
},
{
"date": "2026-09-25",
"opens": 2,
"unique_visitors": 2,
"play_store": 0,
"app_store": 1,
"landing_page": 1
},
{
"date": "2026-09-26",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
}
],
"weekday_hours": [
{
"weekday": 1,
"hours": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"weekday": 2,
"hours": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"weekday": 3,
"hours": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"weekday": 4,
"hours": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0
]
},
{
"weekday": 5,
"hours": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0
]
},
{
"weekday": 6,
"hours": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"weekday": 7,
"hours": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"pages": [
{
"key": "app",
"label": "App download link (/app)",
"opens": 3,
"unique_visitors": 3,
"play_store": 2,
"app_store": 1,
"landing_page": 0,
"average_seconds": null,
"average_scroll_depth": null,
"call_to_action_opens": 0,
"signup_clicks": 0
},
{
"key": "stores",
"label": "All store types",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1,
"average_seconds": 74,
"average_scroll_depth": 88,
"call_to_action_opens": 1,
"signup_clicks": 0
},
{
"key": "pharmacy",
"label": "Pharmacies",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1,
"average_seconds": 126,
"average_scroll_depth": 71,
"call_to_action_opens": 1,
"signup_clicks": 1
}
],
"breakdowns": {
"sources": [
{
"key": "instagram",
"label": "Instagram",
"opens": 2,
"unique_visitors": 2,
"play_store": 1,
"app_store": 0,
"landing_page": 1
},
{
"key": "whatsapp",
"label": "WhatsApp",
"opens": 1,
"unique_visitors": 1,
"play_store": 1,
"app_store": 0,
"landing_page": 0
},
{
"key": "qr",
"label": "QR code",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 1,
"landing_page": 0
},
{
"key": "google",
"label": "Google",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
}
],
"mediums": [
{
"key": "paid",
"label": "paid",
"opens": 2,
"unique_visitors": 2,
"play_store": 1,
"app_store": 0,
"landing_page": 1
},
{
"key": "social",
"label": "social",
"opens": 1,
"unique_visitors": 1,
"play_store": 1,
"app_store": 0,
"landing_page": 0
},
{
"key": "organic",
"label": "organic",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
},
{
"key": "offline",
"label": "offline",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 1,
"landing_page": 0
}
],
"campaigns": [
{
"key": "diwali_2026",
"label": "diwali_2026",
"opens": 2,
"unique_visitors": 2,
"play_store": 2,
"app_store": 0,
"landing_page": 0
},
{
"key": "poster_vijayawada",
"label": "poster_vijayawada",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 1,
"landing_page": 0
},
{
"key": "pharmacy_launch",
"label": "pharmacy_launch",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
},
{
"key": null,
"label": "Not tagged",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
}
],
"platforms": [
{
"key": "android",
"label": "Android",
"opens": 3,
"unique_visitors": 3,
"play_store": 2,
"app_store": 0,
"landing_page": 1
},
{
"key": "ios",
"label": "iPhone / iPad",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 1,
"landing_page": 0
},
{
"key": "desktop",
"label": "Desktop",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
}
],
"device_types": [
{
"key": "smartphone",
"label": "Smartphone",
"opens": 4,
"unique_visitors": 4,
"play_store": 2,
"app_store": 1,
"landing_page": 1
},
{
"key": "desktop",
"label": "Desktop",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
}
],
"device_brands": [
{
"key": "Realme",
"label": "Realme",
"opens": 2,
"unique_visitors": 2,
"play_store": 1,
"app_store": 0,
"landing_page": 1
},
{
"key": "Samsung",
"label": "Samsung",
"opens": 1,
"unique_visitors": 1,
"play_store": 1,
"app_store": 0,
"landing_page": 0
},
{
"key": "Apple",
"label": "Apple",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 1,
"landing_page": 0
}
],
"device_models": [
{
"key": "Realme · C55",
"label": "Realme C55",
"opens": 2,
"unique_visitors": 2,
"play_store": 1,
"app_store": 0,
"landing_page": 1
},
{
"key": null,
"label": "Unknown",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
},
{
"key": "Apple · iPhone",
"label": "Apple iPhone",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 1,
"landing_page": 0
},
{
"key": "Samsung · Galaxy A15",
"label": "Samsung Galaxy A15",
"opens": 1,
"unique_visitors": 1,
"play_store": 1,
"app_store": 0,
"landing_page": 0
}
],
"operating_systems": [
{
"key": "Android · 13",
"label": "Android 13",
"opens": 2,
"unique_visitors": 2,
"play_store": 1,
"app_store": 0,
"landing_page": 1
},
{
"key": "Android · 14",
"label": "Android 14",
"opens": 1,
"unique_visitors": 1,
"play_store": 1,
"app_store": 0,
"landing_page": 0
},
{
"key": "Windows · 10",
"label": "Windows 10",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
},
{
"key": "iOS · 17.4",
"label": "iOS 17.4",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 1,
"landing_page": 0
}
],
"browsers": [
{
"key": "Mobile Safari",
"label": "Mobile Safari",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 1,
"landing_page": 0
},
{
"key": "Chromium",
"label": "Chromium",
"opens": 1,
"unique_visitors": 1,
"play_store": 1,
"app_store": 0,
"landing_page": 0
},
{
"key": "Chrome Mobile",
"label": "Chrome Mobile",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
},
{
"key": "Chrome",
"label": "Chrome",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
}
],
"in_app_browsers": [
{
"key": "Instagram",
"label": "Instagram",
"opens": 1,
"unique_visitors": 1,
"play_store": 1,
"app_store": 0,
"landing_page": 0
}
],
"referrers": [
{
"key": "l.instagram.com",
"label": "l.instagram.com",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
},
{
"key": "www.google.com",
"label": "www.google.com",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
}
],
"countries": [
{
"key": "IN",
"label": "India",
"opens": 5,
"unique_visitors": 5,
"play_store": 2,
"app_store": 1,
"landing_page": 2
}
],
"regions": [
{
"key": "IN · Andhra Pradesh",
"label": "Andhra Pradesh, IN",
"opens": 3,
"unique_visitors": 3,
"play_store": 1,
"app_store": 1,
"landing_page": 1
},
{
"key": "IN · Karnataka",
"label": "Karnataka, IN",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
},
{
"key": "IN · Telangana",
"label": "Telangana, IN",
"opens": 1,
"unique_visitors": 1,
"play_store": 1,
"app_store": 0,
"landing_page": 0
}
],
"cities": [
{
"key": "IN · Andhra Pradesh · Vijayawada",
"label": "Vijayawada",
"region": "Andhra Pradesh",
"country_code": "IN",
"opens": 2,
"unique_visitors": 2,
"latitude": 16.5062,
"longitude": 80.648
},
{
"key": "IN · Karnataka · Bengaluru",
"label": "Bengaluru",
"region": "Karnataka",
"country_code": "IN",
"opens": 1,
"unique_visitors": 1,
"latitude": 12.9716,
"longitude": 77.5946
},
{
"key": "IN · Andhra Pradesh · Guntur",
"label": "Guntur",
"region": "Andhra Pradesh",
"country_code": "IN",
"opens": 1,
"unique_visitors": 1,
"latitude": 16.3067,
"longitude": 80.4365
},
{
"key": "IN · Telangana · Hyderabad",
"label": "Hyderabad",
"region": "Telangana",
"country_code": "IN",
"opens": 1,
"unique_visitors": 1,
"latitude": 17.385,
"longitude": 78.4867
}
],
"languages": [
{
"key": "te-IN",
"label": "te-IN",
"opens": 5,
"unique_visitors": 5,
"play_store": 2,
"app_store": 1,
"landing_page": 2
}
],
"page_languages": [
{
"key": "te",
"label": "తెలుగు",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
}
],
"calls_to_action": [
{
"key": "signup",
"label": "Start free / sign up",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
},
{
"key": "pricing",
"label": "Pricing",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
}
],
"screen_sizes": [
{
"key": "412 · 915",
"label": "412 × 915",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
},
{
"key": "1920 · 1080",
"label": "1920 × 1080",
"opens": 1,
"unique_visitors": 1,
"play_store": 0,
"app_store": 0,
"landing_page": 1
}
]
},
"link_previews": [
{
"label": "WhatsApp",
"fetches": 1
}
]
}
}
Example response (401):
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "This action is unauthorized."
}
Example response (422):
{
"message": "Choose a range of at most 366 days.",
"errors": {
"from": [
"Choose a range of at most 366 days."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List individual app link opens.
requires authentication
Newest first, including link previews (is_link_preview: true). No raw IP address, name,
phone number or email is stored or returned. Location is approximate and may be null.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/app-link/visits?from=2026-01-15&to=2026-01-15&source=whatsapp&campaign=diwali_2026&platform=android&tracked_page=app&per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/admin/app-link/visits"
);
const params = {
"from": "2026-01-15",
"to": "2026-01-15",
"source": "whatsapp",
"campaign": "diwali_2026",
"platform": "android",
"tracked_page": "app",
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/app-link/visits';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'from' => '2026-01-15',
'to' => '2026-01-15',
'source' => 'whatsapp',
'campaign' => 'diwali_2026',
'platform' => 'android',
'tracked_page' => 'app',
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 6,
"visited_at": "2026-09-26T11:20:00+05:30",
"page": "app",
"page_label": "App download link (/app)",
"page_locale": null,
"outcome": "landing_page",
"is_link_preview": true,
"preview_agent": "WhatsApp",
"is_new_visitor": true,
"source": "whatsapp",
"source_label": "WhatsApp",
"medium": "social",
"campaign": "diwali_2026",
"utm": {
"source": "whatsapp",
"medium": null,
"campaign": "diwali_2026",
"content": null,
"term": null
},
"ad_click_id": null,
"referrer_host": null,
"referrer_app": null,
"platform": "other",
"device": {
"type": null,
"brand": null,
"model": null
},
"os": {
"name": "Android",
"version": null
},
"browser": {
"name": "WhatsApp",
"version": "2.23",
"in_app": null
},
"location": {
"country_code": "IN",
"region": "Maharashtra",
"city": "Mumbai",
"postal_code": "400001",
"latitude": 19.076,
"longitude": 72.8777,
"timezone": "Asia/Kolkata"
},
"language": "te-IN",
"screen": null,
"engagement": {
"seconds": null,
"scroll_depth": null,
"call_to_action": null
}
},
{
"id": 5,
"visited_at": "2026-09-26T10:05:00+05:30",
"page": "pharmacy",
"page_label": "Pharmacies",
"page_locale": "te",
"outcome": "landing_page",
"is_link_preview": false,
"preview_agent": null,
"is_new_visitor": true,
"source": "instagram",
"source_label": "Instagram",
"medium": "paid",
"campaign": "pharmacy_launch",
"utm": {
"source": "instagram",
"medium": "paid",
"campaign": "pharmacy_launch",
"content": null,
"term": null
},
"ad_click_id": null,
"referrer_host": "l.instagram.com",
"referrer_app": null,
"platform": "android",
"device": {
"type": "smartphone",
"brand": "Realme",
"model": "C55"
},
"os": {
"name": "Android",
"version": "13"
},
"browser": {
"name": "Chrome Mobile",
"version": "124",
"in_app": null
},
"location": {
"country_code": "IN",
"region": "Andhra Pradesh",
"city": "Guntur",
"postal_code": "522001",
"latitude": 16.3067,
"longitude": 80.4365,
"timezone": "Asia/Kolkata"
},
"language": "te-IN",
"screen": {
"width": 412,
"height": 915
},
"engagement": {
"seconds": 126,
"scroll_depth": 71,
"call_to_action": "signup"
}
}
],
"meta": {
"current_page": 1,
"last_page": 3,
"per_page": 2,
"total": 6
}
}
Example response (401):
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "This action is unauthorized."
}
Example response (422):
{
"message": "The selected platform is invalid.",
"errors": {
"platform": [
"The selected platform is invalid."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Billing webhooks
Receive Razorpay subscription events
This public provider callback requires a valid X-Razorpay-Signature HMAC header and is idempotent.
Workers fetch current Razorpay state. Cancelled, completed, or expired subscriptions end paid access
and restore Free Essentials only when no newer subscription has replaced the ended subscription.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/webhooks/razorpay" \
--header "X-Razorpay-Signature: string required Razorpay webhook HMAC signature. Example: 0123456789abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/webhooks/razorpay"
);
const headers = {
"X-Razorpay-Signature": "string required Razorpay webhook HMAC signature. Example: 0123456789abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/webhooks/razorpay';
$response = $client->post(
$url,
[
'headers' => [
'X-Razorpay-Signature' => 'string required Razorpay webhook HMAC signature. Example: 0123456789abcdef',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"received": true
}
Example response (401):
{
"message": "Invalid webhook signature."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Business details
The card a shop shows a customer so they can pay it by direct bank transfer: the registered name and GSTIN, plus one bank account in full.
The full account number, IFSC and branch are stored encrypted on the payment account. The payment-accounts listing carries only the last four digits. Full details are available here and in receiving_bank_account on business resources for accounting members. This card endpoint is served no-store.
The shareable business and bank details.
requires authentication
With no payment_account_id, the account returned is the one the shop
already prints on its bills, else its default receiving bank, else any
active bank account it has. A workspace that has not saved a bank account
yet gets bank_account: null — show the "add bank details" path rather
than an empty card.
share_message is the same details as plain text, ready for WhatsApp or a
share sheet. Render the card from the fields; do not parse this string.
Not available offline: bank credentials are fetched fresh each time and must not be cached.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/settings/payment-details?payment_account_id=8" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"payment_account_id\": 16
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/settings/payment-details"
);
const params = {
"payment_account_id": "8",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"payment_account_id": 16
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/settings/payment-details';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'payment_account_id' => '8',
],
'json' => [
'payment_account_id' => 16,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"business": {
"id": 17,
"name": "Haniot",
"legal_name": "HANIOT PRIVATE LIMITED",
"gstin": "37AAFCH7061N1Z5",
"gst_registration_status": "active",
"is_gst_registered": true,
"phone": "9876543210",
"email": "[email protected]",
"logo_url": null,
"address": {
"line_1": "12 MG Road",
"line_2": null,
"city": "Visakhapatnam",
"state_code": "37",
"pincode": "530001"
}
},
"bank_account": {
"payment_account_id": 8,
"name": "ICICI Current",
"type": "bank",
"bank_name": "ICICI Bank",
"account_name": "Haniot Private Limited",
"account_number": "236001505028",
"account_last_four": "5028",
"ifsc": "ICIC0012360",
"branch": "Dwaraka Nagar",
"upi_id": null
},
"upi_id": "haniot@icici",
"share_message": "HANIOT PRIVATE LIMITED\nGSTIN: 37AAFCH7061N1Z5\n\nAccount name: Haniot Private Limited\nAccount number: 236001505028\nBank: ICICI Bank\nIFSC: ICIC0012360\nBranch: Dwaraka Nagar\nUPI: haniot@icici"
}
}
Example response (401):
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "This action is unauthorized."
}
Example response (422):
{
"message": "The selected payment account id is invalid.",
"errors": {
"payment_account_id": [
"The selected payment account id is invalid."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
business
object
legal_name
string
The registered name to print on the card. Falls back to the workspace name when no legal name is saved.
gstin
string|null
The primary GST registration's GSTIN, else the one saved on the workspace.
gst_registration_status
string
One of not_assessed, not_registered, pending, active, suspended, cancelled.
is_gst_registered
boolean
Whether to show the verified tick: an active registration with a GSTIN on record. It does not re-check the GST portal.
bank_account
object|null
The account to transfer into, or null when the workspace has saved none.
account_number
string|null
The full account number. Available here and in business receiving_bank_account for accounting members; fetch fresh rather than persisting it on the device.
upi_id
string|null
The account's UPI ID, else the workspace's.
share_message
string
The same details as plain text for a share sheet.
ChatGPT plugin
Inspect the connected plugin account and shop.
requires authentication
Requires a plugin OAuth token with dukanam:read; ordinary mobile tokens return 403.
Membership and plan/seat eligibility are rechecked on every request.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/plugin/session" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/plugin/session"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/plugin/session';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"user": {
"id": 1,
"name": "Priya"
},
"business": {
"id": 1,
"name": "Priya Store"
},
"scope": "dukanam:read",
"resource": "https://mcp.example.com/mcp",
"timezone": "Asia/Kolkata"
}
}
Example response (403):
{
"message": "A plugin OAuth token is required."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Invoice-linked expenses
Advanced Plan feature invoice_costs. Owner/admin writes; accountant reads. Every sales
or purchase cost is an operating expense, recorded once in Expenses with an immutable
invoice reference and description. Purchase costs do not increase inventory/COGS. Sales
costs reduce party/worker contribution. Unpaid costs accrue a payable; settlement never
creates a second expense. Costs remain after invoice returns/voids until explicitly voided.
List expenses on an invoice.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/invoices/1/expenses?per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"per_page\": 2
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/1/expenses"
);
const params = {
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"per_page": 2
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/1/expenses';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '20',
],
'json' => [
'per_page' => 2,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
invoice_reference
string
Saved sales number or supplier invoice reference (falling back to the purchase number).
paid_paise
integer
Amount actually paid, separate from recognized expense.
outstanding_paise
integer
Amount remaining payable; zero for a void expense.
cost_treatment
string
Always operating, on both sale and purchase invoices.
Record a sales or purchase invoice expense.
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/invoices/1/expenses" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"category\": \"Transport\",
\"payee\": \"Local transport\",
\"amount_paise\": 30000,
\"occurred_on\": \"2026-10-04\",
\"note\": \"Transport for this invoice\",
\"paid\": true,
\"payment_method\": \"bank\",
\"idempotency_key\": \"fdf5dfdf-aead-473b-8c98-dcfbbda6c02a\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/1/expenses"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"category": "Transport",
"payee": "Local transport",
"amount_paise": 30000,
"occurred_on": "2026-10-04",
"note": "Transport for this invoice",
"paid": true,
"payment_method": "bank",
"idempotency_key": "fdf5dfdf-aead-473b-8c98-dcfbbda6c02a"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/1/expenses';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'category' => 'Transport',
'payee' => 'Local transport',
'amount_paise' => 30000,
'occurred_on' => '2026-10-04',
'note' => 'Transport for this invoice',
'paid' => true,
'payment_method' => 'bank',
'idempotency_key' => 'fdf5dfdf-aead-473b-8c98-dcfbbda6c02a',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List expenses on an invoice.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/documents/architecto/expenses?per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"per_page\": 2
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/architecto/expenses"
);
const params = {
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"per_page": 2
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/architecto/expenses';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '20',
],
'json' => [
'per_page' => 2,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
invoice_reference
string
Saved sales number or supplier invoice reference (falling back to the purchase number).
paid_paise
integer
Amount actually paid, separate from recognized expense.
outstanding_paise
integer
Amount remaining payable; zero for a void expense.
cost_treatment
string
Always operating, on both sale and purchase invoices.
Record a sales or purchase invoice expense.
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/documents/architecto/expenses" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"category\": \"Transport\",
\"payee\": \"Local transport\",
\"amount_paise\": 30000,
\"occurred_on\": \"2026-10-04\",
\"note\": \"Transport for this invoice\",
\"paid\": true,
\"payment_method\": \"bank\",
\"idempotency_key\": \"fdf5dfdf-aead-473b-8c98-dcfbbda6c02a\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/documents/architecto/expenses"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"category": "Transport",
"payee": "Local transport",
"amount_paise": 30000,
"occurred_on": "2026-10-04",
"note": "Transport for this invoice",
"paid": true,
"payment_method": "bank",
"idempotency_key": "fdf5dfdf-aead-473b-8c98-dcfbbda6c02a"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/documents/architecto/expenses';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'category' => 'Transport',
'payee' => 'Local transport',
'amount_paise' => 30000,
'occurred_on' => '2026-10-04',
'note' => 'Transport for this invoice',
'paid' => true,
'payment_method' => 'bank',
'idempotency_key' => 'fdf5dfdf-aead-473b-8c98-dcfbbda6c02a',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Pay an outstanding invoice expense.
requires authentication
Recognized expense stays on its original date. Payment reduces the expense payable and cash/bank only. Partial payments are allowed; amount cannot exceed outstanding.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/expenses/architecto/payments" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"amount_paise\": 10000,
\"paid_on\": \"2026-10-04\",
\"payment_method\": \"bank\",
\"idempotency_key\": \"fdf5dfdf-aead-473b-8c98-dcfbbda6c02b\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/expenses/architecto/payments"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"amount_paise": 10000,
"paid_on": "2026-10-04",
"payment_method": "bank",
"idempotency_key": "fdf5dfdf-aead-473b-8c98-dcfbbda6c02b"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/expenses/architecto/payments';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'amount_paise' => 10000,
'paid_on' => '2026-10-04',
'payment_method' => 'bank',
'idempotency_key' => 'fdf5dfdf-aead-473b-8c98-dcfbbda6c02b',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Void an invoice expense payment.
requires authentication
Keeps the expense and restores its payable; reverses the bank/cash movement once.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/expenses/architecto/payments/architecto/void" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"reason\": \"Wrong transfer\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/expenses/architecto/payments/architecto/void"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"reason": "Wrong transfer"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/expenses/architecto/payments/architecto/void';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'reason' => 'Wrong transfer',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Invoice branding
How this workspace's bills look: one of three ready-made templates or a custom layout, any accent colour, a logo, a signature, closing lines, and an optional UPI QR.
Branding is frozen onto every invoice and business document at record time. Changing it here changes what future documents print as and leaves every saved document exactly as it was issued, which is what a reprint two years from now has to show.
Current invoice branding and the choices available.
requires authentication
templates, layout_choices and accents are the pickers — render them rather
than hard-coding values, because they can grow. accents is a set of suggested
swatches to offer beside a colour input, not the set of allowed values: any
#RRGGBB is accepted. invoice_accent_palette carries the colours that hex
actually prints as, so a client can show the result without redoing the maths.
Available offline: cached. Branding changes rarely and the counter needs it to print without signal.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/17/settings/invoice-branding" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/17/settings/invoice-branding"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/17/settings/invoice-branding';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"invoice_template": "classic",
"invoice_accent_colour": "#10505E",
"invoice_footer_note": "Goods once sold are not returnable.",
"bank_details": "HDFC Bank 5010 1234 5678, IFSC HDFC0000123",
"authorized_signatory": "Priya Sharma",
"show_upi_qr_on_invoice": false,
"show_logo_on_invoice": true,
"upi_id": "priyatextiles@hdfcbank",
"logo_url": "https://cdn.dukanam.com/business-logos/17/logo.png",
"signature_url": null,
"templates": [
{
"value": "classic",
"label": "Classic",
"summary": "Boxed party details under a ruled header. The layout Dukanam has always printed."
},
{
"value": "compact",
"label": "Compact",
"summary": "Tighter type and flat rows, so a long bill fits on fewer pages."
},
{
"value": "accent",
"label": "Accent",
"summary": "A filled colour band across the head, for a shop that wants its colour seen."
}
],
"accents": [
{
"value": "#10505E",
"label": "Dukanam teal"
}
],
"max_image_kilobytes": 1024
}
}
Example response (401):
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "This action is unauthorized."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
logo_url
string|null
Temporary signed object-storage URL when the logo is stored on S3. Refresh the resource after it expires.
signature_url
string|null
As logo_url, for the authorised signature image.
accents
object
value
string
Seven-character uppercase hex. A suggestion to offer as a swatch — any #RRGGBB is accepted on update.
invoice_accent_palette
object
What the chosen accent prints as: dark and soft companions, ink for text on the accent, and text for the accent used as text on white.
invoice_layout
object
The saved custom-template layout: header, density, logo.height, logo.align, table, and the columns switches.
layout_choices
object
The complete layout pickers, including the allowed logo_height range.
Update invoice branding.
requires authentication
Requires the accounting permission, the same as the rest of business
settings. Send only the fields you are changing. Saved documents are
untouched: each froze its own branding when it was raised.
Online only.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/businesses/17/settings/invoice-branding" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"invoice_template\": \"accent\",
\"invoice_accent_colour\": \"#047857\",
\"invoice_layout\": {
\"header\": \"band\",
\"density\": \"compact\",
\"table\": \"flat\",
\"logo\": {
\"height\": 64,
\"align\": \"center\"
},
\"columns\": {
\"hsn\": true,
\"uqc\": true,
\"discount\": true
}
},
\"invoice_footer_note\": \"Goods once sold are not returnable.\",
\"bank_details\": \"HDFC Bank 5010 1234 5678, IFSC HDFC0000123\",
\"authorized_signatory\": \"Priya Sharma\",
\"show_upi_qr_on_invoice\": true,
\"show_logo_on_invoice\": true
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/17/settings/invoice-branding"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"invoice_template": "accent",
"invoice_accent_colour": "#047857",
"invoice_layout": {
"header": "band",
"density": "compact",
"table": "flat",
"logo": {
"height": 64,
"align": "center"
},
"columns": {
"hsn": true,
"uqc": true,
"discount": true
}
},
"invoice_footer_note": "Goods once sold are not returnable.",
"bank_details": "HDFC Bank 5010 1234 5678, IFSC HDFC0000123",
"authorized_signatory": "Priya Sharma",
"show_upi_qr_on_invoice": true,
"show_logo_on_invoice": true
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/17/settings/invoice-branding';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'invoice_template' => 'accent',
'invoice_accent_colour' => '#047857',
'invoice_layout' => [
'header' => 'band',
'density' => 'compact',
'table' => 'flat',
'logo' => ['height' => 64, 'align' => 'center'],
'columns' => ['hsn' => true, 'uqc' => true, 'discount' => true],
],
'invoice_footer_note' => 'Goods once sold are not returnable.',
'bank_details' => 'HDFC Bank 5010 1234 5678, IFSC HDFC0000123',
'authorized_signatory' => 'Priya Sharma',
'show_upi_qr_on_invoice' => true,
'show_logo_on_invoice' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"invoice_template": "accent",
"invoice_accent_colour": "#047857",
"show_upi_qr_on_invoice": true
}
}
Example response (403):
{
"message": "Your role does not allow this action."
}
Example response (422):
{
"message": "Add a UPI ID in business settings before printing a UPI QR on invoices.",
"errors": {
"show_upi_qr_on_invoice": [
"Add a UPI ID in business settings before printing a UPI QR on invoices."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Preview a sample bill under branding that has not been saved.
requires authentication
Renders a plausible invoice — never created, never numbered, never stored —
so the settings screen can show the real document while the shop is still
choosing. Returns a PDF by default; pass as=html for a WebView.
Online only.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/17/settings/invoice-branding/preview?template=accent&accent=%23047857&format=80mm&as=html" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/17/settings/invoice-branding/preview"
);
const params = {
"template": "accent",
"accent": "#047857",
"format": "80mm",
"as": "html",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/17/settings/invoice-branding/preview';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'template' => 'accent',
'accent' => '#047857',
'format' => '80mm',
'as' => 'html',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
A PDF or HTML document, not JSON.
Example response (422):
{
"message": "The selected template is invalid.",
"errors": {
"template": [
"The selected template is invalid."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Upload the shop logo or the authorised signature.
requires authentication
Multipart. PNG or JPEG, up to 1 MB. Replaces whatever is stored under that name; the previous file is deleted once the new one is committed.
Online only.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/17/settings/invoice-branding/logo" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "image=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/businesses/17/settings/invoice-branding/logo"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('image', document.querySelector('input[name="image"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/17/settings/invoice-branding/logo';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'image',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"invoice_template": "classic",
"logo_url": "https://cdn.dukanam.com/business-logos/17/logo.png",
"signature_url": null
}
}
Example response (422):
{
"message": "The image must not be greater than 1024 kilobytes.",
"errors": {
"image": [
"The image must not be greater than 1024 kilobytes."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Remove the shop logo or the authorised signature.
requires authentication
Documents already saved keep the image they were issued with; only future documents print without it.
Online only.
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/17/settings/invoice-branding/signature" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/17/settings/invoice-branding/signature"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/17/settings/invoice-branding/signature';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"invoice_template": "classic",
"logo_url": null,
"signature_url": null
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Preview a real invoice under branding that has not been saved.
requires authentication
Renders a saved invoice with the requested template and accent so the shop can judge the choice against its own bill. Nothing is written: the invoice keeps the branding it was saved with, and printing it still produces that.
Online only.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/17/invoices/482/preview?template=compact&accent=%239A3412&format=80mm&as=html" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/17/invoices/482/preview"
);
const params = {
"template": "compact",
"accent": "#9A3412",
"format": "80mm",
"as": "html",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/17/invoices/482/preview';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'template' => 'compact',
'accent' => '#9A3412',
'format' => '80mm',
'as' => 'html',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
A PDF or HTML document, not JSON.
Example response (404):
{
"message": "Not Found"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Loans and EMI
Borrowings the shop carries — a vehicle loan, a working-capital loan, a credit card — and the repayments made against them. Each loan owns a liability account in the existing chart of accounts, so it appears on the balance sheet as a liability rather than a negative asset, and every EMI records a split journal: the principal reduces what is owed, the interest is an expense.
Plan feature loans_and_income; permission accounting. Online-only — nothing here belongs in
the offline cache or the write queue.
List loans and credit cards.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/loan-accounts?status=active&type=loan&per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"status\": \"active\",
\"type\": \"loan\",
\"per_page\": 1
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/loan-accounts"
);
const params = {
"status": "active",
"type": "loan",
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"status": "active",
"type": "loan",
"per_page": 1
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/loan-accounts';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'status' => 'active',
'type' => 'loan',
'per_page' => '20',
],
'json' => [
'status' => 'active',
'type' => 'loan',
'per_page' => 1,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
outstanding_paise
integer
What is still owed, read off the accounting entries.
next_installment
object|null
The next unpaid instalment, or null when nothing is scheduled.
summary
object
outstanding_paise
integer
Total still owed across every active loan.
due_this_month_paise
integer
Instalments falling due in the current calendar month.
Open a loan.
requires authentication
Naming disbursed_to_payment_account_id says the money actually landed in that account, so
the opening journal is a disbursement — the bank balance rises and the liability rises with
it. Leave it out for a loan taken before the shop kept books here: there is no receipt to
record, so the borrowing opens against equity, the way a party's opening balance does.
A loan with a tenure_months gets its amortisation worked out on the spot. A
credit_card never does — it revolves, and an invented schedule would be a fiction.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/loan-accounts" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"b\",
\"type\": \"loan\",
\"lender_name\": \"n\",
\"account_last_four\": \"gzmiyvdl\",
\"principal\": 19,
\"interest_rate\": 17,
\"emi_amount\": 5,
\"emi_day_of_month\": 1,
\"tenure_months\": 2,
\"started_on\": \"2026-01-15\",
\"first_emi_on\": \"2026-01-15\",
\"disbursed_to_payment_account_id\": 16,
\"notes\": \"n\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/loan-accounts"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "b",
"type": "loan",
"lender_name": "n",
"account_last_four": "gzmiyvdl",
"principal": 19,
"interest_rate": 17,
"emi_amount": 5,
"emi_day_of_month": 1,
"tenure_months": 2,
"started_on": "2026-01-15",
"first_emi_on": "2026-01-15",
"disbursed_to_payment_account_id": 16,
"notes": "n"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/loan-accounts';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'b',
'type' => 'loan',
'lender_name' => 'n',
'account_last_four' => 'gzmiyvdl',
'principal' => 19,
'interest_rate' => 17,
'emi_amount' => 5,
'emi_day_of_month' => 1,
'tenure_months' => 2,
'started_on' => '2026-01-15',
'first_emi_on' => '2026-01-15',
'disbursed_to_payment_account_id' => 16,
'notes' => 'n',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "This EMI does not cover the interest on the outstanding balance, so the loan would never be repaid. Raise the EMI or lower the interest rate.",
"errors": {
"emi_amount": [
"This EMI does not cover the interest on the outstanding balance, so the loan would never be repaid. Raise the EMI or lower the interest rate."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Show one loan with its schedule and repayment history.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/loan-accounts/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/loan-accounts/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/loan-accounts/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Correct a loan's description and terms.
requires authentication
This is a whole-record write: a field left out is cleared, exactly as contact updates behave. The borrowing itself is saved, so the principal and the start date are not editable here. Revising the terms rebuilds the instalments still to come; instalments already paid are left exactly as they were paid.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/businesses/1/loan-accounts/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"b\",
\"lender_name\": \"n\",
\"account_last_four\": \"gzmiyvdl\",
\"interest_rate\": 19,
\"emi_amount\": 17,
\"emi_day_of_month\": 1,
\"tenure_months\": 1,
\"first_emi_on\": \"2026-01-15\",
\"status\": \"active\",
\"notes\": \"h\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/loan-accounts/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "b",
"lender_name": "n",
"account_last_four": "gzmiyvdl",
"interest_rate": 19,
"emi_amount": 17,
"emi_day_of_month": 1,
"tenure_months": 1,
"first_emi_on": "2026-01-15",
"status": "active",
"notes": "h"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/loan-accounts/1';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'b',
'lender_name' => 'n',
'account_last_four' => 'gzmiyvdl',
'interest_rate' => 19,
'emi_amount' => 17,
'emi_day_of_month' => 1,
'tenure_months' => 1,
'first_emi_on' => '2026-01-15',
'status' => 'active',
'notes' => 'h',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
The instalments worked out for this loan.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/loan-accounts/1/schedule?status=pending" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"status\": \"pending\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/loan-accounts/1/schedule"
);
const params = {
"status": "pending",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"status": "pending"
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/loan-accounts/1/schedule';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'status' => 'pending',
],
'json' => [
'status' => 'pending',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
meta
object
pending_principal_paise
integer
Principal still to be repaid across the pending instalments.
pending_interest_paise
integer
Interest still to be paid across the pending instalments.
List repayments made against a loan.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/loan-accounts/1/payments?per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"per_page\": 1
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/loan-accounts/1/payments"
);
const params = {
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"per_page": 1
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/loan-accounts/1/payments';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '20',
],
'json' => [
'per_page' => 1,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Record a repayment.
requires authentication
Name loan_emi_schedule_id and the lender's own split for that instalment is used, which is
what a shop paying this month's EMI wants. State principal and interest instead for a
part payment, a prepayment, or a credit-card payment with no schedule behind it. Sending
amount as well is a check, not an override: parts that do not add up are refused.
The journal is split — principal off the liability, interest to expense, any lender charges to operating expenses — because a lump sum booked as one expense misstates both the balance sheet and the profit and loss.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/loan-accounts/1/payments" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"loan_emi_schedule_id\": 16,
\"paid_on\": \"2026-01-15\",
\"amount\": 22,
\"principal\": 7,
\"interest\": 16,
\"charges\": 17,
\"payment_method\": \"cash\",
\"payment_account_id\": 16,
\"reference\": \"n\",
\"note\": \"g\",
\"idempotency_key\": \"6d61f406-f07d-482d-a284-3e06edfd7f55\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/loan-accounts/1/payments"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"loan_emi_schedule_id": 16,
"paid_on": "2026-01-15",
"amount": 22,
"principal": 7,
"interest": 16,
"charges": 17,
"payment_method": "cash",
"payment_account_id": 16,
"reference": "n",
"note": "g",
"idempotency_key": "6d61f406-f07d-482d-a284-3e06edfd7f55"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/loan-accounts/1/payments';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'loan_emi_schedule_id' => 16,
'paid_on' => '2026-01-15',
'amount' => 22,
'principal' => 7,
'interest' => 16,
'charges' => 17,
'payment_method' => 'cash',
'payment_account_id' => 16,
'reference' => 'n',
'note' => 'g',
'idempotency_key' => '6d61f406-f07d-482d-a284-3e06edfd7f55',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "Only ₹12,000.00 of principal is still outstanding on Vehicle loan. Reduce the principal portion, or record the excess as interest or charges.",
"errors": {
"principal": [
"Only ₹12,000.00 of principal is still outstanding on Vehicle loan. Reduce the principal portion, or record the excess as interest or charges."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Void a repayment.
requires authentication
Saved transactions keep their history. The journal is reversed, the instalment goes back to pending, and the reason stays in the audit trail.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/loan-accounts/1/payments/1/void" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"reason\": \"b\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/loan-accounts/1/payments/1/void"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"reason": "b"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/loan-accounts/1/payments/1/void';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'reason' => 'b',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List other-income receipts.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/other-incomes?q=rent&category=Rent+received&payment_account_id=9&from=2026-08-01&to=2026-08-31&status=active&per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"q\": \"b\",
\"category\": \"n\",
\"payment_account_id\": 16,
\"from\": \"2026-01-15\",
\"to\": \"2026-01-15\",
\"status\": \"active\",
\"per_page\": 22
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/other-incomes"
);
const params = {
"q": "rent",
"category": "Rent received",
"payment_account_id": "9",
"from": "2026-08-01",
"to": "2026-08-31",
"status": "active",
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"q": "b",
"category": "n",
"payment_account_id": 16,
"from": "2026-01-15",
"to": "2026-01-15",
"status": "active",
"per_page": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/other-incomes';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'q' => 'rent',
'category' => 'Rent received',
'payment_account_id' => '9',
'from' => '2026-08-01',
'to' => '2026-08-31',
'status' => 'active',
'per_page' => '20',
],
'json' => [
'q' => 'b',
'category' => 'n',
'payment_account_id' => 16,
'from' => '2026-01-15',
'to' => '2026-01-15',
'status' => 'active',
'per_page' => 22,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
summary
object
filtered_total_paise
integer
Total of non-voided receipts matching the current filters.
month_total_paise
integer
Total of all non-voided receipts in the current calendar month, including its first and last day.
categories
string[]
Categories this workspace has used, for the category picker.
Record a receipt that is not a sale.
requires authentication
The receipt, its journal, the cash-drawer movement, and the audit record either all commit or all roll back.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/other-incomes" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"category\": \"b\",
\"payer\": \"n\",
\"amount\": 7,
\"occurred_on\": \"2026-01-15\",
\"payment_method\": \"cash\",
\"payment_account_id\": 16,
\"note\": \"n\",
\"idempotency_key\": \"6d61f406-f07d-482d-a284-3e06edfd7f55\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/other-incomes"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"category": "b",
"payer": "n",
"amount": 7,
"occurred_on": "2026-01-15",
"payment_method": "cash",
"payment_account_id": 16,
"note": "n",
"idempotency_key": "6d61f406-f07d-482d-a284-3e06edfd7f55"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/other-incomes';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'category' => 'b',
'payer' => 'n',
'amount' => 7,
'occurred_on' => '2026-01-15',
'payment_method' => 'cash',
'payment_account_id' => 16,
'note' => 'n',
'idempotency_key' => '6d61f406-f07d-482d-a284-3e06edfd7f55',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
GET api/v1/businesses/{business}/other-incomes/{otherIncome_id}
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/other-incomes/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/other-incomes/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/other-incomes/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Void a receipt.
requires authentication
The void marker, the journal reversal, the cash-drawer reversal, and the audit record either all commit or all roll back.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/other-incomes/1/void" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"reason\": \"b\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/other-incomes/1/void"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"reason": "b"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/other-incomes/1/void';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'reason' => 'b',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Lookups
Search tenant-scoped values for lookup controls. Results are limited to the active business and the caller's workspace permissions and plan features.
Search lookup values.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/lookups?source=customers&q=priya&method=upi&direction=received&contact=42&item=12&supplier=17&warehouse=1&ids[]=12&ids[]=18&category_id=1&subcategory_id=2&brand_id=3&manufacturer_id=4" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/lookups"
);
const params = {
"source": "customers",
"q": "priya",
"method": "upi",
"direction": "received",
"contact": "42",
"item": "12",
"supplier": "17",
"warehouse": "1",
"ids[0]": "12",
"ids[1]": "18",
"category_id": "1",
"subcategory_id": "2",
"brand_id": "3",
"manufacturer_id": "4",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/lookups';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'source' => 'customers',
'q' => 'priya',
'method' => 'upi',
'direction' => 'received',
'contact' => '42',
'item' => '12',
'supplier' => '17',
'warehouse' => '1',
'ids[0]' => '12',
'ids[1]' => '18',
'category_id' => '1',
'subcategory_id' => '2',
'brand_id' => '3',
'manufacturer_id' => '4',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"value": "42",
"label": "Priya Sharma",
"meta": "Customer · 9876543210",
"attributes": {
"type": "customer",
"phone": "9876543210"
}
}
],
"meta": {
"source": "customers",
"query": "priya",
"has_more": false
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
meta
object
item_filters
object
Item searches include tenant-owned category, subcategory (with parent_id), brand and manufacturer filter choices.
source
string
Lookup source used for the response.
query
string
Normalized search query.
has_more
boolean
Whether more matching values exist beyond this response.
data
object
attributes
object
Source-specific form attributes. Contact results can include type, name, phone, gstin, state_code (billing GST state code), and gst_treatment. Item results include name, price, list_price, price_source, tax, cess, hsn_sac (when the item has one), and stock, plus price_list and price_breaks when a rate card applies; price is the rate for the contact named by contact, falling back to the item master, and purchase_price is present only for callers with purchases, inventory, or accounting permission. Payment-account results include type, methods, default, upi, and open. Outstanding-document results include balance, contact, and contact_label.
contact, falling back to the item master, and purchase_price is present only for callers with purchases, inventory, or accounting permission. Payment-account results include type, methods, default, upi, and open. Outstanding-document results include balance, contact, and contact_label.category_id
integer|null
Item classification ID; subcategory_id, brand_id and manufacturer_id and their *_name fields are also included when assigned.
price_source
string
Where the item rate came from: item, price_list, or party_discount.
supplier_price
number|null
Purchase rate to open a purchase line at for the supplier named by supplier. Absent when no agreed or last-billed rate is known.
supplier_price_source
string|null
agreed or last_purchase.
supplier_name
string|null
Supplier the rate belongs to; with supplier=preferred, the item's preferred supplier.
track_batches
string
1 when the item is batch tracked, in which case a document line must name the lot it leaves from.
batch
string|null
The lot to pre-select for a batch-tracked item: earliest expiry with stock, not expired. Absent when no sellable lot exists.
batch_label
string|null
Display label for that lot, such as B-2291 · exp Mar 2027.
track_serials
string
1 when the item is serial tracked, in which case a document line must name one unit per quantity billed.
serials
string|null
Units on the shelf, oldest first, separated by |, up to fifty. Absent when the item holds none.
price_breaks
string|null
Quantity breaks as minimum:paise pairs separated by |, such as 25:44590|50:43000.
value
string
Stable form value for the result.
label
string
Human-readable result label.
meta
string|null
Optional secondary result description.
photo
string|null
Item results only: the first item photo, so a search result can be recognised by the pack rather than the name. Null when the item has none. Item lookup tax/cess rates are zero for businesses that cannot charge GST; HSN/SAC is omitted for unregistered businesses.
Master partner teams
Authenticated partner-portal session cookies are required (not a workspace bearer token). Sign in at /partners/login with WhatsApp OTP. Mutating requests also require the session CSRF token in X-CSRF-TOKEN or X-XSRF-TOKEN. Only active masters may use these routes. Team data is scoped to that master; foreign IDs return 404. Amounts are integer paise in responses; reward requests use rupees and percent.
Get my team performance.
requires authentication
Aggregate funnel, per-member clicks/sign-ups/paid accounts, platform earnings, team rewards and retained amount. No referred-shop identity or contact details.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/partners/team?period=30d" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}"const url = new URL(
"https://dukanam.com/api/v1/partners/team"
);
const params = {
"period": "30d",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/team';
$response = $client->get(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
],
'query' => [
'period' => '30d',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"member_count": 1,
"funnel": {
"clicks": 12,
"signups": 2,
"paid": 1
},
"platform_earnings": {
"lifetime_paise": 39900
},
"team_rewards": {
"lifetime_paise": 20000
},
"retained_paise": 19900,
"members": [
{
"id": 8,
"name": "Anita Rao",
"status": "active",
"team_role": "associate",
"phone": "+919123456789",
"email": null,
"funnel": {
"clicks": 12,
"signups": 2,
"paid": 1
},
"earnings": {
"lifetime_paise": 20000
}
}
]
}
}
Example response (403):
{
"message": "Forbidden"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create my team member.
requires authentication
Creates a new account belonging only to this master, with WhatsApp OTP login. It does not send an invitation message; share /partners/login with the person. Existing partner numbers cannot be claimed. Members cannot create their own teams.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/partners/team/members" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}" \
--header "X-CSRF-TOKEN: {SESSION_CSRF_TOKEN}" \
--data "{
\"name\": \"Anita Rao\",
\"phone\": \"9123456789\",
\"email\": \"[email protected]\",
\"status\": \"active\",
\"team_role\": \"associate\"
}"
const url = new URL(
"https://dukanam.com/api/v1/partners/team/members"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
"X-CSRF-TOKEN": "{SESSION_CSRF_TOKEN}",
};
let body = {
"name": "Anita Rao",
"phone": "9123456789",
"email": "[email protected]",
"status": "active",
"team_role": "associate"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/team/members';
$response = $client->post(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
'X-CSRF-TOKEN' => '{SESSION_CSRF_TOKEN}',
],
'json' => [
'name' => 'Anita Rao',
'phone' => '9123456789',
'email' => '[email protected]',
'status' => 'active',
'team_role' => 'associate',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 8,
"name": "Anita Rao",
"phone": "+919123456789",
"email": null,
"status": "active",
"account_type": "member",
"team_role": "associate",
"master_partner_id": 3
}
}
Example response (422):
{
"message": "Another partner already uses this number.",
"errors": {
"phone": [
"Another partner already uses this number."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get my team member and link performance.
requires authentication
Includes eligible programs and reward balances, never platform admin notes or PAN.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/partners/team/members/1" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}"const url = new URL(
"https://dukanam.com/api/v1/partners/team/members/1"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/team/members/1';
$response = $client->get(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"member": {
"id": 8,
"name": "Anita Rao",
"master_partner_id": 3
},
"funnel": {
"clicks": 12,
"signups": 2,
"paid": 1
},
"earnings": {
"lifetime_paise": 20000
},
"available_programs": [],
"links": [
{
"id": 4,
"code": "ANITA4K",
"program_id": 2,
"label": "WhatsApp",
"url": "https://dukanam.com/p/ANITA4K",
"destination": "register",
"is_active": true,
"stats": {
"clicks": 12,
"signups": 2,
"paid": 1
}
}
]
}
}
Example response (404):
{
"message": "Not found."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update my team member.
requires authentication
Only profile, phone, status and team_role may change. Parent, account type and platform commission fields are prohibited. Suspending stops login and new attribution.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/partners/team/members/1" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}" \
--header "X-CSRF-TOKEN: {SESSION_CSRF_TOKEN}" \
--data "{
\"name\": \"Anita Rao\",
\"phone\": \"9123456789\",
\"email\": \"[email protected]\",
\"status\": \"suspended\",
\"team_role\": \"employee\"
}"
const url = new URL(
"https://dukanam.com/api/v1/partners/team/members/1"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
"X-CSRF-TOKEN": "{SESSION_CSRF_TOKEN}",
};
let body = {
"name": "Anita Rao",
"phone": "9123456789",
"email": "[email protected]",
"status": "suspended",
"team_role": "employee"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/team/members/1';
$response = $client->put(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
'X-CSRF-TOKEN' => '{SESSION_CSRF_TOKEN}',
],
'json' => [
'name' => 'Anita Rao',
'phone' => '9123456789',
'email' => '[email protected]',
'status' => 'suspended',
'team_role' => 'employee',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 8,
"name": "Anita Rao",
"phone": "+919123456789",
"email": null,
"status": "suspended",
"account_type": "member",
"team_role": "employee",
"master_partner_id": 3
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create a link for my team member.
requires authentication
A team program eligible for this member is required. Default platform programs and other masters' programs cannot be used. Shared link limit is 25 per member.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/partners/team/members/1/links" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}" \
--header "X-CSRF-TOKEN: {SESSION_CSRF_TOKEN}" \
--data "{
\"program_id\": 2,
\"label\": \"WhatsApp\",
\"code\": \"ANITA4K\",
\"destination\": \"register\"
}"
const url = new URL(
"https://dukanam.com/api/v1/partners/team/members/1/links"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
"X-CSRF-TOKEN": "{SESSION_CSRF_TOKEN}",
};
let body = {
"program_id": 2,
"label": "WhatsApp",
"code": "ANITA4K",
"destination": "register"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/team/members/1/links';
$response = $client->post(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
'X-CSRF-TOKEN' => '{SESSION_CSRF_TOKEN}',
],
'json' => [
'program_id' => 2,
'label' => 'WhatsApp',
'code' => 'ANITA4K',
'destination' => 'register',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 4,
"code": "ANITA4K",
"program_id": 2,
"label": "WhatsApp",
"url": "https://dukanam.com/p/ANITA4K",
"destination": "register",
"is_active": true,
"created_at": "2026-10-06T12:00:00Z"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Pause or resume my member's link.
requires authentication
Both member and link must belong to this master's team. Paused links stop new attribution.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/partners/team/members/1/links/1" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}" \
--header "X-CSRF-TOKEN: {SESSION_CSRF_TOKEN}" \
--data "{
\"is_active\": false
}"
const url = new URL(
"https://dukanam.com/api/v1/partners/team/members/1/links/1"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
"X-CSRF-TOKEN": "{SESSION_CSRF_TOKEN}",
};
let body = {
"is_active": false
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/team/members/1/links/1';
$response = $client->patch(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
'X-CSRF-TOKEN' => '{SESSION_CSRF_TOKEN}',
],
'json' => [
'is_active' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 4,
"program_id": 2,
"code": "ANITA4K",
"is_active": false
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List my team programs.
requires authentication
All/selected audiences apply only within this master's team. Includes paused and scheduled programs.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/partners/team/programs" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}"const url = new URL(
"https://dukanam.com/api/v1/partners/team/programs"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/team/programs';
$response = $client->get(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 2,
"owner_master_partner_id": 3,
"name": "Shop referrals",
"audience": "all",
"partner_ids": [],
"is_active": true,
"commission": {
"type": "flat",
"flat_amount_paise": 20000,
"percent_basis_points": null,
"custom": false,
"description": "₹200.00 per paid account"
},
"hold_days": 7
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create my team program.
requires authentication
Fixed rewards must be below the master's fixed platform rate. Percent rewards use the master's platform commission, not the shop payment, up to 99.99%. A rounding safeguard retains at least one paise for the master per conversion. Team hold is the longer of program and master platform holds. Terms are saved at sign-up.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/partners/team/programs" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}" \
--header "X-CSRF-TOKEN: {SESSION_CSRF_TOKEN}" \
--data "{
\"name\": \"Shop referrals\",
\"description\": \"Refer shops and earn rewards.\",
\"is_active\": true,
\"audience\": \"all\",
\"partner_ids\": [
8
],
\"starts_at\": null,
\"ends_at\": null,
\"commission_type\": \"flat\",
\"flat_amount\": 200,
\"percent\": 50,
\"hold_days\": 7
}"
const url = new URL(
"https://dukanam.com/api/v1/partners/team/programs"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
"X-CSRF-TOKEN": "{SESSION_CSRF_TOKEN}",
};
let body = {
"name": "Shop referrals",
"description": "Refer shops and earn rewards.",
"is_active": true,
"audience": "all",
"partner_ids": [
8
],
"starts_at": null,
"ends_at": null,
"commission_type": "flat",
"flat_amount": 200,
"percent": 50,
"hold_days": 7
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/team/programs';
$response = $client->post(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
'X-CSRF-TOKEN' => '{SESSION_CSRF_TOKEN}',
],
'json' => [
'name' => 'Shop referrals',
'description' => 'Refer shops and earn rewards.',
'is_active' => true,
'audience' => 'all',
'partner_ids' => [8],
'starts_at' => null,
'ends_at' => null,
'commission_type' => 'flat',
'flat_amount' => 200.0,
'percent' => 50.0,
'hold_days' => 7,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 2,
"owner_master_partner_id": 3,
"name": "Shop referrals",
"audience": "all",
"partner_ids": [],
"is_active": true,
"commission": {
"type": "flat",
"flat_amount_paise": 20000,
"percent_basis_points": null,
"custom": false,
"description": "₹200.00 per paid account"
},
"hold_days": 7
}
}
Example response (422):
{
"message": "Fixed team rewards must be lower than your fixed platform commission.",
"errors": {
"flat_amount": [
"Fixed team rewards must be lower than your fixed platform commission."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Replace my team program.
requires authentication
Uses the same full configuration and validation as creation. Prior referrals retain saved master and member terms. Omitted optional timestamps clear the window.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/partners/team/programs/1" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}" \
--header "X-CSRF-TOKEN: {SESSION_CSRF_TOKEN}" \
--data "{
\"name\": \"b\",
\"description\": \"Et animi quos velit et fugiat.\",
\"is_active\": false,
\"audience\": \"all\",
\"partner_ids\": [
16
],
\"starts_at\": \"2026-01-15\",
\"ends_at\": \"2026-01-15\",
\"commission_type\": \"flat\",
\"flat_amount\": 22,
\"percent\": 7,
\"hold_days\": 16
}"
const url = new URL(
"https://dukanam.com/api/v1/partners/team/programs/1"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
"X-CSRF-TOKEN": "{SESSION_CSRF_TOKEN}",
};
let body = {
"name": "b",
"description": "Et animi quos velit et fugiat.",
"is_active": false,
"audience": "all",
"partner_ids": [
16
],
"starts_at": "2026-01-15",
"ends_at": "2026-01-15",
"commission_type": "flat",
"flat_amount": 22,
"percent": 7,
"hold_days": 16
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/team/programs/1';
$response = $client->put(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
'X-CSRF-TOKEN' => '{SESSION_CSRF_TOKEN}',
],
'json' => [
'name' => 'b',
'description' => 'Et animi quos velit et fugiat.',
'is_active' => false,
'audience' => 'all',
'partner_ids' => [16],
'starts_at' => '2026-01-15',
'ends_at' => '2026-01-15',
'commission_type' => 'flat',
'flat_amount' => 22,
'percent' => 7,
'hold_days' => 16,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 2,
"owner_master_partner_id": 3,
"name": "Shop referrals",
"is_active": false,
"audience": "all",
"partner_ids": [],
"commission": {
"type": "flat",
"flat_amount_paise": 20000,
"percent_basis_points": null,
"custom": false,
"description": "₹200.00 per paid account"
},
"hold_days": 7
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List my team settlements.
requires authentication
Processing, paid and cancelled manual transfers, separate from Dukanam payouts.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/partners/team/settlements" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}"const url = new URL(
"https://dukanam.com/api/v1/partners/team/settlements"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/team/settlements';
$response = $client->get(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 1,
"master_partner_id": 3,
"partner_id": 8,
"status": "processing",
"gross_paise": 20000,
"commissions_count": 1,
"account_label": "UPI · an••@okhdfcbank",
"reference": null,
"notes": null,
"created_by_partner_id": 3,
"paid_by_partner_id": null,
"created_at": "2026-10-06T12:00:00Z",
"paid_at": null,
"cancelled_at": null
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get my settlement's payment details.
requires authentication
Full saved bank/UPI destination, only for the owning master, to make the external transfer. List responses are masked. Later member edits never change this destination.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/partners/team/settlements/1" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}"const url = new URL(
"https://dukanam.com/api/v1/partners/team/settlements/1"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/team/settlements/1';
$response = $client->get(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 1,
"status": "processing",
"gross_paise": 20000,
"account": {
"method": "upi",
"account_holder_name": "Anita Rao",
"upi_id": "anita@okhdfcbank"
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Prepare my member's settlement.
requires authentication
Reserves all approved unsettled rewards for this member. Requires completed profile and primary payout account; keeps an encrypted destination snapshot. Duplicate preparation without new rewards returns 422. Does not transfer money or reserve any Dukanam payout. Total is the full reward amount with no automatic withholding.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/partners/team/members/1/settlements" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}" \
--header "X-CSRF-TOKEN: {SESSION_CSRF_TOKEN}"const url = new URL(
"https://dukanam.com/api/v1/partners/team/members/1/settlements"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
"X-CSRF-TOKEN": "{SESSION_CSRF_TOKEN}",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/team/members/1/settlements';
$response = $client->post(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
'X-CSRF-TOKEN' => '{SESSION_CSRF_TOKEN}',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 1,
"master_partner_id": 3,
"partner_id": 8,
"status": "processing",
"gross_paise": 20000,
"commissions_count": 1,
"account_label": "UPI · an••@okhdfcbank",
"reference": null,
"notes": null,
"created_by_partner_id": 3,
"paid_by_partner_id": null,
"created_at": "2026-10-06T12:00:00Z",
"paid_at": null,
"cancelled_at": null
}
}
Example response (422):
{
"message": "No eligible rewards or missing payment details.",
"errors": {
"settlement": [
"No eligible rewards or missing payment details."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Mark my settlement paid.
requires authentication
Only processing records. Stores reference, date and acting master; marks reserved member rewards paid. Repeated payment returns 422. Transfer happens outside Dukanam.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/partners/team/settlements/1/mark-paid" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}" \
--header "X-CSRF-TOKEN: {SESSION_CSRF_TOKEN}" \
--data "{
\"reference\": \"UTR123456\",
\"paid_at\": null,
\"notes\": \"Paid by UPI\"
}"
const url = new URL(
"https://dukanam.com/api/v1/partners/team/settlements/1/mark-paid"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
"X-CSRF-TOKEN": "{SESSION_CSRF_TOKEN}",
};
let body = {
"reference": "UTR123456",
"paid_at": null,
"notes": "Paid by UPI"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/team/settlements/1/mark-paid';
$response = $client->post(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
'X-CSRF-TOKEN' => '{SESSION_CSRF_TOKEN}',
],
'json' => [
'reference' => 'UTR123456',
'paid_at' => null,
'notes' => 'Paid by UPI',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 1,
"master_partner_id": 3,
"partner_id": 8,
"status": "paid",
"gross_paise": 20000,
"reference": "UTR123456",
"paid_by_partner_id": 3,
"paid_at": "2026-10-06T12:00:00Z"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Cancel my processing settlement.
requires authentication
Frees its rewards for a future settlement. A paid record cannot be cancelled.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/partners/team/settlements/1/cancel" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}" \
--header "X-CSRF-TOKEN: {SESSION_CSRF_TOKEN}" \
--data "{
\"notes\": \"Wrong account; prepare again\"
}"
const url = new URL(
"https://dukanam.com/api/v1/partners/team/settlements/1/cancel"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
"X-CSRF-TOKEN": "{SESSION_CSRF_TOKEN}",
};
let body = {
"notes": "Wrong account; prepare again"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/team/settlements/1/cancel';
$response = $client->post(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
'X-CSRF-TOKEN' => '{SESSION_CSRF_TOKEN}',
],
'json' => [
'notes' => 'Wrong account; prepare again',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 1,
"master_partner_id": 3,
"partner_id": 8,
"status": "cancelled",
"gross_paise": 20000,
"cancelled_at": "2026-10-06T12:00:00Z"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Partner portal
Own-account access for individual partners, masters and team members. Requires a partner session from /partners/login (WhatsApp OTP), not a workspace bearer token. Writes require the session CSRF token. Responses are private and not cached. Team members see their master's eligible programs and their own rewards/payments.
Get my partner dashboard.
requires authentication
Shop identities and internal notes are excluded. Earnings are lifetime; funnel uses the requested period. Team members never see other members' balances.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/partners/me?period=30d" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}"const url = new URL(
"https://dukanam.com/api/v1/partners/me"
);
const params = {
"period": "30d",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/me';
$response = $client->get(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
],
'query' => [
'period' => '30d',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"partner": {
"id": 8,
"name": "Anita Rao",
"account_type": "member",
"team_role": "associate",
"master_partner_id": 3
},
"available_programs": [
{
"id": 2,
"name": "Shop referrals",
"commission": {
"type": "flat",
"flat_amount_paise": 20000
}
}
],
"funnel": {
"clicks": 12,
"signups": 2,
"paid": 1
},
"earnings": {
"on_hold_paise": 0,
"available_paise": 20000,
"processing_paise": 0,
"paid_paise": 0,
"lifetime_paise": 20000
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List my links and performance.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/partners/me/links" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}"const url = new URL(
"https://dukanam.com/api/v1/partners/me/links"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/me/links';
$response = $client->get(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 4,
"program_id": 2,
"code": "ANITA4K",
"label": "WhatsApp",
"url": "https://dukanam.com/p/ANITA4K",
"destination": "register",
"is_active": true,
"stats": {
"clicks": 12,
"signups": 2,
"paid": 1
},
"share": {
"code": "ANITA4K",
"link": "https://dukanam.com/p/ANITA4K",
"message": "Try Dukanam",
"whatsapp_url": "https://wa.me/?text=Try%20Dukanam"
}
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create my program link.
requires authentication
Team members must choose an active eligible team program. Individual/master partners may omit program_id for their default platform deal. Maximum 25 links.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/partners/me/links" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}" \
--header "X-CSRF-TOKEN: {SESSION_CSRF_TOKEN}" \
--data "{
\"program_id\": 2,
\"label\": \"WhatsApp\",
\"code\": \"ANITA4K\",
\"destination\": \"register\"
}"
const url = new URL(
"https://dukanam.com/api/v1/partners/me/links"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
"X-CSRF-TOKEN": "{SESSION_CSRF_TOKEN}",
};
let body = {
"program_id": 2,
"label": "WhatsApp",
"code": "ANITA4K",
"destination": "register"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/me/links';
$response = $client->post(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
'X-CSRF-TOKEN' => '{SESSION_CSRF_TOKEN}',
],
'json' => [
'program_id' => 2,
'label' => 'WhatsApp',
'code' => 'ANITA4K',
'destination' => 'register',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 4,
"program_id": 2,
"code": "ANITA4K",
"label": "WhatsApp",
"url": "https://dukanam.com/p/ANITA4K",
"destination": "register",
"is_active": true
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update my link.
requires authentication
Only your links; code, program and ownership cannot be changed. A paused link still redirects, but stops attribution of new clicks and sign-ups.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/partners/me/links/1" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}" \
--header "X-CSRF-TOKEN: {SESSION_CSRF_TOKEN}" \
--data "{
\"label\": \"Instagram\",
\"destination\": \"register\",
\"is_active\": false
}"
const url = new URL(
"https://dukanam.com/api/v1/partners/me/links/1"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
"X-CSRF-TOKEN": "{SESSION_CSRF_TOKEN}",
};
let body = {
"label": "Instagram",
"destination": "register",
"is_active": false
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/me/links/1';
$response = $client->patch(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
'X-CSRF-TOKEN' => '{SESSION_CSRF_TOKEN}',
],
'json' => [
'label' => 'Instagram',
'destination' => 'register',
'is_active' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 4,
"program_id": 2,
"code": "ANITA4K",
"label": "Instagram",
"destination": "register",
"is_active": false
}
}
Example response (404):
{
"message": "Not found."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List my earnings history.
requires authentication
Paginated commissions, using member rewards for a team account. Excludes shop identities and the master's gross platform commission. All amounts are paise. partner_referral_id is null if the referred account has been deleted.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/partners/me/commissions" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}"const url = new URL(
"https://dukanam.com/api/v1/partners/me/commissions"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/me/commissions';
$response = $client->get(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 1,
"partner_referral_id": 9,
"amount_paise": 20000,
"status": "approved",
"program_name": "Shop referrals",
"earned_at": "2026-10-06T12:00:00Z",
"hold_until": "2026-10-06T12:00:00Z",
"paid_at": null,
"rejection_reason": null
}
],
"current_page": 1,
"per_page": 25,
"total": 1
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List my payments.
requires authentication
Team accounts receive master-funded manual settlement records; other partners receive platform payout records. Saved payment destinations are masked.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/partners/me/payments" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Cookie: dukanam-session={PARTNER_SESSION}"const url = new URL(
"https://dukanam.com/api/v1/partners/me/payments"
);
const headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": "dukanam-session={PARTNER_SESSION}",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/partners/me/payments';
$response = $client->get(
$url,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Cookie' => 'dukanam-session={PARTNER_SESSION}',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 1,
"master_partner_id": 3,
"partner_id": 8,
"status": "paid",
"gross_paise": 20000,
"account_label": "UPI · an••@ybl",
"reference": "UTR123456",
"paid_at": "2026-10-06T12:00:00Z"
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Partner program administration
Requires a super-admin token. Named programs supplement the existing default shop referral program; global enablement, payout minimum and TDS still apply.
List named partner programs.
requires authentication
Includes platform-owned drafts, paused, future and expired programs for administration. Master-owned programs are managed only through the owning master’s team API. The default program is managed by /admin/partner-program and has no database ID.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/partner-programs?per_page=25" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"per_page\": 1
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partner-programs"
);
const params = {
"per_page": "25",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"per_page": 1
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partner-programs';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
],
'json' => [
'per_page' => 1,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 1,
"owner_master_partner_id": null,
"name": "Creator campaign",
"description": "Help shops discover Dukanam.",
"is_active": true,
"audience": "selected",
"partner_ids": [
3
],
"starts_at": null,
"ends_at": null,
"commission": {
"type": "percent",
"flat_amount_paise": null,
"percent_basis_points": 2500,
"custom": false,
"description": "25% of the first payment"
},
"hold_days": 15,
"created_at": "2026-10-06T12:00:00.000000Z"
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create a named partner program.
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/admin/partner-programs" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Creator campaign\",
\"description\": \"Help shops discover Dukanam.\",
\"is_active\": true,
\"audience\": \"selected\",
\"partner_ids\": [
3
],
\"starts_at\": null,
\"ends_at\": null,
\"commission_type\": \"percent\",
\"flat_amount\": 250,
\"percent\": 25,
\"hold_days\": 15
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partner-programs"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Creator campaign",
"description": "Help shops discover Dukanam.",
"is_active": true,
"audience": "selected",
"partner_ids": [
3
],
"starts_at": null,
"ends_at": null,
"commission_type": "percent",
"flat_amount": 250,
"percent": 25,
"hold_days": 15
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partner-programs';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Creator campaign',
'description' => 'Help shops discover Dukanam.',
'is_active' => true,
'audience' => 'selected',
'partner_ids' => [3],
'starts_at' => null,
'ends_at' => null,
'commission_type' => 'percent',
'flat_amount' => 250.0,
'percent' => 25.0,
'hold_days' => 15,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 1,
"owner_master_partner_id": null,
"name": "Creator campaign",
"description": "Help shops discover Dukanam.",
"is_active": true,
"audience": "selected",
"partner_ids": [
3
],
"starts_at": null,
"ends_at": null,
"commission": {
"type": "percent",
"flat_amount_paise": null,
"percent_basis_points": 2500,
"custom": false,
"description": "25% of the first payment"
},
"hold_days": 15,
"created_at": "2026-10-06T12:00:00.000000Z"
}
}
Example response (422):
{
"message": "The selected audience is invalid.",
"errors": {
"audience": [
"The selected audience is invalid."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get a named partner program.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/partner-programs/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/admin/partner-programs/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partner-programs/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 1,
"owner_master_partner_id": null,
"name": "Creator campaign",
"description": "Help shops discover Dukanam.",
"is_active": true,
"audience": "selected",
"partner_ids": [
3
],
"starts_at": null,
"ends_at": null,
"commission": {
"type": "percent",
"flat_amount_paise": null,
"percent_basis_points": 2500,
"custom": false,
"description": "25% of the first payment"
},
"hold_days": 15,
"created_at": "2026-10-06T12:00:00.000000Z"
}
}
Example response (404):
{
"message": "Not found."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Replace a named partner program.
requires authentication
Send the full configuration. Pausing, expiry or removing an eligible partner stops new attribution. Already attributed referrals retain a snapshot of the program reward and hold period from sign-up, even if the program changes later. Suspending the partner still blocks commissions. Omitted timestamps clear the window.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/admin/partner-programs/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Creator campaign\",
\"description\": \"Help shops discover Dukanam.\",
\"is_active\": true,
\"audience\": \"selected\",
\"partner_ids\": [
3
],
\"starts_at\": null,
\"ends_at\": null,
\"commission_type\": \"percent\",
\"flat_amount\": 250,
\"percent\": 25,
\"hold_days\": 15
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partner-programs/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Creator campaign",
"description": "Help shops discover Dukanam.",
"is_active": true,
"audience": "selected",
"partner_ids": [
3
],
"starts_at": null,
"ends_at": null,
"commission_type": "percent",
"flat_amount": 250,
"percent": 25,
"hold_days": 15
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partner-programs/1';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Creator campaign',
'description' => 'Help shops discover Dukanam.',
'is_active' => true,
'audience' => 'selected',
'partner_ids' => [3],
'starts_at' => null,
'ends_at' => null,
'commission_type' => 'percent',
'flat_amount' => 250.0,
'percent' => 25.0,
'hold_days' => 15,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 1,
"owner_master_partner_id": null,
"name": "Creator campaign",
"description": "Help shops discover Dukanam.",
"is_active": true,
"audience": "selected",
"partner_ids": [
3
],
"starts_at": null,
"ends_at": null,
"commission": {
"type": "percent",
"flat_amount_paise": null,
"percent_basis_points": 2500,
"custom": false,
"description": "25% of the first payment"
},
"hold_days": 15,
"created_at": "2026-10-06T12:00:00.000000Z"
}
}
Example response (422):
{
"message": "The ends at field must be a date after starts at.",
"errors": {
"ends_at": [
"The ends at field must be a date after starts at."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get partner program settings.
requires authentication
The default commission deal, the refund hold period, the minimum payout, the TDS rate applied to payouts and the share message, with program-wide totals.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/partner-program" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/admin/partner-program"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partner-program';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"enabled": true,
"commission_type": "flat",
"flat_amount_paise": 20000,
"percent_basis_points": 2000,
"hold_days": 30,
"minimum_payout_paise": 50000,
"tds_basis_points": 0,
"share_message": "I use Dukanam for billing, stock and khata. Try it free: {link}",
"placeholders": [
"{link}",
"{code}",
"{name}"
],
"totals": {
"partners": 4,
"clicks": 1820,
"signups": 96,
"paid": 14,
"on_hold_paise": 120000,
"available_paise": 80000,
"processing_paise": 0,
"paid_out_net_paise": 160000
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update partner program settings.
requires authentication
Amounts in rupees and rates in percent, as the admin console takes them. Applies to commissions earned and payouts prepared from now on; each existing commission keeps the rule it was earned under. Turning the program off stops new clicks and sign-ups being credited; accounts already credited still earn when they pay.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/admin/partner-program" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"enabled\": true,
\"commission_type\": \"flat\",
\"flat_amount\": 200,
\"percent\": 20,
\"hold_days\": 30,
\"minimum_payout\": 500,
\"tds_percent\": 2,
\"share_message\": \"Try Dukanam free: {link}\"
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partner-program"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"enabled": true,
"commission_type": "flat",
"flat_amount": 200,
"percent": 20,
"hold_days": 30,
"minimum_payout": 500,
"tds_percent": 2,
"share_message": "Try Dukanam free: {link}"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partner-program';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'enabled' => true,
'commission_type' => 'flat',
'flat_amount' => 200.0,
'percent' => 20.0,
'hold_days' => 30,
'minimum_payout' => 500.0,
'tds_percent' => 2.0,
'share_message' => 'Try Dukanam free: {link}',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"enabled": true,
"commission_type": "percent",
"flat_amount_paise": 20000,
"percent_basis_points": 2500,
"hold_days": 15,
"minimum_payout_paise": 50000,
"tds_basis_points": 200,
"share_message": "Try Dukanam free: {link}",
"placeholders": [
"{link}",
"{code}",
"{name}"
],
"totals": {
"partners": 4,
"clicks": 1820,
"signups": 96,
"paid": 14,
"on_hold_paise": 120000,
"available_paise": 80000,
"processing_paise": 0,
"paid_out_net_paise": 160000
}
}
}
Example response (422):
{
"message": "The commission type field is required.",
"errors": {
"commission_type": [
"The commission type field is required."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List partners.
requires authentication
Every partner with their deal and a leaderboard row for the period: human clicks, sign-ups and paid accounts in the period, and lifetime money earned (not rejected), owed (on hold or approved, not yet paid) and paid out net of TDS.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/partners?search=ravi&status=active&period=30d&per_page=25" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"search\": \"b\",
\"per_page\": 22
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partners"
);
const params = {
"search": "ravi",
"status": "active",
"period": "30d",
"per_page": "25",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"search": "b",
"per_page": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partners';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'search' => 'ravi',
'status' => 'active',
'period' => '30d',
'per_page' => '25',
],
'json' => [
'search' => 'b',
'per_page' => 22,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 3,
"name": "Ravi Kumar",
"email": "[email protected]",
"phone": "+919876543210",
"pan_on_file": true,
"pan_masked": "••••••234F",
"status": "active",
"signup_source": "invited",
"profile_completed_at": "2026-09-29T10:00:00.000000Z",
"commission": {
"type": "flat",
"flat_amount_paise": 25000,
"percent_basis_points": null,
"custom": true,
"description": "₹250.00 per paid account"
},
"notes": "YouTube, 80k subscribers",
"links_count": 3,
"has_payout_account": true,
"last_login_at": "2026-09-28T10:00:00.000000Z",
"created_at": "2026-09-01T10:00:00.000000Z",
"stats": {
"clicks": 640,
"signups": 31,
"paid": 6,
"earned_paise": 150000,
"owed_paise": 50000,
"paid_out_net_paise": 100000
},
"account_type": "individual",
"master_partner_id": null,
"team_role": null,
"payment_responsibility": "platform",
"platform_payout_terms": {
"hold_days": 30,
"minimum_payout_paise": 50000,
"tds_basis_points": 0
}
}
],
"links": {},
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Invite a partner.
requires authentication
Creates the partner and a first link. They can sign in at /partners/login straight
away with a WhatsApp code to this number. Leave commission_type empty for the
program default.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/admin/partners" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"account_type\": \"master\",
\"commission_hold_days\": 7,
\"payout_minimum_amount\": 1000,
\"payout_tds_percent\": 2,
\"name\": \"Ravi Kumar\",
\"phone\": \"9876543210\",
\"email\": \"[email protected]\",
\"status\": \"active\",
\"commission_type\": \"flat\",
\"commission_flat_amount\": 250,
\"commission_percent\": 25,
\"notes\": \"YouTube, 80k subscribers\"
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partners"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"account_type": "master",
"commission_hold_days": 7,
"payout_minimum_amount": 1000,
"payout_tds_percent": 2,
"name": "Ravi Kumar",
"phone": "9876543210",
"email": "[email protected]",
"status": "active",
"commission_type": "flat",
"commission_flat_amount": 250,
"commission_percent": 25,
"notes": "YouTube, 80k subscribers"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partners';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'account_type' => 'master',
'commission_hold_days' => 7,
'payout_minimum_amount' => 1000.0,
'payout_tds_percent' => 2.0,
'name' => 'Ravi Kumar',
'phone' => '9876543210',
'email' => '[email protected]',
'status' => 'active',
'commission_type' => 'flat',
'commission_flat_amount' => 250.0,
'commission_percent' => 25.0,
'notes' => 'YouTube, 80k subscribers',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 3,
"name": "Ravi Kumar",
"email": "[email protected]",
"phone": "+919876543210",
"pan_on_file": false,
"pan_masked": null,
"status": "active",
"signup_source": "invited",
"profile_completed_at": "2026-09-29T10:00:00.000000Z",
"commission": {
"type": "flat",
"flat_amount_paise": 25000,
"percent_basis_points": null,
"custom": true,
"description": "₹250.00 per paid account"
},
"notes": "YouTube, 80k subscribers",
"last_login_at": null,
"created_at": "2026-09-29T10:00:00.000000Z",
"links": [
{
"id": 7,
"code": "RAVI7K",
"label": "Main link",
"program_id": null,
"url": "https://dukanam.com/p/RAVI7K",
"destination": "register",
"is_active": true,
"created_at": "2026-09-29T10:00:00.000000Z"
}
],
"account_type": "individual",
"master_partner_id": null,
"team_role": null,
"payment_responsibility": "platform",
"platform_payout_terms": {
"hold_days": 30,
"minimum_payout_paise": 50000,
"tds_basis_points": 0
}
}
}
Example response (422):
{
"message": "Another partner already uses this number.",
"errors": {
"phone": [
"Another partner already uses this number."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get a partner's performance.
requires authentication
The partner, their funnel for the period, lifetime earnings, every link with its own
funnel, click sources and a zero-filled 30-day daily series. available_programs
uses the same active, scheduled and audience filtering as the partner portal;
id null denotes the default program with the partner’s personal deal applied.
Suspended partners and global program pause return an empty available_programs list.
Active masters include team_overview with team funnel, per-member performance,
platform earnings, team obligations and retained amount. Member available_programs
contains only their master's eligible programs; earnings use the team reward ledger.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/partners/1?period=30d" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
const url = new URL(
"https://dukanam.com/api/v1/admin/partners/1"
);
const params = {
"period": "30d",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partners/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'period' => '30d',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"partner": {
"id": 3,
"name": "Ravi Kumar",
"email": "[email protected]",
"phone": "+919876543210",
"pan_on_file": true,
"pan_masked": "••••••234F",
"status": "active",
"signup_source": "invited",
"profile_completed_at": "2026-09-29T10:00:00.000000Z",
"commission": {
"type": "flat",
"flat_amount_paise": 25000,
"percent_basis_points": null,
"custom": true,
"description": "₹250.00 per paid account"
},
"notes": null,
"has_payout_account": true,
"last_login_at": null,
"created_at": "2026-09-01T10:00:00.000000Z",
"account_type": "individual",
"master_partner_id": null,
"team_role": null,
"payment_responsibility": "platform",
"platform_payout_terms": {
"hold_days": 30,
"minimum_payout_paise": 50000,
"tds_basis_points": 0
}
},
"funnel": {
"clicks": 640,
"unique_visitors": 512,
"signups": 31,
"checkouts_started": 9,
"paid": 6,
"click_to_signup_rate": 4.8,
"signup_to_paid_rate": 19.4
},
"earnings": {
"on_hold_paise": 50000,
"available_paise": 0,
"processing_paise": 0,
"paid_paise": 100000,
"lifetime_paise": 150000,
"paid_out_net_paise": 100000,
"tds_deducted_paise": 0
},
"links": [
{
"id": 7,
"code": "RAVI7K",
"label": "Main link",
"program_id": null,
"url": "https://dukanam.com/p/RAVI7K",
"destination": "register",
"is_active": true,
"created_at": "2026-09-01T10:00:00.000000Z",
"stats": {
"clicks": 640,
"unique_visitors": 512,
"signups": 31,
"checkouts_started": 9,
"paid": 6,
"click_to_signup_rate": 4.8,
"signup_to_paid_rate": 19.4
}
}
],
"sources": {
"youtube": 420,
"whatsapp": 150,
"direct": 70
},
"daily": [
{
"date": "2026-09-29",
"clicks": 24,
"signups": 1,
"paid": 0
}
],
"available_programs": [
{
"id": null,
"name": "Shop referral program",
"description": "Help shops get started with Dukanam and earn on their first paid plan payment.",
"is_default": true,
"commission": {
"type": "flat",
"flat_amount_paise": 25000,
"percent_basis_points": null,
"custom": true,
"description": "₹250.00 per paid account"
},
"hold_days": 30,
"minimum_payout_paise": 50000,
"tds_basis_points": 0,
"starts_at": null,
"ends_at": null
}
],
"team_overview": null
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update a partner.
requires authentication
Send only what changes. Suspending a partner signs them out, stops their links
crediting new sign-ups and stops new commissions. Send commission_type as null to
move individuals back to the program default. Members cannot change hierarchy or
platform deals. Master fixed deals must remain above active fixed team rewards.
Null payout overrides restore global defaults. Masters with members cannot be downgraded.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/admin/partners/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"account_type\": \"master\",
\"commission_hold_days\": 7,
\"payout_minimum_amount\": 1000,
\"payout_tds_percent\": 2,
\"name\": \"Ravi Kumar\",
\"phone\": \"9876543210\",
\"email\": \"[email protected]\",
\"status\": \"suspended\",
\"commission_type\": \"percent\",
\"commission_flat_amount\": 250,
\"commission_percent\": 25,
\"notes\": \"Paused while the campaign is reviewed\"
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partners/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"account_type": "master",
"commission_hold_days": 7,
"payout_minimum_amount": 1000,
"payout_tds_percent": 2,
"name": "Ravi Kumar",
"phone": "9876543210",
"email": "[email protected]",
"status": "suspended",
"commission_type": "percent",
"commission_flat_amount": 250,
"commission_percent": 25,
"notes": "Paused while the campaign is reviewed"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partners/1';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'account_type' => 'master',
'commission_hold_days' => 7,
'payout_minimum_amount' => 1000.0,
'payout_tds_percent' => 2.0,
'name' => 'Ravi Kumar',
'phone' => '9876543210',
'email' => '[email protected]',
'status' => 'suspended',
'commission_type' => 'percent',
'commission_flat_amount' => 250.0,
'commission_percent' => 25.0,
'notes' => 'Paused while the campaign is reviewed',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 3,
"name": "Ravi Kumar",
"email": "[email protected]",
"phone": "+919876543210",
"pan_on_file": false,
"pan_masked": null,
"status": "suspended",
"commission": {
"type": "percent",
"flat_amount_paise": null,
"percent_basis_points": 2500,
"custom": true,
"description": "25% of the first payment"
},
"notes": "Paused while the campaign is reviewed",
"last_login_at": null,
"created_at": "2026-09-01T10:00:00.000000Z",
"account_type": "individual",
"master_partner_id": null,
"team_role": null,
"payment_responsibility": "platform",
"platform_payout_terms": {
"hold_days": 30,
"minimum_payout_paise": 50000,
"tds_basis_points": 0
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Add a link for a partner.
requires authentication
Codes are letters and numbers, 4–16 characters, unique across partner links and
customer referral codes. Omit code to generate one from the partner's name.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/admin/partners/1/links" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"program_id\": null,
\"label\": \"Instagram bio\",
\"code\": \"RAVIINSTA\",
\"destination\": \"pricing\"
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partners/1/links"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"program_id": null,
"label": "Instagram bio",
"code": "RAVIINSTA",
"destination": "pricing"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partners/1/links';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'program_id' => null,
'label' => 'Instagram bio',
'code' => 'RAVIINSTA',
'destination' => 'pricing',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 8,
"code": "RAVIINSTA",
"label": "Instagram bio",
"program_id": null,
"url": "https://dukanam.com/p/RAVIINSTA",
"destination": "pricing",
"is_active": true,
"created_at": "2026-09-29T10:00:00.000000Z"
}
}
Example response (422):
{
"message": "This code is already taken. Try another.",
"errors": {
"code": [
"This code is already taken. Try another."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Pause or resume a partner link.
requires authentication
A paused link still sends visitors to sign-up but no longer counts clicks or credits sign-ups to the partner.
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/admin/partners/1/links/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"is_active\": false
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partners/1/links/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"is_active": false
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partners/1/links/1';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'is_active' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 8,
"code": "RAVIINSTA",
"label": "Instagram bio",
"program_id": null,
"url": "https://dukanam.com/p/RAVIINSTA",
"destination": "pricing",
"is_active": false,
"created_at": "2026-09-29T10:00:00.000000Z"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List a partner's sign-ups.
requires authentication
Every account credited to the partner, with the account and workspace. Partners themselves never see these names.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/partners/1/referrals?status=paid&per_page=25" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"per_page\": 1
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partners/1/referrals"
);
const params = {
"status": "paid",
"per_page": "25",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"per_page": 1
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partners/1/referrals';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'status' => 'paid',
'per_page' => '25',
],
'json' => [
'per_page' => 1,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 41,
"code": "RAVI7K",
"link": {
"id": 7,
"label": "Main link"
},
"source": "web",
"status": "paid",
"status_label": "Paid",
"user": {
"id": 88,
"name": "Meena Shah",
"email": "[email protected]"
},
"business": {
"id": 90,
"name": "Meena Stores"
},
"payment_source": "razorpay",
"payment_amount_paise": 84661,
"commission_blocked_reason": null,
"registered_at": "2026-09-10T08:00:00.000000Z",
"subscription_pending_at": "2026-09-12T08:00:00.000000Z",
"paid_at": "2026-09-12T08:05:00.000000Z"
}
],
"links": {},
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Add a bonus or adjustment.
requires authentication
A campaign fee or bonus (positive) or a claw-back (negative), in rupees. Approved at once and included in the partner's next payout.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/admin/partners/1/adjustments" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"amount\": 1500,
\"description\": \"Diwali campaign fee\"
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partners/1/adjustments"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"amount": 1500,
"description": "Diwali campaign fee"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partners/1/adjustments';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'amount' => 1500.0,
'description' => 'Diwali campaign fee',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 55,
"partner_referral_id": null,
"kind": "adjustment",
"amount_paise": 150000,
"basis": null,
"description": "Diwali campaign fee",
"status": "approved",
"status_label": "Approved",
"earned_at": "2026-09-29T10:00:00.000000Z",
"hold_until": null,
"approved_at": "2026-09-29T10:00:00.000000Z",
"rejected_at": null,
"rejection_reason": null,
"partner_payout_id": null,
"paid_at": null
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List partner commissions.
requires authentication
A conversion commission is earned when a credited account's first paid charge is
captured; it is pending (on hold) until hold_until, then approved. An approved
commission with a partner_payout_id is in a payout being sent. basis records the
rule it was earned under. For named-program links it also includes program_id,
program_name and the reward/hold terms saved at sign-up.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/partner-commissions?partner_id=3&status=pending&per_page=25" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"partner_id\": 16,
\"per_page\": 22
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partner-commissions"
);
const params = {
"partner_id": "3",
"status": "pending",
"per_page": "25",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"partner_id": 16,
"per_page": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partner-commissions';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'partner_id' => '3',
'status' => 'pending',
'per_page' => '25',
],
'json' => [
'partner_id' => 16,
'per_page' => 22,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 54,
"partner": {
"id": 3,
"name": "Ravi Kumar"
},
"partner_referral_id": 41,
"kind": "conversion",
"amount_paise": 25000,
"basis": {
"type": "flat",
"flat_paise": 25000,
"percent_basis_points": null,
"custom": true,
"base_paise": 84661,
"hold_days": 30,
"program_id": 1,
"program_name": "Creator campaign"
},
"description": "Paid account via RAVI7K",
"status": "pending",
"status_label": "On hold",
"earned_at": "2026-09-12T08:05:00.000000Z",
"hold_until": "2026-10-12T08:05:00.000000Z",
"approved_at": null,
"rejected_at": null,
"rejection_reason": null,
"partner_payout_id": null,
"paid_at": null
}
],
"links": {},
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Approve a commission now.
requires authentication
Skips the rest of the hold period, so the commission goes into the next payout.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/admin/partner-commissions/1/approve" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/admin/partner-commissions/1/approve"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partner-commissions/1/approve';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 54,
"partner_referral_id": 41,
"kind": "conversion",
"amount_paise": 25000,
"basis": {
"type": "flat",
"flat_paise": 25000,
"percent_basis_points": null,
"custom": true,
"base_paise": 84661,
"hold_days": 30
},
"description": "Paid account via RAVI7K",
"status": "approved",
"status_label": "Approved",
"earned_at": "2026-09-12T08:05:00.000000Z",
"hold_until": "2026-10-12T08:05:00.000000Z",
"approved_at": "2026-09-29T10:00:00.000000Z",
"rejected_at": null,
"rejection_reason": null,
"partner_payout_id": null,
"paid_at": null
}
}
Example response (422):
{
"message": "Only a commission on hold can be approved.",
"errors": {
"commission": [
"Only a commission on hold can be approved."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Reject a commission.
requires authentication
For a refund, a fake sign-up or a policy breach. Only an unpaid commission that is not in a payout can be rejected; cancel the payout first. The partner sees the reason. Its unpaid team reward is also rejected; a reserved team settlement is cancelled, releasing other rewards. Already recorded external team payments remain paid.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/admin/partner-commissions/1/reject" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"reason\": \"Subscription refunded\"
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partner-commissions/1/reject"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"reason": "Subscription refunded"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partner-commissions/1/reject';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'reason' => 'Subscription refunded',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 54,
"partner_referral_id": 41,
"kind": "conversion",
"amount_paise": 25000,
"basis": null,
"description": "Paid account via RAVI7K",
"status": "rejected",
"status_label": "Rejected",
"earned_at": "2026-09-12T08:05:00.000000Z",
"hold_until": "2026-10-12T08:05:00.000000Z",
"approved_at": null,
"rejected_at": "2026-09-29T10:00:00.000000Z",
"rejection_reason": "Subscription refunded",
"partner_payout_id": null,
"paid_at": null
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List payouts.
requires authentication
Payouts are listed without full account numbers; download the payment sheet for those.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/partner-payouts?partner_id=3&status=processing&per_page=25" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"partner_id\": 16,
\"per_page\": 22
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partner-payouts"
);
const params = {
"partner_id": "3",
"status": "processing",
"per_page": "25",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"partner_id": 16,
"per_page": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partner-payouts';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'partner_id' => '3',
'status' => 'processing',
'per_page' => '25',
],
'json' => [
'partner_id' => 16,
'per_page' => 22,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 12,
"partner": {
"id": 3,
"name": "Ravi Kumar"
},
"status": "processing",
"status_label": "Processing",
"gross_paise": 100000,
"tds_basis_points": 200,
"tds_paise": 2000,
"net_paise": 98000,
"commissions_count": 4,
"account": {
"method": "bank",
"label": "HDFC Bank ••6789",
"account_holder_name": "Ravi Kumar",
"ifsc": "HDFC0001234"
},
"reference": null,
"notes": null,
"created_at": "2026-09-29T10:00:00.000000Z",
"paid_at": null,
"cancelled_at": null
}
],
"links": {},
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Prepare payouts.
requires authentication
Releases commissions whose hold has ended, then groups every approved commission not
yet in a payout into one processing payout per partner, sent to their primary payout
account with their effective TDS rate deducted. Master payout overrides apply.
Team members are paid separately by their masters and never receive platform payouts.
Partners under their effective minimum payout, or
without a payout account, are listed and skipped.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/admin/partner-payouts/prepare" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"partner_ids\": [
3
]
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partner-payouts/prepare"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"partner_ids": [
3
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partner-payouts/prepare';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'partner_ids' => [3],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"created": [
{
"id": 12,
"partner": {
"id": 3,
"name": "Ravi Kumar"
},
"status": "processing",
"status_label": "Processing",
"gross_paise": 100000,
"tds_basis_points": 200,
"tds_paise": 2000,
"net_paise": 98000,
"commissions_count": 4,
"account": {
"method": "bank",
"label": "HDFC Bank ••6789",
"account_holder_name": "Ravi Kumar",
"ifsc": "HDFC0001234"
},
"reference": null,
"notes": null,
"created_at": "2026-09-29T10:00:00.000000Z",
"paid_at": null,
"cancelled_at": null
}
],
"missing_account": [
{
"id": 5,
"name": "Anita Rao"
}
],
"below_minimum": []
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Download the payment sheet.
requires authentication
A CSV with one row per payout and the full account details to transfer to: partner name, phone, email, PAN, method, account holder, account number, IFSC, bank, UPI ID, gross, TDS and net amounts in rupees, and the reference and date once paid.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/partner-payouts/export?status=processing" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
const url = new URL(
"https://dukanam.com/api/v1/admin/partner-payouts/export"
);
const params = {
"status": "processing",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partner-payouts/export';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'status' => 'processing',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, CSV file):
payout_id,partner_id,partner_name,phone,email,pan,method,account_holder_name,account_number,ifsc,bank_name,upi_id,gross_amount,tds_amount,net_amount,commissions,status,prepared_at,reference,paid_at
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Mark a payout paid.
requires authentication
Record the bank reference (UTR) and date. The payout and its commissions become
paid, and the partner sees it in their portal.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/admin/partner-payouts/1/mark-paid" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"reference\": \"UTR123456789\",
\"paid_on\": \"2026-09-29\",
\"notes\": \"Paid by NEFT\"
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partner-payouts/1/mark-paid"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"reference": "UTR123456789",
"paid_on": "2026-09-29",
"notes": "Paid by NEFT"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partner-payouts/1/mark-paid';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'reference' => 'UTR123456789',
'paid_on' => '2026-09-29',
'notes' => 'Paid by NEFT',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 12,
"status": "paid",
"status_label": "Paid",
"gross_paise": 100000,
"tds_basis_points": 200,
"tds_paise": 2000,
"net_paise": 98000,
"commissions_count": 4,
"account": {
"method": "bank",
"label": "HDFC Bank ••6789",
"account_holder_name": "Ravi Kumar",
"ifsc": "HDFC0001234"
},
"reference": "UTR123456789",
"notes": "Paid by NEFT",
"created_at": "2026-09-29T10:00:00.000000Z",
"paid_at": "2026-09-29T12:00:00.000000Z",
"cancelled_at": null
}
}
Example response (422):
{
"message": "Only a payout that is processing can be marked paid.",
"errors": {
"payout": [
"Only a payout that is processing can be marked paid."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Cancel a payout.
requires authentication
For a transfer that bounced or will not be made. Its commissions return to approved and go into the next payout.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/admin/partner-payouts/1/cancel" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"notes\": \"Account closed; partner is adding a new one\"
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/partner-payouts/1/cancel"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"notes": "Account closed; partner is adding a new one"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/partner-payouts/1/cancel';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'notes' => 'Account closed; partner is adding a new one',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 12,
"status": "cancelled",
"status_label": "Cancelled",
"gross_paise": 100000,
"tds_basis_points": 200,
"tds_paise": 2000,
"net_paise": 98000,
"commissions_count": 4,
"account": {
"method": "bank",
"label": "HDFC Bank ••6789",
"account_holder_name": "Ravi Kumar",
"ifsc": "HDFC0001234"
},
"reference": null,
"notes": "Account closed; partner is adding a new one",
"created_at": "2026-09-29T10:00:00.000000Z",
"paid_at": null,
"cancelled_at": "2026-09-29T12:00:00.000000Z"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Party and referrer profitability
Advanced Plan exports require the corresponding profitability feature, accounting and exports permissions, and data_export. PDF, Excel and CSV contain every matching row, independently of screen pagination. Dates, calculations and tenant checks match the on-screen reports. Excel/CSV money is numeric rupees; PDF displays INR.
Export party profit and loss.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/contacts/1/profitability/export?format=pdf&period=this-financial-year" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contacts/1/profitability/export"
);
const params = {
"format": "pdf",
"period": "this-financial-year",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contacts/1/profitability/export';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'format' => 'pdf',
'period' => 'this-financial-year',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
Binary data - All invoice contribution rows and report total.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Export referrer profit and loss.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/workers/architecto/profitability/export?format=xlsx&period=this-financial-year" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/workers/architecto/profitability/export"
);
const params = {
"format": "xlsx",
"period": "this-financial-year",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/workers/architecto/profitability/export';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'format' => 'xlsx',
'period' => 'this-financial-year',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
Binary data - All owner-attributed invoice contribution rows and report total.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Export the referrer commission statement.
requires authentication
Opening balance precedes the selected period. Every dated earning, return debit note, payout and correction is retained with a running balance. Negative balances are recoverable; customer dues are not offset. Payments do not create commission expenses.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/workers/architecto/statement/export?format=csv&period=this-financial-year" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/workers/architecto/statement/export"
);
const params = {
"format": "csv",
"period": "this-financial-year",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/workers/architecto/statement/export';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'format' => 'csv',
'period' => 'this-financial-year',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
Binary data - Complete dated statement with opening, running and closing balances.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Compare party profit and loss.
requires authentication
Requires reports.party_profitability and accounting. Includes customer/both parties, archived parties and those with no activity. Same dated facts as profile P&L. Summary covers every party matching search/status, independently of pagination. Invoice count uses current non-voided invoices issued in the period; current invoice dues exclude opening khata balances. Untracked goods have no recorded inventory cost.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/reports/party-profitability?period=this-financial-year&status=all&sort=profit&per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/reports/party-profitability"
);
const params = {
"period": "this-financial-year",
"status": "all",
"sort": "profit",
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/reports/party-profitability';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'period' => 'this-financial-year',
'status' => 'all',
'sort' => 'profit',
'per_page' => '20',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
summary
object
contribution_profit_paise
integer
Total contribution of every party matching filters.
parties
object
data
object
costs_include_estimates
boolean
Incomplete or unverified inventory history; inspect profile costing fields.
invoice_due_paise
integer
Current non-voided invoice dues across all dates.
Export the party profitability comparison.
requires authentication
Same search/status/date filters as comparison. Every matching party is included, independently of pagination. Inventory costs containing estimates are labelled.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/reports/party-profitability/export?format=xlsx&period=this-financial-year&status=all&sort=profit" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/reports/party-profitability/export"
);
const params = {
"format": "xlsx",
"period": "this-financial-year",
"status": "all",
"sort": "profit",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/reports/party-profitability/export';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'format' => 'xlsx',
'period' => 'this-financial-year',
'status' => 'all',
'sort' => 'profit',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
Binary data - All matching parties, current invoice dues and contribution total.
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Party profit and loss.
requires authentication
Feature reports.party_profitability. Reports customer sales contribution, excluding
general shop overhead. Supplier spend is not treated as supplier profit/loss.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/contacts/1/profitability?period=this-financial-year&per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/contacts/1/profitability"
);
const params = {
"period": "this-financial-year",
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/contacts/1/profitability';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'period' => 'this-financial-year',
'per_page' => '20',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
summary
object
net_sales_paise
integer
Net tax-exclusive sales after dated returns.
goods_cost_paise
integer
Recognized goods cost after dated return cost reversals.
commission_paise
integer
Net commission earned after dated debit notes and invoice corrections.
contribution_profit_paise
integer
Net sales minus goods cost, linked selling expenses and commissions. General overhead is excluded.
margin_percent
number|null
Contribution divided by positive net sales; null for zero or negative sales.
costing
object
method
string
Perpetual weighted-average inventory costing.
unverified_item_count
integer
Distinct inventory items in affected invoices whose dated history has not been reconciled.
incomplete_item_count
integer
Distinct inventory items with incomplete or chronologically invalid history; cost estimates remain.
Compare referrer performance.
requires authentication
Requires referral_workers and reports.worker_profitability. Includes referrers with no referrals. Sales/costs use owner-entered shares, while each referrer's commission remains their own amount. Invoice/customer counts count current non-voided referrals issued in the period; dated returns from earlier invoices affect profit independently. Dues and balances are current, and shared customer dues appear under each referrer and must not be added together. Last referral is the latest current non-voided invoice date.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/workers/performance?period=this-financial-year&status=all&worker_type=referral&sort=profit&per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"q\": \"b\",
\"status\": \"active\",
\"worker_type\": \"architecto\",
\"sort\": \"profit\",
\"per_page\": 2,
\"page\": 67
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/workers/performance"
);
const params = {
"period": "this-financial-year",
"status": "all",
"worker_type": "referral",
"sort": "profit",
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"q": "b",
"status": "active",
"worker_type": "architecto",
"sort": "profit",
"per_page": 2,
"page": 67
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/workers/performance';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'period' => 'this-financial-year',
'status' => 'all',
'worker_type' => 'referral',
'sort' => 'profit',
'per_page' => '20',
],
'json' => [
'q' => 'b',
'status' => 'active',
'worker_type' => 'architecto',
'sort' => 'profit',
'per_page' => 2,
'page' => 67,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
workers
object
data
object
contribution_profit_paise
integer
Attributed sales minus goods cost, invoice expenses and this referrer's earned commissions.
worker_type
string
Stable profession/referral type code.
worker_type_label
string
Display label for the referrer type.
referred_customer_due_paise
integer
Current invoice dues, shown under every referrer sharing the invoice; not additive across referrers.
Referrer profit and loss.
requires authentication
Feature reports.worker_profitability, alongside referral_workers. Reports the shop's
contribution from this referrer's referred invoices. The referrer profile commission
statement separately reports the referrer's own earned/paid/due/recoverable amounts.
The costing object has the same inventory confidence fields as party profitability.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/workers/architecto/profitability?period=this-financial-year&per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/workers/architecto/profitability"
);
const params = {
"period": "this-financial-year",
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/workers/architecto/profitability';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'period' => 'this-financial-year',
'per_page' => '20',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Party pricing
Rate cards that decide what a party pays. A contact carries one card and, optionally, a blanket discount; an invoice line for an item then takes its rate from the card unless the request quotes a rate of its own. See the Contacts endpoints for attaching a card to a party.
List the rate cards of the business.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/price-lists" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"per_page\": 1
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/price-lists"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"per_page": 1
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/price-lists';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'per_page' => 1,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
is_effective_today
boolean
Whether the card prices anything today, taking its active flag and validity window into account.
contact_count
integer
Number of parties currently carrying the card.
Create a rate card.
requires authentication
Every row is either a fixed rate or a percentage off the item master, and applies from its
min_quantity upwards, so one item can carry several quantity breaks on the same card.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/price-lists" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Wholesale\",
\"description\": \"Eius et animi quos velit et.\",
\"price_includes_tax\": false,
\"is_active\": false,
\"valid_from\": \"2026-04-01\",
\"valid_to\": \"2027-03-31\",
\"items\": [
{
\"item_id\": 16,
\"sale_price\": 455,
\"discount_percent\": 5,
\"min_quantity\": 25
}
]
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/price-lists"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Wholesale",
"description": "Eius et animi quos velit et.",
"price_includes_tax": false,
"is_active": false,
"valid_from": "2026-04-01",
"valid_to": "2027-03-31",
"items": [
{
"item_id": 16,
"sale_price": 455,
"discount_percent": 5,
"min_quantity": 25
}
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/price-lists';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Wholesale',
'description' => 'Eius et animi quos velit et.',
'price_includes_tax' => false,
'is_active' => false,
'valid_from' => '2026-04-01',
'valid_to' => '2027-03-31',
'items' => [
['item_id' => 16, 'sale_price' => 455, 'discount_percent' => 5, 'min_quantity' => 25],
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "Enter either a fixed rate or a discount for this item, not both.",
"errors": {
"items.0.sale_price": [
"Enter either a fixed rate or a discount for this item, not both."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
GET api/v1/businesses/{business}/price-lists/{id}
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/price-lists/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/price-lists/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/price-lists/1';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Replace a rate card.
requires authentication
Sending items replaces every row on the card; leaving it out edits the card header and
keeps the rows. Saved documents keep the rate they were raised at either way.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/businesses/1/price-lists/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Wholesale\",
\"description\": \"Eius et animi quos velit et.\",
\"price_includes_tax\": false,
\"is_active\": false,
\"valid_from\": \"2026-04-01\",
\"valid_to\": \"2027-03-31\",
\"items\": [
{
\"item_id\": 16,
\"sale_price\": 455,
\"discount_percent\": 5,
\"min_quantity\": 25
}
]
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/price-lists/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Wholesale",
"description": "Eius et animi quos velit et.",
"price_includes_tax": false,
"is_active": false,
"valid_from": "2026-04-01",
"valid_to": "2027-03-31",
"items": [
{
"item_id": 16,
"sale_price": 455,
"discount_percent": 5,
"min_quantity": 25
}
]
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/price-lists/1';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Wholesale',
'description' => 'Eius et animi quos velit et.',
'price_includes_tax' => false,
'is_active' => false,
'valid_from' => '2026-04-01',
'valid_to' => '2027-03-31',
'items' => [
['item_id' => 16, 'sale_price' => 455, 'discount_percent' => 5, 'min_quantity' => 25],
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Delete a rate card that no party carries.
requires authentication
Example request:
curl --request DELETE \
"https://dukanam.com/api/v1/businesses/1/price-lists/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/price-lists/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/price-lists/1';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (422):
{
"message": "Move the parties on this rate card to another one before deleting it.",
"errors": {
"price_list": [
"Move the parties on this rate card to another one before deleting it."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Payment accounts
GET api/v1/businesses/{business}/payment-accounts
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/payment-accounts" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/payment-accounts"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/payment-accounts';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
POST api/v1/businesses/{business}/payment-accounts
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/payment-accounts" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/payment-accounts"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/payment-accounts';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
PATCH api/v1/businesses/{business}/payment-accounts/{paymentAccount_id}
requires authentication
Example request:
curl --request PATCH \
"https://dukanam.com/api/v1/businesses/1/payment-accounts/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/payment-accounts/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "PATCH",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/payment-accounts/1';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
POST api/v1/businesses/{business}/payment-accounts/transfer
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/payment-accounts/transfer" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"from_payment_account_id\": 16,
\"to_payment_account_id\": 16,
\"amount\": 4326.41688,
\"transferred_on\": \"2026-01-15\",
\"reference\": \"m\",
\"notes\": \"i\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/payment-accounts/transfer"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"from_payment_account_id": 16,
"to_payment_account_id": 16,
"amount": 4326.41688,
"transferred_on": "2026-01-15",
"reference": "m",
"notes": "i"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/payment-accounts/transfer';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'from_payment_account_id' => 16,
'to_payment_account_id' => 16,
'amount' => 4326.41688,
'transferred_on' => '2026-01-15',
'reference' => 'm',
'notes' => 'i',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Show eligible transactions and statement-reconciliation history.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/payment-accounts/1/reconciliation" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"statement_date\": \"2026-01-15\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/payment-accounts/1/reconciliation"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"statement_date": "2026-01-15"
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/payment-accounts/1/reconciliation';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'statement_date' => '2026-01-15',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Complete and lock a statement reconciliation.
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/payment-accounts/1/reconciliation" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"statement_date\": \"2026-01-15\",
\"statement_balance\": -999999998,
\"transaction_ids\": [
16
],
\"notes\": \"n\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/payment-accounts/1/reconciliation"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"statement_date": "2026-01-15",
"statement_balance": -999999998,
"transaction_ids": [
16
],
"notes": "n"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/payment-accounts/1/reconciliation';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'statement_date' => '2026-01-15',
'statement_balance' => -999999998,
'transaction_ids' => [16],
'notes' => 'n',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
POST api/v1/businesses/{business}/payment-accounts/{paymentAccount_id}/reconcile/{journalLine_id}
requires authentication
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/payment-accounts/1/reconcile/1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/payment-accounts/1/reconcile/1"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/payment-accounts/1/reconcile/1';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Platform broadcasts
Super admins push an announcement, optionally with a banner image, to the shops that match a set of workspace filters: where they trade, what they sell, and what they pay for.
List sent broadcasts.
requires authentication
Newest first, with the filters each was sent to and how many devices Firebase accepted.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/notifications?per_page=24" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"per_page\": 1
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/notifications"
);
const params = {
"per_page": "24",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"per_page": 1
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/notifications';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '24',
],
'json' => [
'per_page' => 1,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 9,
"title": "GST filing window opens tomorrow",
"body": "GSTR-3B for this month can be prepared from the GST workspace starting tomorrow.",
"image_url": "https://app.dukanam.test/storage/broadcast-images/gst.png",
"link_url": "https://app.dukanam.test/gst",
"audience": {
"state_codes": [
"33"
],
"city": null,
"store_types": [],
"plan_ids": [
2
],
"subscription_statuses": [
"active"
],
"workspace_status": "active",
"recipients": "owners"
},
"audience_summary": "Tamil Nadu · Growth plan · Active · Active workspaces · Business owners only",
"businesses_count": 128,
"recipients_count": 96,
"sent_count": 94,
"failed_count": 2,
"status": "sent",
"failure_message": null,
"created_by": {
"id": 1,
"name": "Platform Admin"
},
"created_at": "2026-09-20T09:00:00.000000Z",
"sent_at": "2026-09-20T09:00:12.000000Z"
}
],
"links": {},
"meta": {
"current_page": 1,
"per_page": 24,
"total": 1
}
}
Example response (401):
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "This action is unauthorized."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Preview how many shops and devices a filter set reaches.
requires authentication
Nothing is sent or stored. reachable is the number of people with a registered device,
which is what a send would actually push to, and push_enabled reports whether Firebase
push is configured on this deployment at all.
The list filters are spelled out here because Scribe reduces an in-constrained list to a
single value when it reads them off the form request.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/notifications/audience?state_codes[]=33&state_codes[]=29&city=Coimbatore&store_types[]=pharmacy&plan_ids[]=2&subscription_statuses[]=active&subscription_statuses[]=trialing&workspace_status=any&recipients=owners" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/admin/notifications/audience"
);
const params = {
"state_codes[0]": "33",
"state_codes[1]": "29",
"city": "Coimbatore",
"store_types[0]": "pharmacy",
"plan_ids[0]": "2",
"subscription_statuses[0]": "active",
"subscription_statuses[1]": "trialing",
"workspace_status": "any",
"recipients": "owners",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/notifications/audience';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'state_codes[0]' => '33',
'state_codes[1]' => '29',
'city' => 'Coimbatore',
'store_types[0]' => 'pharmacy',
'plan_ids[0]' => '2',
'subscription_statuses[0]' => 'active',
'subscription_statuses[1]' => 'trialing',
'workspace_status' => 'any',
'recipients' => 'owners',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"businesses": 128,
"recipients": 141,
"reachable": 96,
"push_enabled": true,
"audience": {
"state_codes": [
"33"
],
"city": null,
"store_types": [],
"plan_ids": [
2
],
"subscription_statuses": [
"active"
],
"workspace_status": "active",
"recipients": "owners"
},
"audience_summary": "Tamil Nadu · Growth plan · Active · Active workspaces · Business owners only"
}
}
Example response (401):
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "This action is unauthorized."
}
Example response (422):
{
"message": "The selected recipients is invalid.",
"errors": {
"recipients": [
"The selected recipients is invalid."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Send a broadcast.
requires authentication
The broadcast is recorded and then pushed on the queue, so the response returns with
status queued and recipients_count already resolved. Send as multipart/form-data
when attaching an image; array filters use state_codes[] style keys. A filter set that
reaches no registered device is rejected with 422 and nothing is pushed.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/admin/notifications" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "state_codes[]=33"\
--form "city=Coimbatore"\
--form "store_types[]=pharmacy"\
--form "plan_ids[]=2"\
--form "subscription_statuses[]=active"\
--form "workspace_status=active"\
--form "recipients=owners"\
--form "title=GST filing window opens tomorrow"\
--form "body=GSTR-3B for this month can be prepared from the GST workspace starting tomorrow."\
--form "image_url=https://cdn.example.com/banners/gst.png"\
--form "link_url=https://app.dukanam.test/gst"\
--form "image=@/path/to/file" const url = new URL(
"https://dukanam.com/api/v1/admin/notifications"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('state_codes[]', '33');
body.append('city', 'Coimbatore');
body.append('store_types[]', 'pharmacy');
body.append('plan_ids[]', '2');
body.append('subscription_statuses[]', 'active');
body.append('workspace_status', 'active');
body.append('recipients', 'owners');
body.append('title', 'GST filing window opens tomorrow');
body.append('body', 'GSTR-3B for this month can be prepared from the GST workspace starting tomorrow.');
body.append('image_url', 'https://cdn.example.com/banners/gst.png');
body.append('link_url', 'https://app.dukanam.test/gst');
body.append('image', document.querySelector('input[name="image"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/notifications';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'state_codes[]',
'contents' => '33'
],
[
'name' => 'city',
'contents' => 'Coimbatore'
],
[
'name' => 'store_types[]',
'contents' => 'pharmacy'
],
[
'name' => 'plan_ids[]',
'contents' => '2'
],
[
'name' => 'subscription_statuses[]',
'contents' => 'active'
],
[
'name' => 'workspace_status',
'contents' => 'active'
],
[
'name' => 'recipients',
'contents' => 'owners'
],
[
'name' => 'title',
'contents' => 'GST filing window opens tomorrow'
],
[
'name' => 'body',
'contents' => 'GSTR-3B for this month can be prepared from the GST workspace starting tomorrow.'
],
[
'name' => 'image_url',
'contents' => 'https://cdn.example.com/banners/gst.png'
],
[
'name' => 'link_url',
'contents' => 'https://app.dukanam.test/gst'
],
[
'name' => 'image',
'contents' => fopen('/path/to/file', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 9,
"title": "GST filing window opens tomorrow",
"body": "GSTR-3B for this month can be prepared from the GST workspace starting tomorrow.",
"image_url": "https://app.dukanam.test/storage/broadcast-images/gst.png",
"link_url": "https://app.dukanam.test/gst",
"audience": {
"state_codes": [
"33"
],
"city": null,
"store_types": [],
"plan_ids": [
2
],
"subscription_statuses": [
"active"
],
"workspace_status": "active",
"recipients": "owners"
},
"audience_summary": "Tamil Nadu · Growth plan · Active · Active workspaces · Business owners only",
"businesses_count": 128,
"recipients_count": 96,
"sent_count": 0,
"failed_count": 0,
"status": "queued",
"failure_message": null,
"created_by": {
"id": 1,
"name": "Platform Admin"
},
"created_at": "2026-09-20T09:00:00.000000Z",
"sent_at": null
}
}
Example response (401):
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "This action is unauthorized."
}
Example response (422):
{
"message": "No workspace matching these filters has a registered device.",
"errors": {
"recipients": [
"No workspace matching these filters has a registered device."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Platform business insights
Inspect aggregate phone usage.
requires authentication
Super-admin only, with no-store caching. Dates use Asia/Kolkata. Defaults to the last 30 calendar days including today; maximum 366 days. Latest is the all-time latest snapshot; snapshots and event totals are limited to the requested range. No installation IDs or raw reports. Source is device only while the latest reported mode is local; otherwise use server records.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/businesses/1/device-usage?from=2026-09-01&to=2026-09-27" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"from\": \"2026-01-15\",
\"to\": \"2026-01-15\"
}"
const url = new URL(
"https://dukanam.com/api/v1/admin/businesses/1/device-usage"
);
const params = {
"from": "2026-09-01",
"to": "2026-09-27",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"from": "2026-01-15",
"to": "2026-01-15"
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/businesses/1/device-usage';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'from' => '2026-09-01',
'to' => '2026-09-27',
],
'json' => [
'from' => '2026-01-15',
'to' => '2026-01-15',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"from": "2026-09-01",
"to": "2026-09-27",
"timezone": "Asia/Kolkata",
"source": "device",
"mode": "local",
"app_version": "1.1.1",
"last_reported_at": "2026-09-27T03:44:05.000000Z",
"latest": {
"date": "2026-09-27",
"mode": "local",
"totals": {
"invoices": 5
},
"money": {
"billed_value_paise": 120000
},
"reported_at": "2026-09-27T03:44:05.000000Z"
},
"snapshots": [],
"events": [
{
"key": "invoice_shared_whatsapp",
"label": "Invoice shared whatsapp",
"count": 2
}
],
"milestones": {
"first_invoice_at": "2026-09-27T03:40:00.000000Z"
}
}
}
Example response (401):
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "This action is unauthorized."
}
Example response (404):
{
"message": "Not found."
}
Example response (422):
{
"message": "The to field must be a date after or equal to from.",
"errors": {
"to": [
"The to field must be a date after or equal to from."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Inspect a business and its daily usage.
requires authentication
Requires a super-admin token; ordinary workspace members cannot access this endpoint. Daily usage counts records by creation time in the business timezone, including today (partial), with zero-filled dates in ascending order. It does not measure sessions or time spent. Invoices include every status. Customer contacts include type customer and both; supplier contacts include type supplier and both, so a contact marked as both is counted in each. Added counts include soft-deleted items, customers and suppliers; current totals exclude them. Contact classification reflects the contact's current type. Permanently deleted records cannot be counted. Last record added is the latest creation across these four categories, regardless of the selected period. All timestamps are ISO 8601; calendar dates are YYYY-MM-DD.
Owner insights include mobile activity (active within 30 days, inactive, seen, unknown), ISO 8601 first/last seen timestamps, push registration, first observed login, and acquisition. Installation status is always unknown. Mobile activity reflects mobile-scoped API tokens, not proof of a current installation. First observed login starts when tracking is enabled. Acquisition source is referral, campaign, referring_site, direct_or_unknown, or unknown.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/businesses/17?days=7" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/admin/businesses/17"
);
const params = {
"days": "7",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/businesses/17';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'days' => '7',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"business": {
"id": 17,
"name": "Sampada Stores",
"legal_name": "Sampada Retail LLP",
"store_type": "grocery",
"is_active": true,
"email": "[email protected]",
"phone": "+919876543210",
"owner": {
"id": 42,
"name": "Kavitha Reddy",
"email": "[email protected]",
"phone": null
},
"gstin": null,
"gst_registration_type": "unregistered",
"address": {
"line_1": "14 Market Road",
"line_2": "First floor",
"city": "Pune",
"state_code": "27",
"state_name": "Maharashtra",
"pincode": "411001"
},
"currency": "INR",
"timezone": "Asia/Kolkata",
"default_locale": "en",
"created_at": "2026-09-01T04:30:00.000000Z"
},
"usage": {
"days": 7,
"from": "2026-09-08",
"to": "2026-09-14",
"timezone": "Asia/Kolkata",
"generated_at": "2026-09-14T08:00:00.000000Z",
"totals": {
"invoices": 23,
"items": 12,
"customers": 31,
"suppliers": 4
},
"added": {
"invoices": 3,
"items": 2,
"customers": 4,
"suppliers": 1
},
"today": {
"date": "2026-09-14",
"invoices": 3,
"items": 2,
"customers": 4,
"suppliers": 1,
"total": 10
},
"active_days": 1,
"last_record_added_at": "2026-09-14T07:00:00.000000Z",
"daily": [
{
"date": "2026-09-08",
"invoices": 0,
"items": 0,
"customers": 0,
"suppliers": 0,
"total": 0
},
{
"date": "2026-09-09",
"invoices": 0,
"items": 0,
"customers": 0,
"suppliers": 0,
"total": 0
},
{
"date": "2026-09-10",
"invoices": 0,
"items": 0,
"customers": 0,
"suppliers": 0,
"total": 0
},
{
"date": "2026-09-11",
"invoices": 0,
"items": 0,
"customers": 0,
"suppliers": 0,
"total": 0
},
{
"date": "2026-09-12",
"invoices": 0,
"items": 0,
"customers": 0,
"suppliers": 0,
"total": 0
},
{
"date": "2026-09-13",
"invoices": 0,
"items": 0,
"customers": 0,
"suppliers": 0,
"total": 0
},
{
"date": "2026-09-14",
"invoices": 3,
"items": 2,
"customers": 4,
"suppliers": 1,
"total": 10
}
]
},
"owner_insights": {
"mobile": {
"status": "unknown",
"label": "No app activity recorded",
"installation_status": "unknown",
"first_seen_at": null,
"last_seen_at": null,
"push_registered": false
},
"first_observed_login": {
"at": null,
"method": null,
"channel": null
},
"acquisition": {
"source": "unknown"
}
}
}
}
Example response (401):
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "This action is unauthorized."
}
Example response (404):
{
"message": "Not found."
}
Example response (422):
{
"message": "The selected days is invalid.",
"errors": {
"days": [
"The selected days is invalid."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Platform overview
The numbers behind the admin console's overview, one section per call. Requires a super-admin token. Money is in paise and excludes GST: plan prices include 18% GST, so the taxable part is price × 10000 / 11800. Days are calendar days in Asia/Kolkata. An active business had someone use the web app, open the mobile app, or create records that day; request-level tracking started on 27 September 2026 and earlier days are reconstructed from created records. Invoices are sales invoices except voided ones.
Get one overview section.
requires authentication
today compares today with yesterday and returns seven-day series (oldest first) for each
headline number, invoices per hour (hours still to come are null), active businesses by
channel, counts that need attention, and today's milestones. growth returns signups,
activation (5+ invoices) and signup-to-paid rates against the previous period of equal
length, a daily series, the activation funnel for people who signed up in the range, signup
sources, and weekly retention for up to eight weekly cohorts of new businesses. engagement
returns active businesses today and over 7 and 30 days ending at to, stickiness (average
daily actives over the last 7 days ÷ 30-day actives), a daily series split by channel with
invoices and billed value, the spread of businesses by invoice count, feature adoption among
active businesses, the top 10 businesses by invoices, and up to 10 businesses quiet for 14+
days. revenue returns paid MRR and ARR (paying subscriptions only: no trials, no
complimentary plans), money collected in the range and the previous one, GST collected,
trial-to-paid conversion for trials that ended in the range, six months of new and churned
MRR, money collected per day by Razorpay and Apple, the current plan mix and a watch list.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/admin/insights/engagement?from=2026-01-15&to=2026-01-15" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/admin/insights/engagement"
);
const params = {
"from": "2026-01-15",
"to": "2026-01-15",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/admin/insights/engagement';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'from' => '2026-01-15',
'to' => '2026-01-15',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"date": "2026-09-26",
"kpis": {
"new_signups": {
"today": 0,
"yesterday": 1,
"last_7_days": [
1,
0,
0,
0,
0,
1,
0
]
},
"new_businesses": {
"today": 0,
"yesterday": 1,
"last_7_days": [
1,
0,
0,
0,
0,
1,
0
]
},
"active_businesses": {
"today": 1,
"yesterday": 2,
"last_7_days": [
0,
0,
0,
0,
0,
2,
1
],
"share_of_businesses": 33.3,
"total_businesses": 3,
"web": 1,
"mobile": 1
},
"invoices_created": {
"today": 2,
"yesterday": 1,
"last_7_days": [
5,
0,
0,
0,
0,
1,
2
],
"per_active_business": 2
},
"billed_value_paise": {
"today": 240000,
"yesterday": 120000,
"last_7_days": [
600000,
0,
0,
0,
0,
120000,
240000
],
"average_invoice_paise": 120000
},
"subscription_revenue_paise": {
"today": 33814,
"yesterday": 0,
"last_7_days": [
0,
0,
0,
0,
0,
0,
33814
],
"payments": 1,
"gross_paise": 39900
},
"new_paying_businesses": {
"today": 1,
"yesterday": 0,
"last_7_days": [
0,
0,
0,
0,
0,
0,
1
],
"paying_businesses": 1
},
"app_link_opens": {
"today": 0,
"yesterday": 0,
"last_7_days": [
0,
0,
0,
0,
0,
0,
0
],
"store_redirects": 0,
"link_previews": 0
}
},
"invoices_by_hour": {
"today": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2,
0,
0,
0,
0,
null,
null,
null,
null,
null,
null,
null,
null
],
"yesterday": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0
]
},
"active_by_channel": {
"mobile_only": 0,
"web_only": 0,
"both": 1,
"records_only": 0
},
"needs_attention": {
"past_due": 0,
"trials_ending_7_days": 1,
"unpaid_checkouts_7_days": 0,
"new_without_invoice": 0,
"feedback_pending": 0,
"testimonials_pending": 0
},
"milestones": [
{
"at": "2026-09-26T15:00:00+05:30",
"type": "trial",
"business": "Ravi Hardware",
"text": "started a Smart Books trial"
},
{
"at": "2026-09-26T09:30:00+05:30",
"type": "payment",
"business": "Kavya Fashions",
"text": "paid for Business · ₹399 incl. GST"
}
]
}
}
Example response (200, engagement):
{
"data": {
"range": {
"from": "2026-09-20",
"to": "2026-09-26",
"days": 7
},
"kpis": {
"active_today": 1,
"active_7_days": 2,
"active_30_days": 3,
"active_businesses_total": 3,
"stickiness": 14.3
},
"daily": [
{
"date": "2026-09-20",
"mobile_only": 0,
"web_only": 0,
"both": 0,
"records_only": 0,
"invoices": 5,
"billed_value_paise": 600000
},
{
"date": "2026-09-21",
"mobile_only": 0,
"web_only": 0,
"both": 0,
"records_only": 0,
"invoices": 0,
"billed_value_paise": 0
},
{
"date": "2026-09-22",
"mobile_only": 0,
"web_only": 0,
"both": 0,
"records_only": 0,
"invoices": 0,
"billed_value_paise": 0
},
{
"date": "2026-09-23",
"mobile_only": 0,
"web_only": 0,
"both": 0,
"records_only": 0,
"invoices": 0,
"billed_value_paise": 0
},
{
"date": "2026-09-24",
"mobile_only": 0,
"web_only": 0,
"both": 0,
"records_only": 0,
"invoices": 0,
"billed_value_paise": 0
},
{
"date": "2026-09-25",
"mobile_only": 1,
"web_only": 1,
"both": 0,
"records_only": 0,
"invoices": 1,
"billed_value_paise": 120000
},
{
"date": "2026-09-26",
"mobile_only": 0,
"web_only": 0,
"both": 1,
"records_only": 0,
"invoices": 2,
"billed_value_paise": 240000
}
],
"invoice_distribution": [
{
"label": "None",
"businesses": 2
},
{
"label": "1–10",
"businesses": 1
},
{
"label": "11–50",
"businesses": 0
},
{
"label": "51–200",
"businesses": 0
},
{
"label": "200+",
"businesses": 0
}
],
"feature_adoption": [
{
"key": "sales_invoices",
"label": "Sales invoices",
"businesses": 1,
"share": 50
},
{
"key": "pos",
"label": "POS counter",
"businesses": 1,
"share": 50
},
{
"key": "payments",
"label": "Payments received",
"businesses": 0,
"share": 0
},
{
"key": "stock",
"label": "Stock tracking",
"businesses": 0,
"share": 0
},
{
"key": "purchases",
"label": "Purchase bills",
"businesses": 0,
"share": 0
},
{
"key": "expenses",
"label": "Expenses",
"businesses": 0,
"share": 0
},
{
"key": "quotes_challans",
"label": "Quotes and challans",
"businesses": 0,
"share": 0
},
{
"key": "email",
"label": "Emailed a document",
"businesses": 0,
"share": 0
},
{
"key": "gst",
"label": "GST returns",
"businesses": 0,
"share": 0
}
],
"top_businesses": [
{
"id": 1,
"name": "Kavya Fashions",
"plan": "Business",
"invoices": 8,
"billed_value_paise": 960000,
"last_active_on": "2026-09-26",
"last_channel": "both"
}
],
"going_quiet": [
{
"id": 3,
"name": "Durga Textiles",
"last_active_on": "2026-09-05",
"quiet_days": 21,
"invoices_ever": 3,
"phone": "9848012345"
}
]
}
}
Example response (401):
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "This action is unauthorized."
}
Example response (404):
{
"message": "Not found."
}
Example response (422):
{
"message": "Choose a range of at most 366 days.",
"errors": {
"from": [
"Choose a range of at most 366 days."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Push testing
Unauthenticated bench for firing a push at a single device token, so mobile work can be verified without a signed-in user or a scheduled reminder behind it.
Enabled by default in all environments, including production, for temporary testing.
Set FIREBASE_TEST_ENDPOINT_ENABLED=false to disable after testing. This endpoint is public.
POST api/v1/fcm-test
Example request:
curl --request POST \
"https://dukanam.com/api/v1/fcm-test" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"fcm_token\": \"eY2x9_example_fcm_registration_token\",
\"title\": \"Stock alert\",
\"body\": \"Rice is running low.\",
\"data\": {
\"type\": \"low_stock\",
\"item_id\": \"7\"
}
}"
const url = new URL(
"https://dukanam.com/api/v1/fcm-test"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"fcm_token": "eY2x9_example_fcm_registration_token",
"title": "Stock alert",
"body": "Rice is running low.",
"data": {
"type": "low_stock",
"item_id": "7"
}
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/fcm-test';
$response = $client->post(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'fcm_token' => 'eY2x9_example_fcm_registration_token',
'title' => 'Stock alert',
'body' => 'Rice is running low.',
'data' => ['type' => 'low_stock', 'item_id' => '7'],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"sent": true,
"token": "eY2x9_e…n_token",
"title": "Stock alert",
"body": "Rice is running low.",
"data": {
"type": "low_stock",
"item_id": "7"
}
}
}
Example response (404):
{
"message": "Not Found."
}
Example response (422):
{
"message": "The data payload must be a flat object.",
"errors": {
"data": [
"The data payload must be a flat object."
]
}
}
Example response (503):
{
"message": "Firebase push is disabled. Set FIREBASE_PUSH_ENABLED=true and provide a readable service-account credentials file."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Referrers and referral commissions
Referrers recommend the shop and bring customers; link them to sales to track performance and commissions. The UI calls these people Referrers. Existing /workers URLs and worker_* request/response keys are retained for compatibility.
Advanced Plan feature referral_workers. Owners/administrators manage referrers, assign
commissions and make payments. Accountants can read profiles and statements. All amounts
are integer paise; percentage rates use basis points (1000 means 10%). Commission is earned
on a saved invoice's discounted, tax-exclusive value. Customer dues remain separate.
Read the invoice's current referrers.
requires authentication
Finance-only assignments are separate from customer-facing invoice payloads. Commission amounts are original earnings before return adjustments; use the referrer statement for debit notes and current balances. Voided invoices have no active awards.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/invoices/1/workers" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/1/workers"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/1/workers';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
worker_id
integer
Referrer identifier in this business.
attribution_basis_points
integer
Owner-entered performance share.
earned_paise
integer
Commission earned on the original invoice value, before returns.
List referrers.
requires authentication
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/workers?q=carpenter&status=active&worker_type=referral&per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"q\": \"b\",
\"status\": \"active\",
\"worker_type\": \"architecto\",
\"per_page\": 2
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/workers"
);
const params = {
"q": "carpenter",
"status": "active",
"worker_type": "referral",
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"q": "b",
"status": "active",
"worker_type": "architecto",
"per_page": 2
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/workers';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'q' => 'carpenter',
'status' => 'active',
'worker_type' => 'referral',
'per_page' => '20',
],
'json' => [
'q' => 'b',
'status' => 'active',
'worker_type' => 'architecto',
'per_page' => 2,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [],
"current_page": 1,
"per_page": 20,
"total": 0
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
pan_masked
string|null
Masked PAN; no raw identity number is returned.
bank_account
object|null
Bank metadata with account_number_masked; full account_number is excluded from lists.
worker_type
string
Stable profession/referral type code.
worker_type_label
string
Display label for the referrer type.
Referrer profile and commission statement.
requires authentication
Earnings are independent of collection. The statement retains every payout, debit note and compensating correction. A negative balance is recoverable from the referrer. Referred invoice dues and the referrer's own customer-account dues are separate current balances.
Full bank account numbers are returned only here for owners and administrators; fetch fresh and do not cache offline. Aadhaar and PAN remain masked. The response is served with Cache-Control: no-store, private.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/workers/architecto?period=this-financial-year&page=1&per_page=20" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"per_page\": 2,
\"page\": 22
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/workers/architecto"
);
const params = {
"period": "this-financial-year",
"page": "1",
"per_page": "20",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"per_page": 2,
"page": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/workers/architecto';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'period' => 'this-financial-year',
'page' => '1',
'per_page' => '20',
],
'json' => [
'per_page' => 2,
'page' => 22,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
summary
object
commission_due_paise
integer
Current positive commission balance.
recoverable_paise
integer
Current amount recoverable after returns or cancellations.
referred_customer_due_paise
integer
Current outstanding on this referrer's referred invoices, independently of date filters.
own_customer_due_paise
integer
Current invoice dues on the referrer's linked customer account; never offset against commission.
total_earned_paise
integer
Lifetime net commissions after return debit notes and invoice corrections, across all dates.
total_paid_paise
integer
Lifetime referrer payments after payment voids, across all dates.
worker
object
pan_masked
string|null
Saved PAN with only the last four characters visible.
bank_account
object|null
Saved payout bank details, or null.
account_number
string
Full account number as a string, preserving leading zeroes; only included for owners and administrators.
account_number_masked
string
Masked account number for display.
worker_type
string
Stable profession/referral type code.
worker_type_label
string
Display label for the referrer type.
statement
object
opening_balance_paise
integer
Signed commission balance before the selected period.
closing_balance_paise
integer
Signed balance through the end of the selected period.
View the referrer's private photo.
requires authentication
Production photos use private S3; legacy photos use their recorded disk. This authenticated route never exposes an object key or public bucket URL.
Returns an authenticated private image, never a public storage URL.
Example request:
curl --request GET \
--get "https://dukanam.com/api/v1/businesses/1/workers/architecto/photo" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://dukanam.com/api/v1/businesses/1/workers/architecto/photo"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/workers/architecto/photo';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401):
Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
x-robots-tag: noindex, nofollow
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: *
{
"message": "Unauthenticated."
}
Example response (404):
{
"message": "Not Found"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Add a referrer.
requires authentication
Aadhaar is optional, encrypted at rest, and returned only as its masked last four digits. PAN and bank details are encrypted, excluded from audit metadata and raw model serialization. Lists and write responses mask bank numbers; only the owner/administrator detail response includes the full number. Photos are private and require the same workspace, plan and role checks as a profile.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/workers" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Ravi\",
\"worker_type\": \"carpenter\",
\"trade\": \"Carpenter\",
\"phone\": \"9876543210\",
\"email\": \"[email protected]\",
\"pan_number\": \"ABCDE1234F\",
\"clear_pan\": false,
\"bank_account\": {
\"bank_name\": \"Example Bank\",
\"account_holder\": \"Ravi Kumar\",
\"account_number\": \"001234567890\",
\"ifsc\": \"HDFC0000123\",
\"branch\": \"Hyderabad\"
},
\"clear_bank_account\": false,
\"commission_rate_basis_points\": 1000,
\"remove_photo\": false
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/workers"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Ravi",
"worker_type": "carpenter",
"trade": "Carpenter",
"phone": "9876543210",
"email": "[email protected]",
"pan_number": "ABCDE1234F",
"clear_pan": false,
"bank_account": {
"bank_name": "Example Bank",
"account_holder": "Ravi Kumar",
"account_number": "001234567890",
"ifsc": "HDFC0000123",
"branch": "Hyderabad"
},
"clear_bank_account": false,
"commission_rate_basis_points": 1000,
"remove_photo": false
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/workers';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Ravi',
'worker_type' => 'carpenter',
'trade' => 'Carpenter',
'phone' => '9876543210',
'email' => '[email protected]',
'pan_number' => 'ABCDE1234F',
'clear_pan' => false,
'bank_account' => ['bank_name' => 'Example Bank', 'account_holder' => 'Ravi Kumar', 'account_number' => '001234567890', 'ifsc' => 'HDFC0000123', 'branch' => 'Hyderabad'],
'clear_bank_account' => false,
'commission_rate_basis_points' => 1000,
'remove_photo' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": {
"id": 1,
"name": "Ravi",
"worker_type": "carpenter",
"worker_type_label": "Carpenter",
"commission_rate_basis_points": 1000,
"is_active": true,
"balance_paise": 0,
"commission_due_paise": 0,
"recoverable_paise": 0,
"aadhaar_masked": null,
"pan_masked": null,
"bank_account": null
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update or archive a referrer.
requires authentication
Archiving preserves financial history and prevents new assignments. Changing a default
percentage affects future assignments only. Leave Aadhaar blank to retain it; pass
clear_aadhaar=true to remove it. Existing payouts and invoice rates are retained.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/businesses/1/workers/architecto" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Ravi\",
\"worker_type\": \"carpenter\",
\"trade\": \"Carpenter\",
\"phone\": \"9876543210\",
\"email\": \"[email protected]\",
\"pan_number\": \"ABCDE1234F\",
\"clear_pan\": false,
\"bank_account\": {
\"bank_name\": \"Example Bank\",
\"account_holder\": \"Ravi Kumar\",
\"account_number\": \"001234567890\",
\"ifsc\": \"HDFC0000123\",
\"branch\": \"Hyderabad\"
},
\"clear_bank_account\": false,
\"clear_aadhaar\": false,
\"commission_rate_basis_points\": 1000,
\"is_active\": true,
\"remove_photo\": false
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/workers/architecto"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Ravi",
"worker_type": "carpenter",
"trade": "Carpenter",
"phone": "9876543210",
"email": "[email protected]",
"pan_number": "ABCDE1234F",
"clear_pan": false,
"bank_account": {
"bank_name": "Example Bank",
"account_holder": "Ravi Kumar",
"account_number": "001234567890",
"ifsc": "HDFC0000123",
"branch": "Hyderabad"
},
"clear_bank_account": false,
"clear_aadhaar": false,
"commission_rate_basis_points": 1000,
"is_active": true,
"remove_photo": false
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/workers/architecto';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Ravi',
'worker_type' => 'carpenter',
'trade' => 'Carpenter',
'phone' => '9876543210',
'email' => '[email protected]',
'pan_number' => 'ABCDE1234F',
'clear_pan' => false,
'bank_account' => ['bank_name' => 'Example Bank', 'account_holder' => 'Ravi Kumar', 'account_number' => '001234567890', 'ifsc' => 'HDFC0000123', 'branch' => 'Hyderabad'],
'clear_bank_account' => false,
'clear_aadhaar' => false,
'commission_rate_basis_points' => 1000,
'is_active' => true,
'remove_photo' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Pay earned commission.
requires authentication
Partial/full payment is the owner's decision, independent of customer receipts. Paying reduces commission payable and the selected cash/bank account, without adding another expense. Cash requires an open register. Overpayments are rejected; retries require the same UUID and identical amount/date/method/account/reference, otherwise 422.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/workers/architecto/payouts" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"amount_paise\": 30000,
\"occurred_on\": \"2026-10-04\",
\"payment_method\": \"bank\",
\"idempotency_key\": \"e0e7b8fb-96bb-44bd-95f8-aad1eb6cf473\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/workers/architecto/payouts"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"amount_paise": 30000,
"occurred_on": "2026-10-04",
"payment_method": "bank",
"idempotency_key": "e0e7b8fb-96bb-44bd-95f8-aad1eb6cf473"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/workers/architecto/payouts';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'amount_paise' => 30000,
'occurred_on' => '2026-10-04',
'payment_method' => 'bank',
'idempotency_key' => 'e0e7b8fb-96bb-44bd-95f8-aad1eb6cf473',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Void a referrer payout.
requires authentication
Adds a current-date compensating event and reverses the cash/bank posting. Original earning, payout and return records remain. Repeated void requests do not duplicate it.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/workers/architecto/payouts/architecto/void" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"reason\": \"Incorrect bank transfer\"
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/workers/architecto/payouts/architecto/void"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"reason": "Incorrect bank transfer"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/workers/architecto/payouts/architecto/void';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'reason' => 'Incorrect bank transfer',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Assign the invoice referrer and earn commission.
requires authentication
Assignment is idempotent for an unchanged referrer/rate/invoice. Overrides require an owner or administrator. Reassignment appends financial corrections on the original dates; prior payouts remain in the previous referrer's account. Existing returns receive debit notes using cumulative rounding. Customer invoice print/shared payloads omit commissions.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/invoices/1/worker" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"attribution_basis_points\": 10000,
\"worker_id\": 1,
\"rate_basis_points\": 1000
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/1/worker"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"attribution_basis_points": 10000,
"worker_id": 1,
"rate_basis_points": 1000
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/1/worker';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'attribution_basis_points' => 10000,
'worker_id' => 1,
'rate_basis_points' => 1000,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": {
"id": 1,
"worker_id": 1,
"invoice_id": 1,
"rate_basis_points": 1000,
"basis_paise": 300000,
"earned_paise": 30000,
"earned_on": "2026-01-10"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Set all invoice referrers and commission rules.
requires authentication
Replaces the complete assignment set atomically (maximum 10, no duplicate referrers). Every performance share is required and shares must total 10000 (100%). Shares allocate sales, goods cost and shared invoice expenses exactly once; each commission is calculated independently. An empty array removes all assignments through compensating corrections. Fixed commissions reduce proportionally on returns, with cumulative paise rounding. Changing assignment rules keeps every previous earning, debit note and payout in history.
Example request:
curl --request PUT \
"https://dukanam.com/api/v1/businesses/1/invoices/1/workers" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"workers\": [
{
\"worker_id\": 1,
\"commission_kind\": \"percentage\",
\"rate_basis_points\": 1000,
\"attribution_basis_points\": 6000
},
{
\"worker_id\": 2,
\"commission_kind\": \"fixed\",
\"fixed_amount_paise\": 20000,
\"attribution_basis_points\": 4000
}
]
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/invoices/1/workers"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"workers": [
{
"worker_id": 1,
"commission_kind": "percentage",
"rate_basis_points": 1000,
"attribution_basis_points": 6000
},
{
"worker_id": 2,
"commission_kind": "fixed",
"fixed_amount_paise": 20000,
"attribution_basis_points": 4000
}
]
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/invoices/1/workers';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => \deepclone_from_array([
'classes' => 'stdClass',
'objectMeta' => 2,
'prepared' => [
'workers' => [0, 1],
],
'mask' => [
'workers' => [true, true],
],
'properties' => [
'stdClass' => [
'worker_id' => [1, 2],
'commission_kind' => ['percentage', 'fixed'],
'rate_basis_points' => [1000],
'attribution_basis_points' => [6000, 4000],
'fixed_amount_paise' => [1 => 20000],
],
],
], null, true),
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Usage reports
Report aggregate phone usage.
requires authentication
Available to every active member, including free workspaces with unfinished onboarding. Maximum 64 KB and 30 requests per hour per user, independent of the general API limit. Reusing a report_id returns 204 without applying event deltas again (90-day deduplication window). Unknown numeric metrics are retained; unknown text fields are discarded, never stored. Metric keys must be snake_case, at most 64 characters. Counts must be JSON integers, not numeric strings. Server dates use Asia/Kolkata; phone timestamps are metadata. Totals replace the day's snapshot; events accumulate. Competing local installs keep the higher invoice count (ties retain the existing snapshot). Server mode may omit totals/money.
Example request:
curl --request POST \
"https://dukanam.com/api/v1/businesses/1/usage-report" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"report_id\": \"9b2f6c1e-4a7d-4f0e-9a51-3d2c8e7b6a10\",
\"installation_id\": \"5d8a1f3c-7e20-4b6a-8c11-0f9e2d4b7a33\",
\"reported_at\": \"2026-09-27T09:14:05+05:30\",
\"period_start\": \"2026-09-26T08:02:11+05:30\",
\"mode\": \"local\",
\"app\": {
\"version\": \"1.1.1\",
\"build\": 20,
\"platform\": \"android\",
\"os_version\": \"g\",
\"locale\": \"en_MT\",
\"theme\": \"m\"
},
\"totals\": {
\"invoices\": 5,
\"customers\": 2
},
\"money\": {
\"billed_value_paise\": 120000
},
\"milestones\": {
\"first_invoice_at\": \"2026-09-27T09:10:00+05:30\",
\"last_invoice_at\": \"2026-01-15\",
\"first_payment_at\": \"2026-01-15\",
\"last_payment_at\": \"2026-01-15\",
\"first_record_at\": \"2026-01-15\",
\"last_record_at\": \"2026-01-15\",
\"onboarding_completed_at\": \"2026-01-15\",
\"first_share_at\": \"2026-01-15\"
},
\"events\": {
\"invoice_created\": 5,
\"invoice_shared_whatsapp\": 2
}
}"
const url = new URL(
"https://dukanam.com/api/v1/businesses/1/usage-report"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"report_id": "9b2f6c1e-4a7d-4f0e-9a51-3d2c8e7b6a10",
"installation_id": "5d8a1f3c-7e20-4b6a-8c11-0f9e2d4b7a33",
"reported_at": "2026-09-27T09:14:05+05:30",
"period_start": "2026-09-26T08:02:11+05:30",
"mode": "local",
"app": {
"version": "1.1.1",
"build": 20,
"platform": "android",
"os_version": "g",
"locale": "en_MT",
"theme": "m"
},
"totals": {
"invoices": 5,
"customers": 2
},
"money": {
"billed_value_paise": 120000
},
"milestones": {
"first_invoice_at": "2026-09-27T09:10:00+05:30",
"last_invoice_at": "2026-01-15",
"first_payment_at": "2026-01-15",
"last_payment_at": "2026-01-15",
"first_record_at": "2026-01-15",
"last_record_at": "2026-01-15",
"onboarding_completed_at": "2026-01-15",
"first_share_at": "2026-01-15"
},
"events": {
"invoice_created": 5,
"invoice_shared_whatsapp": 2
}
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://dukanam.com/api/v1/businesses/1/usage-report';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'report_id' => '9b2f6c1e-4a7d-4f0e-9a51-3d2c8e7b6a10',
'installation_id' => '5d8a1f3c-7e20-4b6a-8c11-0f9e2d4b7a33',
'reported_at' => '2026-09-27T09:14:05+05:30',
'period_start' => '2026-09-26T08:02:11+05:30',
'mode' => 'local',
'app' => ['version' => '1.1.1', 'build' => 20, 'platform' => 'android', 'os_version' => 'g', 'locale' => 'en_MT', 'theme' => 'm'],
'totals' => ['invoices' => 5, 'customers' => 2],
'money' => ['billed_value_paise' => 120000],
'milestones' => ['first_invoice_at' => '2026-09-27T09:10:00+05:30', 'last_invoice_at' => '2026-01-15', 'first_payment_at' => '2026-01-15', 'last_payment_at' => '2026-01-15', 'first_record_at' => '2026-01-15', 'last_record_at' => '2026-01-15', 'onboarding_completed_at' => '2026-01-15', 'first_share_at' => '2026-01-15'],
'events' => ['invoice_created' => 5, 'invoice_shared_whatsapp' => 2],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204):
Empty response
Example response (401):
{
"message": "Unauthenticated."
}
Example response (404):
{
"message": "Not found."
}
Example response (413):
{
"message": "Usage reports must not exceed 64 KB."
}
Example response (422):
{
"message": "The report id field is required.",
"errors": {
"report_id": [
"The report id field is required."
]
}
}
Example response (429):
{
"message": "Too Many Attempts."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.