MENU navbar-image

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."
}
 

Request      

POST api/v1/auth/otp/request

Headers
Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
phone   string     

An Indian mobile number in local or E.164 form. Example: 9876543210

challenge_id   string  optional    

Optional opaque ID from an earlier request for the same phone. A valid pending challenge is reused during cooldown; other callers remain rate limited. Example: 00000000-0000-4000-8000-000000000001

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."
}
 

Request      

POST api/v1/auth/otp/verify

Headers
Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
challenge_id   string     

The opaque challenge ID from the request response. Example: 00000000-0000-4000-8000-000000000001

code   string     

The six-digit WhatsApp code. Example: 123456

device_name   string     

A name for the device token. Example: Priya's phone

fcm_token   string  optional    

Optional Firebase Cloud Messaging registration token to save after successful sign-in. Example: eY2x9_example_fcm_registration_token

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."
}
 

Request      

POST api/v1/auth/otp/register

Headers
Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
challenge_id   string     

The verified challenge ID. Example: 00000000-0000-4000-8000-000000000001

registration_proof   string     

One-time secret returned by OTP verification. Example: one-time-registration-proof

name   string     

The account owner's name. Example: Priya Rao

business_name   string     

The first workspace name. Example: Priya Textiles

email   string     

Account and recovery email address. Example: [email protected]

password   string     

A recovery password, at least eight characters. Example: SecurePassword123!

device_name   string     

A name for the device token. Example: Priya's phone

fcm_token   string  optional    

Optional Firebase Cloud Messaging registration token to save for the new user. Example: eY2x9_example_fcm_registration_token

referral_code   string  optional    

Optional referral code from a shared link's ref parameter: a customer's referral code or an influencer partner's link code. An unknown code is rejected with 422 while the referral program is on, and ignored while it is paused. Example: K7M2QX9A

password_confirmation   string     

Must match password. Example: SecurePassword123!

acquisition   object  optional    

Optional first-touch attribution captured by the mobile client. Stored only at signup; referral records take precedence.

utm_source   string  optional    

Optional campaign source, max 255 characters. Example: google

utm_medium   string  optional    

Optional campaign medium, max 255 characters. Example: organic

utm_campaign   string  optional    

Optional campaign name, max 255 characters. Example: launch

referrer_host   string  optional    

Optional hostname only, max 253 characters; no URL path or query. Example: www.google.com

Response

Response Fields
data   object     
business   object     
is_owner   boolean     

Always true for the newly registered account's workspace.

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."
}
 

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"
    }
}
 

Request      

POST api/v1/auth/register

Headers
Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
name   string     

Must not be greater than 128 characters. Example: b

email   string     

Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

phone   string  optional    

Must not be greater than 32 characters. Example: i

business_name   string     

Must not be greater than 128 characters. Example: y

password   string     

Must be at least 8 characters. Example: pBNvYg

referral_code   string  optional    

Optional referral code from a shared link's ref parameter: a customer's referral code or an influencer partner's link code. An unknown code is rejected with 422 while the referral program is on, and ignored while it is paused. Example: K7M2QX9A

acquisition   object  optional    

Optional first-touch attribution captured by the mobile client. Stored only at signup; referral records take precedence.

utm_source   string  optional    

Optional campaign source, max 255 characters. Example: google

utm_medium   string  optional    

Optional campaign medium, max 255 characters. Example: organic

utm_campaign   string  optional    

Optional campaign name, max 255 characters. Example: launch

referrer_host   string  optional    

Optional hostname only, max 253 characters; no URL path or query. Example: www.google.com

device_name   string     

A name for the device token. Example: Scribe API Docs

fcm_token   string  optional    

An optional Firebase Cloud Messaging registration token to save for the user. Example: eY2x9_example_fcm_registration_token

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
    }
}
 

Request      

POST api/v1/auth/login

Headers
Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
email   string     

Must be a valid email address. Example: [email protected]

password   string     

Example: |]|{+-

remember   boolean  optional    

Example: false

fcm_token   string  optional    

Must not be greater than 4096 characters. Example: v

device_name   string     

A name for the device token. Example: Scribe API Docs

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."
}
 

Request      

POST api/v1/auth/forgot-password

Headers
Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
email   string     

The account email address. Example: [email protected]

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."
        ]
    }
}
 

Request      

POST api/v1/auth/reset-password

Headers
Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
token   string     

The token delivered in the password-reset email. Example: reset-token

email   string     

The account email address. Example: [email protected]

password   string     

The new password, at least eight characters. Example: new-secure-password

password_confirmation   string     

Must match password. Example: new-secure-password

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
        }
    }
}
 

Request      

GET api/v1/auth/me

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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."
}
 

Request      

PATCH api/v1/auth/fcm-token

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
fcm_token   string     

The Firebase Cloud Messaging registration token, or null to clear it. Example: eY2x9_example_fcm_registration_token

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));

Request      

DELETE api/v1/auth/token

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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));

Request      

DELETE api/v1/auth/tokens

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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."
}
 

Request      

GET api/v1/businesses

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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."
        ]
    }
}
 

Request      

POST api/v1/businesses

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
name   string     

Business name, up to 128 characters. Example: Priya Textiles

skip_setup   boolean  optional    

Create the workspace ready to use instead of at the start of guided onboarding. Transactional endpoints work immediately. Defaults to false. Example: true

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"
}
 

Request      

POST api/v1/businesses/{business}/select

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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"
        }
    }
}
 

Request      

PATCH api/v1/businesses/{business}/settings

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
name   string     

Must not be greater than 128 characters. Example: b

legal_name   string  optional    

Must not be greater than 191 characters. Example: n

phone   string  optional    

Must not be greater than 32 characters. Example: g

email   string  optional    

Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

gst_registration_type   string     

Example: unregistered

Must be one of:
  • unregistered
  • regular
  • composition
gstin   string  optional    

15-character GSTIN. Required when gst_registration_type is regular or composition; ignored and cleared when unregistered. Example: 36ABCDE1234F1Z5

address_line_1   string  optional    

Must not be greater than 191 characters. Example: w

address_line_2   string  optional    

Must not be greater than 191 characters. Example: p

city   string  optional    

Must not be greater than 96 characters. Example: w

state_code   string  optional    

Two-digit Indian state or union territory code of the business. Example: 36

pincode   string  optional    

Must be 6 digits. Example: 569775

default_place_of_supply   string  optional    

Example: 1

Must be one of:
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 26
  • 27
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 96
  • 97
invoice_prefix   string     

Invoice-series prefix, up to 15 characters. GST permits letters, numbers, hyphens, and slashes; the generated invoice number is limited to 16 characters. Example: HAWIOT/26-27/

prices_include_tax   boolean  optional    

Example: false

authorized_signatory   string  optional    

Must not be greater than 128 characters. Example: g

bank_details   string  optional    

Must not be greater than 1000 characters. Example: z

upi_id   string  optional    

Must match the regex /^[a-zA-Z0-9._-]{2,191}@[a-zA-Z0-9.-]{2,63}$/. Must not be greater than 255 characters. Example: m

receiving_bank_account   object  optional    

Shop bank account for customer payments. Omit to retain the current account; send all text fields empty to clear instructions. Uses the active default receiving bank without creating a duplicate ledger.

institution   string  optional    

Bank name, required when any bank text field is filled. Example: HDFC Bank

account_name   string  optional    

Account holder, required when any bank text field is filled. Example: Anika Stores

account_number   string  optional    

Full account number as 6–34 digits; preserve leading zeroes. Required when any bank text field is filled. Example: 001234567890

ifsc   string  optional    

Valid 11-character IFSC; normalised to uppercase. Required when any bank text field is filled. Example: HDFC0000123

branch   string  optional    

Optional branch, up to 128 characters. Example: Pune

show_on_invoice   boolean  optional    

Include the bank on invoice PDFs, buyer links and customer sharing/reminder messages. Defaults to true when bank text fields are supplied; a visibility-only object preserves saved details and empty accounts remain hidden. Example: true

theme   string  optional    

Workspace preset key (blue, emerald, teal, violet, rose, maroon, graphite) or a custom hex colour. Accepts #RGB/#RRGGBB or bare hex, normalised to uppercase #RRGGBB. Null resets to the default; omission preserves it. Example: #2563EB

custom_theme_colour   string  optional    

Optional web-form mirror for a hex theme. Used only when theme itself is a hex value; API clients normally send theme directly. Example: #2563EB

default_locale   string  optional    

Example: en

Must be one of:
  • en
  • hi
  • ta
  • te
  • ml
  • kn
  • mr
  • gu
  • bn
invoice_share_message_template   string  optional    

Must not be greater than 2000 characters. Example: w

notification_preferences   object[]  optional    
enabled   boolean  optional    

Example: false

days   integer[]  optional    

Must be between -365 and 365.

threshold   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 6

min_shelf_life_days   integer  optional    

Must be at least 0. Must not be greater than 730. Example: 25

short_expiry_action   string  optional    

Example: warn

Must be one of:
  • warn
  • block

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."
}
 

Request      

GET api/v1/businesses/{business}/dashboard

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/onboarding

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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));

Request      

PATCH api/v1/businesses/{business}/onboarding/language

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
locale   string     

Supported locale code. Example: hi

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));

Request      

POST api/v1/businesses/{business}/onboarding/business

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
name   string     

Public business name. Example: Veera Stores

store_type   string     

Store type key. Example: kirana-store

supply_type   string     

goods, services, or both. Example: goods

phone   string     

Public phone in E.164 or a 10-digit Indian local format. Example: 9876543210

logo   file  optional    

Optional PNG, JPG, or WebP logo up to 2 MB. Example: /path/to/file

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));

Request      

PATCH api/v1/businesses/{business}/onboarding/tax

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
gst_status   string     

registered or not_registered. Example: registered

gst_registration_type   string  optional    

Required when registered: normal or composition. Ignored for not_registered. Example: normal

gstin   string  optional    

Required when registered. Ignored for not_registered. May match another business workspace. Example: 29ABCDE1234F1Z5

legal_name   string  optional    

Must not be greater than 191 characters. Example: a

address_line_1   string     

Example: 12 Market Road

address_line_2   string  optional    

Must not be greater than 191 characters. Example: k

city   string     

Example: Bengaluru

state_code   string     

Indian GST state code. Example: 29

pincode   string     

Six digit PIN code. Example: 560001

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));

Request      

POST api/v1/businesses/{business}/onboarding/steps/{step}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

step   string     

The step. Example: architecto

Body Parameters
skip   boolean  optional    

Set true to finish this optional step later. Example: true

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));

Request      

POST api/v1/businesses/{business}/onboarding/product-imports

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
file   file     

Completed Dukanam XLSX template, maximum 5 MB. Example: /path/to/file

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/onboarding/product-imports/{itemImport_id}/confirm

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

itemImport_id   integer     

The ID of the itemImport. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/gstin/lookup

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
gstin   string     

The 15-character GSTIN to verify. Example: 03AAFCE1234J1Z0

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.
 

Request      

GET api/v1/businesses/{business}/contacts/{contact_id}/ledger.pdf

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

contact_id   integer     

The ID of the contact. Example: 1

Query Parameters
from   string  optional    

Start date, inclusive. Example: 2026-04-01

to   string  optional    

End date, inclusive. Must be on or after from. Example: 2027-03-31

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/contacts/{contact_id}/ledger/email

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

contact_id   integer     

The ID of the contact. Example: 1

Body Parameters
recipients   string[]     

A recipient key: primary for the party's primary contact, or person-{id} for an additional contact. Must match the regex /^(primary|person-\d+)$/.

recipient_emails   string[]  optional    

Must be a valid email address. Must not be greater than 254 characters.

subject   string     

Email subject line. Must not be greater than 150 characters. Example: Sri Lakshmi Traders | Purchase order: PO-0007

message   string     

Message body. Blank lines separate paragraphs. The PDF is attached and a button links to the signed public page. Must not be greater than 2000 characters. Example: Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.

from   string  optional    

Ledger statements only: start date, inclusive. Must be a valid date. Example: 2026-04-01

to   string  optional    

Ledger statements only: end date, inclusive. Must be on or after from. Must be a valid date. Must be a date after or equal to from. Example: 2027-03-31

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."
        ]
    }
}
 

Request      

PATCH api/v1/businesses/{business}/contacts/{contact_id}/active

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

contact_id   integer     

The ID of the contact. Example: 1

Body Parameters
is_active   boolean     

Whether the shop still trades with this party. Example: false

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
    }
}
 

Request      

PATCH api/v1/businesses/{business}/contacts/bulk

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
contact_ids   integer[]     
action   string     

activate or deactivate changes only status. customer, supplier or both changes only the party role. Documents, opening balances, journals and other profile fields are preserved. Example: both

Must be one of:
  • activate
  • deactivate
  • customer
  • supplier
  • both

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.
 

Request      

GET api/v1/businesses/{business}/contacts/{contact}/photo

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

contact   integer     

The contact. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/contacts

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
status   string  optional    

Which parties to list by state: all (the default) lists both, with inactive parties last, active lists only parties the shop still trades with, and inactive lists only inactive ones. Use active when building a picker. Example: `active

The search term also matches the name and phone of a contact's additional contacts.`

Body Parameters
type   string  optional    

Example: customer

Must be one of:
  • customer
  • supplier
  • both
search   string  optional    

Must not be greater than 128 characters. Example: b

status   string  optional    

Example: all

Must be one of:
  • all
  • active
  • inactive
per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 22

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));

Request      

POST api/v1/businesses/{business}/contacts

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
type   string     

Example: customer

Must be one of:
  • customer
  • supplier
  • both
is_active   boolean  optional    

Example: false

profile_type   string     

Example: consumer

Must be one of:
  • consumer
  • business
name   string     

Must not be greater than 128 characters. Example: b

company_name   string  optional    

Must not be greater than 191 characters. Example: n

contact_person   string  optional    

Primary contact person at a business party. Ignored for consumer contacts. The primary contact phone and email are the contact's own phone and email. Must not be greater than 128 characters. Example: Anita Rao

phone   string  optional    

Must not be greater than 32 characters. Example: g

email   string  optional    

Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

gst_treatment   string     

GST registration status. Use unregistered for an Indian business without GST registration and overseas for a contact outside India. Example: unregistered

Must be one of:
  • unregistered
  • registered_regular
  • registered_composition
  • consumer
  • overseas
  • sez
gstin   string  optional    

15-character Indian GSTIN. Required for registered_regular, registered_composition, and sez. Optional for an overseas contact registered under Indian GST; ignored for unregistered and consumer contacts. The same GSTIN may be used by multiple contacts. Must match the regex /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/. Must be 15 characters. Example: 29ABCDE1234F1Z5

pan   string  optional    

Optional Indian PAN. Ignored for consumer and overseas contacts. Must match the regex /^[A-Z]{5}[0-9]{4}[A-Z]$/. Must be 10 characters. Example: ABCDE1234F

country   string  optional    

Country where the overseas business is registered. Required when gst_treatment is overseas; ignored otherwise. Must not be greater than 96 characters. Example: United Arab Emirates

foreign_tax_id   string  optional    

Optional foreign Tax, VAT, or government-issued business identification number. Used only for overseas contacts. Must not be greater than 64 characters. Example: 100123456700003

latitude   number  optional    

Optional contact location latitude in decimal degrees, from -90 to 90. Send both latitude and longitude together. Omit both on update to preserve the location; send both as null to remove it. Mobile map pickers submit the selected coordinates here. This field is required when longitude is present. Must be between -90 and 90. Example: 17.448583

longitude   number  optional    

Optional contact location longitude in decimal degrees, from -180 to 180. Required with latitude; send both fields together, including when clearing them. This field is required when latitude is present. Must be between -180 and 180. Example: 78.390803

address   string  optional    

Must not be greater than 1000 characters. Example: d

billing_address_line_1   string  optional    

Must not be greater than 191 characters. Example: l

billing_address_line_2   string  optional    

Must not be greater than 191 characters. Example: j

billing_city   string  optional    

Must not be greater than 96 characters. Example: n

billing_state_code   string  optional    

Example: 1

Must be one of:
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 26
  • 27
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 96
  • 97
  • 99
billing_region   string  optional    

State, province, emirate, or other first-level region for an overseas billing address. Ignored for contacts in India. Must not be greater than 96 characters. Example: Dubai

billing_pincode   string  optional    

Must be 6 digits. Example: 569775

billing_postal_code   string  optional    

Postal or ZIP code for an overseas billing address. Ignored for contacts in India. Must not be greater than 32 characters. Example: SW1A 1AA

shipping_same_as_billing   boolean  optional    

Example: false

shipping_address_line_1   string  optional    

Must not be greater than 191 characters. Example: n

shipping_address_line_2   string  optional    

Must not be greater than 191 characters. Example: g

shipping_city   string  optional    

Must not be greater than 96 characters. Example: z

shipping_state_code   string  optional    

Example: 1

Must be one of:
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 26
  • 27
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 96
  • 97
  • 99
shipping_region   string  optional    

State, province, emirate, or other first-level region for an overseas shipping address. Ignored for contacts in India. Must not be greater than 96 characters. Example: Dubai

shipping_pincode   string  optional    

Must be 6 digits. Example: 569775

shipping_postal_code   string  optional    

Postal or ZIP code for an overseas shipping address. Ignored for contacts in India. Must not be greater than 32 characters. Example: SW1A 1AA

price_list_id   integer  optional    

Rate card this party buys at. Item lines raised for the party take their rate from it unless the request quotes one. Requires the party pricing feature; leave empty to bill at the item master rate. Example: 7

default_discount_percent   number  optional    

Blanket percentage off whatever rate the party would otherwise pay, applied after the rate card. Defaults to 0. Must be at least 0. Must not be greater than 100. Example: 2

credit_limit   number  optional    

Most this customer may owe at any moment, in rupees. Omit or send null for no limit; send 0 to put the party on cash only. Ignored for supplier contacts. A sales invoice or manual khata credit that would take the receivable balance past it is refused with HTTP 422. Must be at least 0. Must not be greater than 999999999. Example: 50000

opening_balance   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 22

opening_balance_side   string     

Example: receivable

Must be one of:
  • receivable
  • payable
opening_balance_date   string  optional    

Must be a valid date. Example: 2026-01-15

additional_contacts   object[]  optional    
name   string     

Must not be greater than 128 characters. Example: Suresh Gupta

designation   string  optional    

Must not be greater than 96 characters. Example: Accounts

phone   string  optional    

Must not be greater than 32 characters. Example: 9811111111

email   string  optional    

Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

photo   file  optional    

Optional private JPEG, PNG or WebP profile photo, up to 2 MB and 8192 pixels per side. Multipart upload. Omit to retain the saved photo; null does not remove it. Clients may crop before uploading. Must be an image. Must not be greater than 2048 kilobytes. Example: /path/to/file

remove_photo   boolean  optional    

Set true to remove the saved photo. Cannot be combined with a photo upload. Defaults to false. Example: false

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."
}
 

Request      

GET api/v1/businesses/{business}/contacts/{id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

id   integer     

The ID of the contact. Example: 1

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));

Request      

PUT api/v1/businesses/{business}/contacts/{id}

PATCH api/v1/businesses/{business}/contacts/{id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

id   integer     

The ID of the contact. Example: 1

Body Parameters
type   string     

Example: customer

Must be one of:
  • customer
  • supplier
  • both
is_active   boolean  optional    

Example: false

profile_type   string     

Example: consumer

Must be one of:
  • consumer
  • business
name   string     

Must not be greater than 128 characters. Example: b

company_name   string  optional    

Must not be greater than 191 characters. Example: n

contact_person   string  optional    

Primary contact person at a business party. Ignored for consumer contacts. The primary contact phone and email are the contact's own phone and email. Must not be greater than 128 characters. Example: Anita Rao

phone   string  optional    

Must not be greater than 32 characters. Example: g

email   string  optional    

Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

gst_treatment   string     

GST registration status. Use unregistered for an Indian business without GST registration and overseas for a contact outside India. Example: unregistered

Must be one of:
  • unregistered
  • registered_regular
  • registered_composition
  • consumer
  • overseas
  • sez
gstin   string  optional    

15-character Indian GSTIN. Required for registered_regular, registered_composition, and sez. Optional for an overseas contact registered under Indian GST; ignored for unregistered and consumer contacts. The same GSTIN may be used by multiple contacts. Must match the regex /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/. Must be 15 characters. Example: 29ABCDE1234F1Z5

pan   string  optional    

Optional Indian PAN. Ignored for consumer and overseas contacts. Must match the regex /^[A-Z]{5}[0-9]{4}[A-Z]$/. Must be 10 characters. Example: ABCDE1234F

country   string  optional    

Country where the overseas business is registered. Required when gst_treatment is overseas; ignored otherwise. Must not be greater than 96 characters. Example: United Arab Emirates

foreign_tax_id   string  optional    

Optional foreign Tax, VAT, or government-issued business identification number. Used only for overseas contacts. Must not be greater than 64 characters. Example: 100123456700003

latitude   number  optional    

Optional contact location latitude in decimal degrees, from -90 to 90. Send both latitude and longitude together. Omit both on update to preserve the location; send both as null to remove it. Mobile map pickers submit the selected coordinates here. This field is required when longitude is present. Must be between -90 and 90. Example: 17.448583

longitude   number  optional    

Optional contact location longitude in decimal degrees, from -180 to 180. Required with latitude; send both fields together, including when clearing them. This field is required when latitude is present. Must be between -180 and 180. Example: 78.390803

address   string  optional    

Must not be greater than 1000 characters. Example: d

billing_address_line_1   string  optional    

Must not be greater than 191 characters. Example: l

billing_address_line_2   string  optional    

Must not be greater than 191 characters. Example: j

billing_city   string  optional    

Must not be greater than 96 characters. Example: n

billing_state_code   string  optional    

Example: 1

Must be one of:
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 26
  • 27
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 96
  • 97
  • 99
billing_region   string  optional    

State, province, emirate, or other first-level region for an overseas billing address. Ignored for contacts in India. Must not be greater than 96 characters. Example: Dubai

billing_pincode   string  optional    

Must be 6 digits. Example: 569775

billing_postal_code   string  optional    

Postal or ZIP code for an overseas billing address. Ignored for contacts in India. Must not be greater than 32 characters. Example: SW1A 1AA

shipping_same_as_billing   boolean  optional    

Example: false

shipping_address_line_1   string  optional    

Must not be greater than 191 characters. Example: n

shipping_address_line_2   string  optional    

Must not be greater than 191 characters. Example: g

shipping_city   string  optional    

Must not be greater than 96 characters. Example: z

shipping_state_code   string  optional    

Example: 1

Must be one of:
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 26
  • 27
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 96
  • 97
  • 99
shipping_region   string  optional    

State, province, emirate, or other first-level region for an overseas shipping address. Ignored for contacts in India. Must not be greater than 96 characters. Example: Dubai

shipping_pincode   string  optional    

Must be 6 digits. Example: 569775

shipping_postal_code   string  optional    

Postal or ZIP code for an overseas shipping address. Ignored for contacts in India. Must not be greater than 32 characters. Example: SW1A 1AA

price_list_id   integer  optional    

Rate card this party buys at. Item lines raised for the party take their rate from it unless the request quotes one. Requires the party pricing feature; leave empty to bill at the item master rate. Example: 7

default_discount_percent   number  optional    

Blanket percentage off whatever rate the party would otherwise pay, applied after the rate card. Defaults to 0. Must be at least 0. Must not be greater than 100. Example: 2

credit_limit   number  optional    

Most this customer may owe at any moment, in rupees. Omit or send null for no limit; send 0 to put the party on cash only. Ignored for supplier contacts. A sales invoice or manual khata credit that would take the receivable balance past it is refused with HTTP 422. Must be at least 0. Must not be greater than 999999999. Example: 50000

opening_balance   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 22

opening_balance_side   string     

Example: receivable

Must be one of:
  • receivable
  • payable
opening_balance_date   string  optional    

Must be a valid date. Example: 2026-01-15

additional_contacts   object[]  optional    
name   string     

Must not be greater than 128 characters. Example: Suresh Gupta

designation   string  optional    

Must not be greater than 96 characters. Example: Accounts

phone   string  optional    

Must not be greater than 32 characters. Example: 9811111111

email   string  optional    

Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

photo   file  optional    

Optional private JPEG, PNG or WebP profile photo, up to 2 MB and 8192 pixels per side. Multipart upload. Omit to retain the saved photo; null does not remove it. Clients may crop before uploading. Must be an image. Must not be greater than 2048 kilobytes. Example: /path/to/file

remove_photo   boolean  optional    

Set true to remove the saved photo. Cannot be combined with a photo upload. Defaults to false. Example: false

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));

Request      

DELETE api/v1/businesses/{business}/contacts/{id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

id   integer     

The ID of the contact. Example: 1

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.
 

Request      

GET api/v1/businesses/{business}/contact-imports/template

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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));

Request      

POST api/v1/businesses/{business}/contact-imports

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
file   file     

The party list, .csv or .xlsx, up to 5 MB and 5,000 rows. The format is detected from the file's contents, not its name. Example: /path/to/file

duplicate_mode   string  optional    

What to do with a row naming a party the books already hold: skip, update, or create. Defaults to skip. Example: skip

column_map   object  optional    

Our field name to the column heading in your file, for a file whose headings cannot be guessed.

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."
}
 

Request      

GET api/v1/businesses/{business}/contact-imports/{contactImport_uuid}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

contactImport_uuid   string     

Example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed

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."
}
 

Request      

POST api/v1/businesses/{business}/contact-imports/{contactImport_uuid}/commit

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

contactImport_uuid   string     

Example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed

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."
}
 

Request      

DELETE api/v1/businesses/{business}/contact-imports/{contactImport_uuid}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

contactImport_uuid   string     

Example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed

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."
}
 

Request      

GET api/v1/businesses/{business}/ledger-entries

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
contact_id   integer  optional    

Example: 16

kind   string  optional    

Must not be greater than 32 characters. Example: n

from   string  optional    

Must be a valid date. Example: 2026-01-15

to   string  optional    

Must be a valid date. Must be a date after or equal to from. Example: 2026-01-15

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 22

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));

Request      

POST api/v1/businesses/{business}/ledger-entries

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
contact_id   integer     

Example: 16

kind   string     

Example: customer_credit

Must be one of:
  • customer_credit
  • customer_payment
  • supplier_credit
  • supplier_payment
amount   number     

Must not be greater than 999999999. Example: 22

occurred_on   string     

Must be a valid date. Example: 2026-01-15

reference   string  optional    

Must not be greater than 64 characters. Example: g

note   string  optional    

Must not be greater than 1000 characters. Example: z

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."
}
 

Request      

GET api/v1/businesses/{business}/ledger-entries/{ledgerEntry_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

ledgerEntry_id   integer     

The ID of the ledgerEntry. Example: 1

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));

Request      

POST api/v1/businesses/{business}/ledger-entries/{ledgerEntry_id}/reverse

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

ledgerEntry_id   integer     

The ID of the ledgerEntry. Example: 1

Body Parameters
reason   string     

Must be at least 3 characters. Must not be greater than 255 characters. Example: b

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."
}
 

Request      

GET api/v1/businesses/{business}/items/{item_id}/transactions

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

Query Parameters
limit   integer  optional    

Number of transactions to return, 1-100. Defaults to 25. Example: 10

Body Parameters
limit   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 1

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
    }
}
 

Request      

GET api/v1/businesses/{business}/items/{item_id}/ledger

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

Query Parameters
from   string  optional    

date Movements on or after this date. Example: 2026-09-01

to   string  optional    

date Movements on or before this date. Example: 2026-09-30

type   string  optional    

Narrow to one document type: sale, purchase, sales_return, purchase_return, delivery_challan, adjustment, opening, or transfer. Example: purchase

warehouse   integer  optional    

Only movements in this warehouse. Closing stock then becomes that warehouse's balance. Example: 1

sort   string  optional    

date (default), unit_cost, or total_cost. Example: date

direction   string  optional    

desc (default) or asc. Example: desc

per_page   integer  optional    

Rows per page, 1-100. Defaults to 25. Example: 25

page   integer  optional    

Page number. Example: 1

Body Parameters
per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 1

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."
        ]
    }
}
 

Request      

PATCH api/v1/businesses/{business}/items/{item_id}/opening-stock/{movement_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

movement_id   integer     

Opening stock movement ID from the item's ledger. Example: 1

Body Parameters
quantity   number     

Corrected opening quantity, with at most three decimal places. Zero removes the entry from the visible ledger. Must be at least 0. Must not be greater than 999999999. Example: 12.5

unit_cost   number     

Cost per opening unit in rupees, with at most two decimal places. The original date, warehouse, and batch are retained. Must be at least 0. Must not be greater than 999999999. Example: 80

reason   string     

Reason retained with the old and new values in the audit trail. Must not be greater than 500 characters. Example: Corrected the initial stock count

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
 

Request      

DELETE api/v1/businesses/{business}/items/{item_id}/opening-stock/{movement_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

movement_id   integer     

Opening stock movement ID. Example: 1

Body Parameters
reason   string     

Reason recorded in the audit trail, maximum 500 characters. Example: Opening stock was entered twice

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."
}
 

Request      

GET api/v1/businesses/{business}/items/{item_id}/suppliers

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/items/{item_id}/suppliers

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

Body Parameters
contact_id   integer     

Supplier or customer-and-supplier contact. Example: 17

supplier_sku   string  optional    

The supplier's own code for the item. Example: MC-TAP-01

purchase_price   number  optional    

Agreed rate in rupees. Example: 110.5

minimum_order_quantity   number  optional    

Minimum order in the item's unit. Example: 12

lead_time_days   integer  optional    

Days from order to delivery, 0-365. Example: 7

is_preferred   boolean  optional    

Use this supplier's rate when no supplier is chosen. Example: true

notes   string  optional    

Free-text notes, up to 1000 characters. Example: Delivers on Tuesdays.

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));

Request      

PATCH api/v1/businesses/{business}/items/{item_id}/suppliers/{itemSupplier_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

itemSupplier_id   integer     

The ID of the itemSupplier. Example: 1

Body Parameters
contact_id   integer  optional    

Must be at least 1. Example: 16

supplier_sku   string  optional    

Must not be greater than 64 characters. Example: n

purchase_price   number  optional    

Agreed rate in rupees. Example: 108

minimum_order_quantity   number  optional    

Must be at least 0. Must not be greater than 99999999999. Example: 16

lead_time_days   integer  optional    

Days from order to delivery, 0-365. Example: 5

is_preferred   boolean  optional    

Example: true

notes   string  optional    

Must not be greater than 1000 characters. Example: i

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));

Request      

DELETE api/v1/businesses/{business}/items/{item_id}/suppliers/{itemSupplier_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

itemSupplier_id   integer     

The ID of the itemSupplier. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/warehouses

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/warehouses

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
name   string     

Unique within the business, up to 100 characters. Example: Sanitary godown

location   string  optional    

Short location, up to 150 characters. Example: Gomaty Nagar

address   string  optional    

Optional address, up to 1000 characters. Example: 12 Station Road, Lucknow

is_default   boolean  optional    

Make this the default warehouse. Example: false

is_active   boolean  optional    

Example: false

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));

Request      

PUT api/v1/businesses/{business}/warehouses/{id}

PATCH api/v1/businesses/{business}/warehouses/{id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

id   integer     

The ID of the warehouse. Example: 1

Body Parameters
name   string  optional    

Must not be greater than 100 characters. Example: b

location   string  optional    

Must not be greater than 150 characters. Example: n

address   string  optional    

Must not be greater than 1000 characters. Example: g

is_default   boolean  optional    

Example: false

is_active   boolean  optional    

Example: false

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."
        ]
    }
}
 

Request      

DELETE api/v1/businesses/{business}/warehouses/{id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

id   integer     

The ID of the warehouse. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/items/{item_id}/stock-transfers

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

Body Parameters
from_warehouse_id   integer     

Active warehouse the stock leaves. Example: 1

to_warehouse_id   integer     

A different active warehouse. Example: 2

quantity   number     

More than 0, up to three decimals. Example: 5

transferred_on   date  optional    

Defaults to today; cannot be in the future. Example: 2026-09-29

note   string  optional    

Up to 255 characters. Example: Moved for the weekend sale

item_batch_id   integer  optional    

For a batch-tracked item, the one lot to move. Left out, lots in the source warehouse move earliest expiry first. Example: 3

serial_numbers   string[]  optional    

For a serial-tracked item, the units to move, one per unit of quantity. Left out, the oldest units in the source warehouse move.

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."
}
 

Request      

GET api/v1/businesses/{business}/items/{item_id}/batches

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

Query Parameters
in_stock   boolean  optional    

Only batches still holding stock. Example: true

sellable   boolean  optional    

Only batches that can be billed today: in stock, not expired, not on hold or recalled, and — when the business blocks short expiry — clear of the minimum shelf life. Example: true

Body Parameters
in_stock   boolean  optional    

Example: false

sellable   boolean  optional    

Example: false

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/items/{item_id}/batches

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

Body Parameters
warehouse_id   integer  optional    

Must be at least 1. Example: 16

batch_number   string     

Must not be greater than 64 characters. Example: n

expiry_date   string  optional    

Must be a valid date. Example: 2026-01-15

manufactured_on   string  optional    

Must be a valid date. Example: 2026-01-15

mrp   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 7

purchase_price   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 16

quantity   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 17

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."
        ]
    }
}
 

Request      

PATCH api/v1/businesses/{business}/items/{item_id}/batches/{itemBatch_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

itemBatch_id   integer     

The ID of the itemBatch. Example: 1

Body Parameters
warehouse_id   integer  optional    

Must be at least 1. Example: 16

batch_number   string  optional    

Must not be greater than 64 characters. Example: n

expiry_date   string  optional    

Must be a valid date. Example: 2026-01-15

manufactured_on   string  optional    

Must be a valid date. Example: 2026-01-15

mrp   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 7

purchase_price   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 16

quantity   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 17

status   string  optional    

available, on_hold, or recalled. Example: on_hold

status_reason   string  optional    

Why the lot is held or recalled, up to 255 characters. Required with on_hold or recalled. Example: Customer complaint, checking the carton

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));

Request      

DELETE api/v1/businesses/{business}/items/{item_id}/batches/{itemBatch_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

itemBatch_id   integer     

The ID of the itemBatch. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/items/{item_id}/batches/{itemBatch_id}/write-offs

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

itemBatch_id   integer     

The ID of the itemBatch. Example: 1

Body Parameters
quantity   number     

Quantity to remove, no more than the lot holds. Example: 4

reason   string     

Why: expired, damaged, recalled, or other. Example: expired

note   string  optional    

Free text kept with the movement, up to 255 characters. Example: Destroyed with the chemists' association

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."
}
 

Request      

GET api/v1/businesses/{business}/items/{item_id}/serials

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

Query Parameters
on_shelf   boolean  optional    

Only units that can be billed: in stock or returned. Example: true

status   string  optional    

One exact status: in_stock, sold, returned, or void. Example: sold

q   string  optional    

Match part of a serial number. Example: 3567

Body Parameters
on_shelf   boolean  optional    

Example: false

status   string  optional    
q   string  optional    

Must not be greater than 64 characters. Example: b

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."
}
 

Request      

GET api/v1/businesses/{business}/item-serials

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
q   string  optional    

Part or all of a serial number. Example: 356938

item_id   integer  optional    

Narrow to one item. Example: 12

status   string  optional    

One exact status: in_stock, sold, returned, or void. Example: sold

Body Parameters
q   string  optional    

Must not be greater than 64 characters. Example: b

item_id   integer  optional    

Example: 16

status   string  optional    

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"
}
 

Request      

GET api/v1/businesses/{business}/item-serials/{serial}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

serial   string     

The serial number as scanned. Example: 356938035643809

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/items/{item_id}/serials

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

Body Parameters
warehouse_id   integer  optional    

Must be at least 1. Example: 16

serial_numbers   string[]     

One per unit, up to 100 at a time.

warranty_until   date  optional    

Cover end date applied to every unit in this call. Example: 2027-09-13

received_on   string  optional    

Must be a valid date. Example: 2026-01-15

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));

Request      

PATCH api/v1/businesses/{business}/items/{item_id}/serials/{itemSerial_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

itemSerial_id   integer     

The ID of the itemSerial. Example: 1

Body Parameters
serial_number   string  optional    

Must not be greater than 64 characters. Example: b

warranty_until   string  optional    

Must be a valid date. Example: 2026-01-15

received_on   string  optional    

Must be a valid date. Example: 2026-01-15

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));

Request      

DELETE api/v1/businesses/{business}/items/{item_id}/serials/{itemSerial_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

itemSerial_id   integer     

The ID of the itemSerial. Example: 1

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"
}
 

Request      

GET api/v1/businesses/{business}/items/by-barcode/{code}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

code   string     

The barcode as scanned. Example: 8901234567890

Query Parameters
contact   integer  optional    

Customer id. Adds party_price, the rate that party pays under its rate card. Example: 42

Body Parameters
contact   integer  optional    

Must be at least 1. Example: 16

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."
}
 

Request      

GET api/v1/businesses/{business}/items/by-qr

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
code   string     

The QR payload as scanned — the whole URL, or just the token. Example: https://dukanam.com/q/7Fq2bXm9KdLp3RtVw8ZaCe

contact   integer  optional    

Customer id. Adds party_price, the rate that party pays under its rate card. Example: 42

Body Parameters
code   string     

Must not be greater than 2048 characters. Example: b

contact   integer  optional    

Must be at least 1. Example: 22

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."
        ]
    }
}
 

Request      

GET api/v1/businesses/{business}/items/barcode-labels

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
items   integer[]  optional    

Item ids to label. Required on the collection route, ignored when the URL already names an item. Maximum 200.

copies   integer  optional    

Copies of every label, 1-100. Send copies[<item id>] instead to set a count per item. Example: 10

layout   string  optional    

Stationery to lay the sheet out on: a4-65, a4-40, a4-24, roll-50x25, roll-38x25 or custom (one sticker per page). Defaults to a4-65. Example: a4-65

width_mm   number  optional    

Required for layout=custom. Sticker and page width in millimetres, 25-200. Ignored for presets. Example: 50

height_mm   number  optional    

Required for layout=custom. Sticker and page height in millimetres, 20-200. Must fit selected text plus a QR of at least 14mm or bars of at least 5mm; otherwise returns 422. Ignored for presets. Example: 30

code_kind   string  optional    

barcode to reprint the code the item already carries, or qr to print a Dukanam QR sticker. Defaults to barcode. A barcode run skips items holding no code; a QR run skips nothing, because the token is minted for the item. Example: qr

show_name   boolean  optional    

Print the item name. Defaults to true. Example: true

show_price   boolean  optional    

Print the selling price. Defaults to true. Example: true

show_mrp   boolean  optional    

Print the MRP when the item carries one. Defaults to false. Example: false

show_sku   boolean  optional    

Print the SKU. Defaults to false. Example: false

show_business   boolean  optional    

Print the shop name. Defaults to false. Example: false

format   string  optional    

json for per-item SVG, or pdf for the laid-out sheet. Defaults to json. Example: json

Body Parameters
items   integer[]  optional    

Must be at least 1.

copies   integer[]  optional    

Must be at least 1. Must not be greater than 100.

layout   string  optional    

Example: a4-65

Must be one of:
  • a4-65
  • a4-40
  • a4-24
  • roll-50x25
  • roll-38x25
  • custom
width_mm   number     

Must be between 25 and 200. Example: 25

height_mm   number     

Must be between 20 and 200. Example: 21

code_kind   string  optional    

Example: barcode

Must be one of:
  • barcode
  • qr
show_business   boolean  optional    

Example: false

show_name   boolean  optional    

Example: false

show_sku   boolean  optional    

Example: false

show_price   boolean  optional    

Example: false

show_mrp   boolean  optional    

Example: false

format   string  optional    

Example: json

Must be one of:
  • json
  • pdf
search   string  optional    

Must not be greater than 128 characters. Example: m

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."
        ]
    }
}
 

Request      

GET api/v1/businesses/{business}/items/{item_id}/barcode-labels

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

Query Parameters
items   integer[]  optional    

Item ids to label. Required on the collection route, ignored when the URL already names an item. Maximum 200.

copies   integer  optional    

Copies of every label, 1-100. Send copies[<item id>] instead to set a count per item. Example: 10

layout   string  optional    

Stationery to lay the sheet out on: a4-65, a4-40, a4-24, roll-50x25, roll-38x25 or custom (one sticker per page). Defaults to a4-65. Example: a4-65

width_mm   number  optional    

Required for layout=custom. Sticker and page width in millimetres, 25-200. Ignored for presets. Example: 50

height_mm   number  optional    

Required for layout=custom. Sticker and page height in millimetres, 20-200. Must fit selected text plus a QR of at least 14mm or bars of at least 5mm; otherwise returns 422. Ignored for presets. Example: 30

code_kind   string  optional    

barcode to reprint the code the item already carries, or qr to print a Dukanam QR sticker. Defaults to barcode. A barcode run skips items holding no code; a QR run skips nothing, because the token is minted for the item. Example: qr

show_name   boolean  optional    

Print the item name. Defaults to true. Example: true

show_price   boolean  optional    

Print the selling price. Defaults to true. Example: true

show_mrp   boolean  optional    

Print the MRP when the item carries one. Defaults to false. Example: false

show_sku   boolean  optional    

Print the SKU. Defaults to false. Example: false

show_business   boolean  optional    

Print the shop name. Defaults to false. Example: false

format   string  optional    

json for per-item SVG, or pdf for the laid-out sheet. Defaults to json. Example: json

Body Parameters
items   integer[]  optional    

Must be at least 1.

copies   integer[]  optional    

Must be at least 1. Must not be greater than 100.

layout   string  optional    

Example: a4-65

Must be one of:
  • a4-65
  • a4-40
  • a4-24
  • roll-50x25
  • roll-38x25
  • custom
width_mm   number     

Must be between 25 and 200. Example: 25

height_mm   number     

Must be between 20 and 200. Example: 21

code_kind   string  optional    

Example: barcode

Must be one of:
  • barcode
  • qr
show_business   boolean  optional    

Example: false

show_name   boolean  optional    

Example: false

show_sku   boolean  optional    

Example: false

show_price   boolean  optional    

Example: false

show_mrp   boolean  optional    

Example: false

format   string  optional    

Example: json

Must be one of:
  • json
  • pdf
search   string  optional    

Must not be greater than 128 characters. Example: m

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));

Request      

PATCH api/v1/businesses/{business}/items/{item_id}/active

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

item_id   integer     

The ID of the item. Example: 1

Body Parameters
is_active   boolean     

Whether the shop still offers this item. Example: false

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
    }
}
 

Request      

PATCH api/v1/businesses/{business}/items/organisation

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
item_ids   integer[]     

Up to 100 distinct item IDs owned by this workspace.

category_id   integer  optional    

Category ID, or null to clear it. Example: 1

subcategory_id   integer  optional    

Subcategory belonging to the selected category. Example: 2

brand_id   integer  optional    

Brand ID, or null to clear it. Example: 3

manufacturer_id   integer  optional    

Manufacturer ID, or null to clear it. Example: 4

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."
}
 

Request      

GET api/v1/businesses/{business}/items

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
category_id   integer  optional    

Filter by a tenant-owned category. Example: 1

subcategory_id   integer  optional    

Filter by a tenant-owned subcategory. Example: 2

brand_id   integer  optional    

Filter by a tenant-owned brand. Example: 3

manufacturer_id   integer  optional    

Filter by a tenant-owned manufacturer. Example: 4

gst_rate_id   integer  optional    

Filter by a tenant-owned GST percentage master. Example: 1

attention   string  optional    

low_stock, expiring (next 30 days), expired, uncategorised or warranty. Example: warranty

contact   integer  optional    

Customer id. Adds party_price to every item, showing what that party pays under its rate card. Example: 42

supplier   string  optional    

Supplier contact id, or preferred. Adds supplier_price to every item that supplier has an agreed or last-billed rate for, to open a purchase line at. Example: 17

status   string  optional    

Which items to list by state: all (the default) lists both, with inactive items last, active lists only items the shop still offers, and inactive lists only inactive ones. Use active when building a picker. Example: active

Body Parameters
search   string  optional    

Must not be greater than 128 characters. Example: b

low_stock   boolean  optional    

Example: false

status   string  optional    

Example: all

Must be one of:
  • all
  • active
  • inactive
contact   integer  optional    

Must be at least 1. Example: 22

supplier   string  optional    
per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 7

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/items

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
category_id   integer  optional    

Must match an existing stored value. Example: 16

category_name   string  optional    

Optional category to create or reuse. Example: Electrical

subcategory_id   integer  optional    

Must match an existing stored value. Example: 16

subcategory_name   string  optional    

Optional subcategory under the selected category. Example: LED lighting

brand_id   integer  optional    

Must match an existing stored value. Example: 16

brand_name   string  optional    

Optional brand to create or reuse. Example: Sample Brand

manufacturer_id   integer  optional    

Must match an existing stored value. Example: 16

manufacturer_name   string  optional    

Optional manufacturer to create or reuse. Example: Sample Manufacturer

gst_rate_id   integer  optional    

Optional business GST percentage master. Product tax metadata does not authorize collecting GST; unregistered/composition sales apply zero. Numeric inputs only create percentage masters for Regular GST businesses. Overrides tax_rate for taxable items; cross-business IDs return 422. Example: 1

warranty_duration   integer  optional    

Optional whole-number duration. Example: 6

warranty_unit   string  optional    

days, months or years; required with warranty_duration. Example: months

warranty_provider   string  optional    

brand, manufacturer, shop or both; required with warranty_duration. Example: shop

warranty_terms   string  optional    

Coverage conditions, at most 4000 characters. Example: Replacement for manufacturing defects.

warranty_provider_name   string  optional    

Support name, required for new/changed external warranties. Example: Sample Brand Support

warranty_provider_phone   string  optional    

Support phone; at least one phone/email/website is required for external cover. Example: +91 1800 123 4567

warranty_provider_email   string  optional    

Support email, at most 254 characters. Example: [email protected]

warranty_provider_website   string  optional    

HTTP(S) support URL, at most 2048 characters. Example: https://example.test/support

warranty_provider_address   string  optional    

Optional service address, at most 1000 characters. Example: Example Service Centre, Hyderabad

warranty_provider_notes   string  optional    

Optional claim instructions, at most 2000 characters. Example: Keep your invoice and product serial number ready.

requires_expiry   boolean  optional    

Require expiry on received stock lots. Example: false

name   string     

Must not be greater than 128 characters. Example: k

sku   string  optional    

Must not be greater than 64 characters. Example: h

barcode   string  optional    

Must not be greater than 64 characters. Example: w

item_type   string     

Example: goods

Must be one of:
  • goods
  • service
hsn_sac   string  optional    

Must not be greater than 8 characters. Example: aykcmyuw

unit   string     

Must not be greater than 24 characters. Example: pwlvqwrsitcpscql

uqc   string     

Must not be greater than 16 characters. Example: dzsnrwtujwvlxjkl

gst_taxability   string     

Example: taxable

Must be one of:
  • taxable
  • nil_rated
  • exempt
  • non_gst
sale_price   number     

Must be at least 0. Must not be greater than 999999999. Example: 8

purchase_price   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 10

mrp   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 3

tax_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 14

cess_rate   number  optional    

Optional product cess percentage, from 0 to 100; omitted updates preserve the current item value. Unregistered/composition sales apply zero cess. Example: 0

stock_quantity   number     

Must be at least 0. Must not be greater than 999999999. Example: 4

warehouse_id   integer  optional    

Must be at least 1. Example: 35

reorder_level   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 9

is_active   boolean  optional    

Example: false

track_inventory   boolean  optional    

Example: false

track_batches   boolean  optional    

Example: false

min_shelf_life_days   integer  optional    

Must be at least 0. Must not be greater than 730. Example: 6

track_serials   boolean  optional    

Example: false

opening_batch_number   string  optional    

Must not be greater than 64 characters. Example: n

opening_batch_expiry_date   string  optional    

Must be a valid date. Example: 2026-01-15

opening_serial_numbers   string[]  optional    

Must not be greater than 64 characters.

opening_warranty_until   string  optional    

Must be a valid date. Example: 2026-01-15

price_includes_tax   boolean  optional    

Example: false

photos   file[]  optional    

Must be an image. Must not be greater than 5120 kilobytes.

remove_photos   integer[]  optional    

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."
}
 

Request      

GET api/v1/businesses/{business}/items/{id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

id   integer     

The ID of the item. Example: 1

Query Parameters
contact   integer  optional    

Customer id. Adds party_price to the item, showing what that party pays under its rate card. Example: 42

supplier   string  optional    

Supplier contact id, or preferred. Adds supplier_price, the rate to open a purchase line at. Example: 17

Body Parameters
contact   integer  optional    

Must be at least 1. Example: 16

supplier   string  optional    

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));

Request      

PUT api/v1/businesses/{business}/items/{id}

PATCH api/v1/businesses/{business}/items/{id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

id   integer     

The ID of the item. Example: 1

Body Parameters
category_id   integer  optional    

Must match an existing stored value. Example: 16

category_name   string  optional    

Must not be greater than 100 characters. Example: n

subcategory_id   integer  optional    

Must match an existing stored value. Example: 16

subcategory_name   string  optional    

Must not be greater than 100 characters. Example: n

brand_id   integer  optional    

Must match an existing stored value. Example: 16

brand_name   string  optional    

Must not be greater than 100 characters. Example: n

manufacturer_id   integer  optional    

Must match an existing stored value. Example: 16

manufacturer_name   string  optional    

Must not be greater than 100 characters. Example: n

gst_rate_id   integer  optional    

Must match an existing stored value. Example: 16

warranty_duration   integer  optional    

Must be at least 1. Must not be greater than 3650. Example: 22

warranty_unit   string  optional    

This field is required when warranty_duration is present. Example: days

Must be one of:
  • days
  • months
  • years
warranty_provider   string  optional    

This field is required when warranty_duration is present. Example: brand

Must be one of:
  • brand
  • manufacturer
  • shop
  • both
warranty_terms   string  optional    

Must not be greater than 4000 characters. Example: g

warranty_provider_name   string  optional    

Must not be greater than 160 characters. Example: z

warranty_provider_phone   string  optional    

Must match the regex /^[+0-9][0-9\s().-xX#]{4,49}$/. Must not be greater than 50 characters. Example: m

warranty_provider_email   string  optional    

Must be a valid email address. Must not be greater than 254 characters. Example: [email protected]

warranty_provider_website   string  optional    

Must be a valid URL. Must not be greater than 2048 characters. Example: j

warranty_provider_address   string  optional    

Must not be greater than 1000 characters. Example: n

warranty_provider_notes   string  optional    

Must not be greater than 2000 characters. Example: i

requires_expiry   boolean  optional    

Example: false

name   string     

Must not be greater than 128 characters. Example: k

sku   string  optional    

Must not be greater than 64 characters. Example: h

barcode   string  optional    

Must not be greater than 64 characters. Example: w

item_type   string     

Example: goods

Must be one of:
  • goods
  • service
hsn_sac   string  optional    

Must not be greater than 8 characters. Example: aykcmyuw

unit   string     

Must not be greater than 24 characters. Example: pwlvqwrsitcpscql

uqc   string     

Must not be greater than 16 characters. Example: dzsnrwtujwvlxjkl

gst_taxability   string     

Example: taxable

Must be one of:
  • taxable
  • nil_rated
  • exempt
  • non_gst
sale_price   number     

Must be at least 0. Must not be greater than 999999999. Example: 8

purchase_price   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 10

mrp   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 3

tax_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 14

cess_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 7

stock_quantity   number     

Must be at least 0. Must not be greater than 999999999. Example: 4

warehouse_id   integer  optional    

Must be at least 1. Example: 35

reorder_level   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 9

is_active   boolean  optional    

Example: false

track_inventory   boolean  optional    

Example: false

track_batches   boolean  optional    

Example: false

min_shelf_life_days   integer  optional    

Must be at least 0. Must not be greater than 730. Example: 6

track_serials   boolean  optional    

Example: false

opening_batch_number   string  optional    

Must not be greater than 64 characters. Example: n

opening_batch_expiry_date   string  optional    

Must be a valid date. Example: 2026-01-15

opening_serial_numbers   string[]  optional    

Must not be greater than 64 characters.

opening_warranty_until   string  optional    

Must be a valid date. Example: 2026-01-15

price_includes_tax   boolean  optional    

Example: false

photos   file[]  optional    

Must be an image. Must not be greater than 5120 kilobytes.

remove_photos   integer[]  optional    

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"
}
 

Request      

DELETE api/v1/businesses/{business}/items/{id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

id   integer     

The ID of the item. Example: 1

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%"
        }
    ]
}
 

Request      

GET api/v1/businesses/{business}/gst-rates

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/gst-rates

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
percentage   number     

Percentage from 0 to 100, at most two decimal places. Example: 18

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."
        ]
    }
}
 

Request      

PATCH api/v1/businesses/{business}/gst-rates/{gstRate_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

gstRate_id   integer     

The ID of the gstRate. Example: 1

Body Parameters
percentage   number     

Percentage from 0 to 100, at most two decimal places. Example: 12

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."
        ]
    }
}
 

Request      

DELETE api/v1/businesses/{business}/gst-rates/{gstRate_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

gstRate_id   integer     

The ID of the gstRate. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/item-classifications

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
kind   string  optional    

Optional classification kind. Example: category

parent_id   integer  optional    

Optional parent category for subcategory lookups; must belong to this business. Example: 1

Body Parameters
kind   string  optional    
parent_id   integer  optional    

Example: 16

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));

Request      

POST api/v1/businesses/{business}/item-classifications

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
kind   string     

category, subcategory, brand or manufacturer. Example: category

name   string     

Display name, at most 100 characters. Example: Lighting

parent_id   integer  optional    

Parent category for a subcategory. Example: 1

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));

Request      

PATCH api/v1/businesses/{business}/item-classifications/{itemClassification_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

itemClassification_id   integer     

The ID of the itemClassification. Example: 1

Body Parameters
name   string     

New display name, at most 100 characters. Example: Electrical lighting

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
 

Request      

DELETE api/v1/businesses/{business}/item-classifications/{itemClassification_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

itemClassification_id   integer     

The ID of the itemClassification. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/item-warranties

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
search   string  optional    

Search item, buyer, invoice or serial number. Example: LED

item_id   integer  optional    

Filter one item. Example: 1

contact_id   integer  optional    

Filter one customer. Example: 1

per_page   integer  optional    

Page size, 1-100. Example: 25

Body Parameters
search   string  optional    

Must not be greater than 128 characters. Example: b

item_id   integer  optional    

Must be at least 1. Example: 22

contact_id   integer  optional    

Must be at least 1. Example: 67

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 16

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."
}
 

Request      

GET api/v1/businesses/{business}/item-warranties/{itemWarranty_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

itemWarranty_id   integer     

The ID of the itemWarranty. Example: 1

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.
 

Request      

GET api/v1/businesses/{business}/item-imports/template

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/item-imports

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
file   file     

The product workbook, .xlsx, up to 5 MB and 2,000 rows. Example: /path/to/file

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."
}
 

Request      

GET api/v1/businesses/{business}/item-imports/{itemImport_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

itemImport_id   integer     

The ID of the itemImport. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/item-imports/{itemImport_id}/confirm

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

itemImport_id   integer     

The ID of the itemImport. Example: 1

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."
}
 

Request      

DELETE api/v1/businesses/{business}/item-imports/{itemImport_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

itemImport_id   integer     

The ID of the itemImport. Example: 1

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
 

Request      

GET api/v1/businesses/{business}/invoices/{invoice_id}/warranty.pdf

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

invoice_id   integer     

The ID of the invoice. Example: 1

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
    }
}
 

Request      

POST api/v1/businesses/{business}/invoices/{invoice_id}/warranty/send

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

invoice_id   integer     

The ID of the invoice. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/invoices

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
status   string  optional    

Filter by status. saved aliases saved. Example: saved

outstanding   boolean  optional    

Only non-void invoices with a positive balance after payments and returns. Filtered before pagination. Example: true

Body Parameters
status   string  optional    

Example: saved

Must be one of:
  • saved
  • posted
  • partially_paid
  • paid
  • void
  • partially_returned
  • returned
outstanding   boolean  optional    

Example: false

contact_id   integer  optional    

Example: 16

from   string  optional    

Must be a valid date. Example: 2026-01-15

to   string  optional    

Must be a valid date. Must be a date after or equal to from. Example: 2026-01-15

search   string  optional    

Must not be greater than 128 characters. Example: n

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 7

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/invoices

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
line_layout   object[]  optional    

Optional display rows. Empty or null uses the line array order. On update, omission retains the layout when the line count is unchanged.

type   string     

item, header, or subtotal. Example: item

line_index   integer  optional    

Zero-based index in lines, required for item rows. Every line must appear exactly once. Example: 0

title   string  optional    

Heading required for header rows, maximum 100 characters. Example: Kitchen essentials

show_section_totals   boolean  optional    

Show calculated header totals on document output; defaults true. Example: true

workers   object[]  optional    

Optional complete multi-worker assignment set with commission_kind, fixed_amount_paise or rate_basis_points, and owner-entered attribution_basis_points totalling 10000. Requires Advanced Plan and owner/admin. Earnings commit atomically with the invoice.

worker_id   integer     

Must be at least 1. Example: 66

commission_kind   string     

Example: percentage

Must be one of:
  • percentage
  • fixed
rate_basis_points   integer  optional    

Must be between 0 and 10000. Example: 0

fixed_amount_paise   integer  optional    

Must be between 0 and 99999999900. Example: 0

attribution_basis_points   integer     

Must be between 1 and 10000. Example: 1

worker_attribution_basis_points   integer  optional    

Required with worker_id; owner-entered single-worker share must be 10000.

worker_id   integer  optional    

Optional referral worker. Advanced Plan and owner/administrator required; commission is earned atomically when this sale is saved.

worker_rate_basis_points   integer  optional    

Optional rate override from 0 to 10000; omitted uses the worker default. Earnings use discounted value before tax, with automatic return debit notes.

contact_id   integer     

Example: 16

issue_date   string     

Must be a valid date. Example: 2026-01-15

due_date   string  optional    

Must be a valid date. Must be a date after or equal to issue_date. Example: 2026-01-15

notes   string  optional    

Must not be greater than 2000 characters. Example: n

place_of_supply_state_code   string  optional    

Two-digit GST state code, or 96 for an export. 99 is refused. Defaults from the customer, then the business. Example: 29

reverse_charge   boolean  optional    

Example: false

prices_include_tax   boolean  optional    

Example: false

channel   string  optional    

Example: backoffice

Must be one of:
  • backoffice
  • pos
idempotency_key   string  optional    

Must be a valid UUID. Example: 6b72fe4a-5b40-307c-bc24-f79acf9a1bb9

lines   object[]     

Must have at least 1 items. Must not have more than 50 items.

details   string  optional    

Optional additional description, maximum 2000 characters. Example: Deliver in sealed packs.

item_id   integer  optional    

Example: 16

warehouse_id   integer  optional    

Must be at least 1. Example: 22

item_batch_id   integer  optional    

Example: 16

serial_numbers   string[]  optional    

Must not be greater than 64 characters.

description   string     

Must not be greater than 255 characters. Example: Animi quos velit et fugiat.

hsn_sac   string  optional    

HSN (4, 6 or 8 digits) or SAC (6 digits) for this line. Dots and spaces are stripped. Defaults to the item's code on an item line. Example: 100630

quantity   number     

Must not be greater than 999999. Example: 18

unit_price   number  optional    

This field is required when lines.*.item_id is not present. Must be at least 0. Must not be greater than 999999999. Example: 22

tax_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 24

cess_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 18

discount   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 8

price_includes_tax   boolean  optional    

Example: false

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/invoices/payments

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
payments   object[]     

Must have at least 1 items. Must not have more than 25 items.

invoice_id   integer     

Example: 16

amount   number     

Must not be greater than 999999999. Example: 22

method   string     

Example: cash

Must be one of:
  • cash
  • bank
  • upi
  • card
  • cheque
  • other
payment_account_id   integer     

Example: 16

paid_on   string  optional    

Must be a valid date. Example: 2026-01-15

reference   string  optional    

Must not be greater than 64 characters. Example: n

notes   string  optional    

Must not be greater than 1000 characters. Example: g

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/invoices/void

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
invoices   object[]     

Must have at least 1 items. Must not have more than 25 items.

invoice_id   integer     

Example: 16

reason   string  optional    

This field is required when reason is not present. Must be at least 3 characters. Must not be greater than 255 characters. Example: n

reason   string  optional    

Must be at least 3 characters. Must not be greater than 255 characters. Example: b

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]"
                }
            ]
        }
    }
}
 

Request      

GET api/v1/businesses/{business}/invoices/{invoice_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

invoice_id   integer     

The ID of the invoice. Example: 1

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."
        ]
    }
}
 

Request      

PUT api/v1/businesses/{business}/invoices/{invoice_id}

PATCH api/v1/businesses/{business}/invoices/{invoice_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

invoice_id   integer     

The ID of the invoice. Example: 1

Body Parameters
line_layout   object[]  optional    

Optional display rows. Empty or null uses the line array order. On update, omission retains the layout when the line count is unchanged.

type   string     

item, header, or subtotal. Example: item

line_index   integer  optional    

Zero-based index in lines, required for item rows. Every line must appear exactly once. Example: 0

title   string  optional    

Heading required for header rows, maximum 100 characters. Example: Kitchen essentials

show_section_totals   boolean  optional    

Show calculated header totals on document output; defaults true. Example: true

workers   object[]  optional    

Must not have more than 10 items.

worker_id   integer     

Must be at least 1. Example: 27

commission_kind   string     

Example: percentage

Must be one of:
  • percentage
  • fixed
rate_basis_points   integer  optional    

Must be between 0 and 10000. Example: 0

fixed_amount_paise   integer  optional    

Must be between 0 and 99999999900. Example: 0

attribution_basis_points   integer     

Must be between 1 and 10000. Example: 2

worker_attribution_basis_points   integer  optional    

This field is required when worker_id is present. Example: 10000

Must be one of:
  • 10000
worker_id   integer  optional    

This field is required when worker_rate_basis_points is present. Must be at least 1. Example: 16

worker_rate_basis_points   integer  optional    

Must be between 0 and 10000. Example: 1

contact_id   integer     

Example: 16

issue_date   string     

Must be a valid date. Example: 2026-01-15

due_date   string  optional    

Must be a valid date. Must be a date after or equal to issue_date. Example: 2026-01-15

notes   string  optional    

Must not be greater than 2000 characters. Example: n

place_of_supply_state_code   string  optional    

Example: 1

Must be one of:
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 26
  • 27
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 96
  • 97
reverse_charge   boolean  optional    

Example: false

prices_include_tax   boolean  optional    

Example: false

lines   object[]     

Must have at least 1 items. Must not have more than 50 items.

details   string  optional    

Optional additional description, maximum 2000 characters. Example: Deliver in sealed packs.

item_id   integer  optional    

Example: 16

warehouse_id   integer  optional    

Must be at least 1. Example: 22

item_batch_id   integer  optional    

Example: 16

serial_numbers   string[]  optional    

Must not be greater than 64 characters.

description   string     

Must not be greater than 255 characters. Example: Animi quos velit et fugiat.

hsn_sac   string  optional    

HSN or SAC for this line; omit to keep the line's current code. Example: 100630

quantity   number     

Must not be greater than 999999. Example: 18

unit_price   number  optional    

This field is required when lines.*.item_id is not present. Must be at least 0. Must not be greater than 999999999. Example: 22

tax_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 24

cess_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 18

discount   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 8

price_includes_tax   boolean  optional    

Example: false

id   integer  optional    

Existing line identifier belonging to this invoice; omit for a new line. Values must be distinct. Example: 1

revision   integer     

Current invoice revision from GET; prevents overwriting another edit. Example: 1

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.
 

Request      

GET api/v1/businesses/{business}/invoices/{invoice_id}/pdf

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

invoice_id   integer     

The ID of the invoice. Example: 1

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."
        ]
    }
}
 

Request      

GET api/v1/businesses/{business}/invoices/{invoice_id}/receipt

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

invoice_id   integer     

The ID of the invoice. Example: 1

Query Parameters
cash_register_id   integer  optional    

Counter the receipt prints at. Its saved printer settings (paper, cut, cash drawer) apply. Omit to use the defaults: 80mm, cut, drawer opens for cash sales. Must match an existing stored value. Example: 1

paper   string  optional    

Overrides the counter's paper width: 58mm (32 characters a line) or 80mm (48 characters a line). Example: 58mm

Must be one of:
  • 58mm
  • 80mm
cut   boolean  optional    

Overrides whether the paper is cut at the end: 1 or 0. Example: false

open_drawer   boolean  optional    

Overrides whether the cash drawer opens: 1 always opens it, 0 never does. When omitted, the drawer opens only if the counter allows it and the sale has a cash payment. Example: false

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/invoices/{invoice_id}/email

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

invoice_id   integer     

The ID of the invoice. Example: 1

Body Parameters
recipients   string[]     

A recipient key: primary for the party's primary contact, or person-{id} for an additional contact. Must match the regex /^(primary|person-\d+)$/.

recipient_emails   string[]  optional    

Must be a valid email address. Must not be greater than 254 characters.

subject   string     

Email subject line. Must not be greater than 150 characters. Example: Sri Lakshmi Traders | Purchase order: PO-0007

message   string     

Message body. Blank lines separate paragraphs. The PDF is attached and a button links to the signed public page. Must not be greater than 2000 characters. Example: Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.

from   string  optional    

Ledger statements only: start date, inclusive. Must be a valid date. Example: 2026-04-01

to   string  optional    

Ledger statements only: end date, inclusive. Must be on or after from. Must be a valid date. Must be a date after or equal to from. Example: 2027-03-31

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/invoices/{invoice_id}/payments

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

invoice_id   integer     

The ID of the invoice. Example: 1

Body Parameters
payments   object[]     

Must have at least 1 items. Must not have more than 25 items.

invoice_id   integer     

Example: 16

amount   number     

Must not be greater than 999999999. Example: 22

method   string     

Example: cash

Must be one of:
  • cash
  • bank
  • upi
  • card
  • cheque
  • other
payment_account_id   integer     

Example: 16

paid_on   string  optional    

Must be a valid date. Example: 2026-01-15

reference   string  optional    

Must not be greater than 64 characters. Example: n

notes   string  optional    

Must not be greater than 1000 characters. Example: g

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/invoices/{invoice_id}/void

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

invoice_id   integer     

The ID of the invoice. Example: 1

Body Parameters
invoices   object[]     

Must have at least 1 items. Must not have more than 25 items.

invoice_id   integer     

Example: 16

reason   string  optional    

This field is required when reason is not present. Must be at least 3 characters. Must not be greater than 255 characters. Example: n

reason   string  optional    

Must be at least 3 characters. Must not be greater than 255 characters. Example: b

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."
}
 

Request      

GET api/v1/businesses/{business}/documents/{document}/recurring-settings

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

document   string     

The document. Example: architecto

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));

Request      

PUT api/v1/businesses/{business}/documents/{document}/recurring-settings

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

document   string     

The document. Example: architecto

Body Parameters
frequency   string     

weekly, monthly, quarterly or yearly. Example: monthly

next_issue_date   date     

Next invoice date in business timezone. Example: 2026-10-31

end_date   date  optional    

Nullable inclusive last date. Example: 2027-10-31

repeat_every   integer  optional    

Repeat every 1–52 selected periods. Example: 1

due_after_days   integer  optional    

Payment due 0–365 days after creation date. Example: 7

auto_email   boolean  optional    

Automatically email generated invoices to selected customer contacts. Example: true

email_recipients   string[]  optional    

Selected primary or person-ID keys belonging to this customer.

recipient_emails   object  optional    

Missing emails to save for selected recipients.

notify_owner   boolean  optional    

Notify owner separately; defaults true. Example: true

owner_email   string  optional    

Nullable notification override, not a change to account/login email. Example: [email protected]

schedule_status   string  optional    

active or paused. Example: active

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."
}
 

Request      

POST api/v1/businesses/{business}/documents/{document}/recurring-runs/{run}/retry

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

document   string     

The document. Example: architecto

run   string     

Example: architecto

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."
}
 

Request      

GET api/v1/businesses/{business}/documents

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
type   string     

Example: quote

Must be one of:
  • quote
  • purchase_order
  • purchase_invoice
  • recurring_invoice
  • sales_return
  • purchase_return
  • delivery_challan
status   string  optional    

Must not be greater than 32 characters. Example: b

contact_id   integer  optional    

Example: 16

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 22

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));

Request      

POST api/v1/businesses/{business}/documents/{document_type}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

document_type   string     

Example: quote|purchase_order|purchase_invoice|recurring_invoice|sales_return|purchase_return|delivery_challan

Body Parameters
line_layout   object[]  optional    

Optional display rows. Empty or null uses the line array order. On update, omission retains the layout when the line count is unchanged.

type   string     

item, header, or subtotal. Example: item

line_index   integer  optional    

Zero-based index in lines, required for item rows. Every line must appear exactly once. Example: 0

title   string  optional    

Heading required for header rows, maximum 100 characters. Example: Kitchen essentials

show_section_totals   boolean  optional    

Show calculated header totals on document output; defaults true. Example: true

contact_id   integer     

Example: 16

source_invoice_id   integer  optional    

Example: 16

source_document_id   integer  optional    

Example: 16

issue_date   string     

Must be a valid date. Example: 2026-01-15

due_date   string  optional    

Must be a valid date. Must be a date after or equal to issue_date. Example: 2026-01-15

frequency   string  optional    

Example: weekly

Must be one of:
  • weekly
  • monthly
  • quarterly
  • yearly
next_issue_date   string  optional    

Must be a valid date. Example: 2026-01-15

end_date   string  optional    

Must be a valid date. Must be a date after or equal to next_issue_date. Example: 2026-01-15

notes   string  optional    

Must not be greater than 2000 characters. Example: n

external_reference   string  optional    

Must not be greater than 64 characters. Example: g

challan_reason   string  optional    

Example: job_work

Must be one of:
  • job_work
  • branch_transfer
  • approval
  • line_sale
  • other
destination_warehouse_id   integer  optional    

Must be at least 1. Example: 66

expected_return_on   string  optional    

Must be a valid date. Must be a date after or equal to issue_date. Example: 2026-01-15

place_of_supply_state_code   string  optional    

Two-digit GST state code, or 96 for an export. 99 is refused. Example: 29

reverse_charge   boolean  optional    

Example: false

prices_include_tax   boolean  optional    

Example: false

lines   object[]     

Must have at least 1 items. Must not have more than 100 items.

details   string  optional    

Optional additional description, maximum 2000 characters. Example: Deliver in sealed packs.

item_id   integer  optional    

Example: 16

warehouse_id   integer  optional    

Must be at least 1. Example: 22

item_batch_id   integer  optional    

Example: 16

batch_number   string  optional    

Must not be greater than 64 characters. Example: n

batch_expiry_date   string  optional    

Must be a valid date. Example: 2026-01-15

batch_manufactured_on   string  optional    

Must be a valid date. Example: 2026-01-15

batch_mrp   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 7

serial_numbers   string[]  optional    

Must not be greater than 64 characters.

warranty_until   string  optional    

Must be a valid date. Example: 2026-01-15

source_invoice_line_id   integer  optional    

Example: 16

source_document_line_id   integer  optional    

Example: 16

description   string     

Must not be greater than 255 characters. Example: Et animi quos velit et fugiat.

hsn_sac   string  optional    

HSN (4, 6 or 8 digits) or SAC (6 digits) for this line; defaults to the item's code. Example: `998719

Recurring schedules support the options below, with the same validation as the recurring-settings endpoint. Owner notification defaults on; without an owner email, history remains available and invoice creation continues. Customer auto-email defaults off.`

quantity   number     

Must not be greater than 999999. Example: 18

unit_price   number     

Must be at least 0. Must not be greater than 999999999. Example: 22

tax_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 24

cess_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 18

discount   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 8

price_includes_tax   boolean  optional    

Example: false

repeat_every   integer  optional    

Recurring only: every 1–52 selected periods. Example: 1

due_after_days   integer  optional    

Recurring only: 0–365 days after issue. Example: 7

auto_email   boolean  optional    

Recurring only: automatically email generated invoices. Example: false

email_recipients   string[]  optional    

Recurring only: primary or person-ID keys on this customer.

recipient_emails   object  optional    

Recurring only: missing emails to save for selected contacts.

notify_owner   boolean  optional    

Recurring only: separate owner notification, default true. Example: true

owner_email   string  optional    

Recurring only: nullable notification address; defaults to owner account email. Example: [email protected]

schedule_status   string  optional    

Recurring only: active or paused. Example: active

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."
}
 

Request      

GET api/v1/businesses/{business}/documents/{document_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

document_id   integer     

The ID of the document. Example: 1

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));

Request      

PATCH api/v1/businesses/{business}/documents/{document_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

document_id   integer     

The ID of the document. Example: 1

Body Parameters
line_layout   object[]  optional    

Optional display rows. Empty or null uses the line array order. On update, omission retains the layout when the line count is unchanged.

type   string     

item, header, or subtotal. Example: item

line_index   integer  optional    

Zero-based index in lines, required for item rows. Every line must appear exactly once. Example: 0

title   string  optional    

Heading required for header rows, maximum 100 characters. Example: Kitchen essentials

show_section_totals   boolean  optional    

Show calculated header totals on document output; defaults true. Example: true

contact_id   integer     

Example: 16

issue_date   string     

Must be a valid date. Example: 2026-01-15

due_date   string  optional    

Must be a valid date. Must be a date after or equal to issue_date. Example: 2026-01-15

notes   string  optional    

Must not be greater than 2000 characters. Example: n

external_reference   string  optional    

Must not be greater than 64 characters. Example: g

challan_reason   string  optional    

Example: job_work

Must be one of:
  • job_work
  • branch_transfer
  • approval
  • line_sale
  • other
destination_warehouse_id   integer  optional    

Must be at least 1. Example: 66

expected_return_on   string  optional    

Must be a valid date. Must be a date after or equal to issue_date. Example: 2026-01-15

place_of_supply_state_code   string  optional    

Example: 1

Must be one of:
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 26
  • 27
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 96
  • 97
reverse_charge   boolean  optional    

Example: false

prices_include_tax   boolean  optional    

Example: false

lines   object[]     

Must have at least 1 items. Must not have more than 100 items.

details   string  optional    

Optional additional description, maximum 2000 characters. Example: Deliver in sealed packs.

item_id   integer  optional    

Example: 16

warehouse_id   integer  optional    

Must be at least 1. Example: 22

item_batch_id   integer  optional    

Example: 16

serial_numbers   string[]  optional    

Must not be greater than 64 characters.

description   string     

Must not be greater than 255 characters. Example: Animi quos velit et fugiat.

hsn_sac   string  optional    

Must match the regex /^[0-9]{4}(?:[0-9]{2}){0,2}$/. Example: 5593:14):23)

quantity   number     

Must not be greater than 999999. Example: 18

unit_price   number     

Must be at least 0. Must not be greater than 999999999. Example: 22

tax_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 24

cess_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 18

discount   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 8

price_includes_tax   boolean  optional    

Example: false

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."
        ]
    }
}
 

Request      

DELETE api/v1/businesses/{business}/documents/{document_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

document_id   integer     

The ID of the document. Example: 1

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));

Request      

POST api/v1/businesses/{business}/documents/{document_id}/convert

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

document_id   integer     

The ID of the document. Example: 1

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.
 

Request      

GET api/v1/businesses/{business}/documents/{document_id}/pdf

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

document_id   integer     

The ID of the document. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/documents/{document_id}/email

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

document_id   integer     

The ID of the document. Example: 1

Body Parameters
recipients   string[]     

A recipient key: primary for the party's primary contact, or person-{id} for an additional contact. Must match the regex /^(primary|person-\d+)$/.

recipient_emails   string[]  optional    

Must be a valid email address. Must not be greater than 254 characters.

subject   string     

Email subject line. Must not be greater than 150 characters. Example: Sri Lakshmi Traders | Purchase order: PO-0007

message   string     

Message body. Blank lines separate paragraphs. The PDF is attached and a button links to the signed public page. Must not be greater than 2000 characters. Example: Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.

from   string  optional    

Ledger statements only: start date, inclusive. Must be a valid date. Example: 2026-04-01

to   string  optional    

Ledger statements only: end date, inclusive. Must be on or after from. Must be a valid date. Must be a date after or equal to from. Example: 2027-03-31

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business_id}/documents/{document_id}/attachments

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters
business_id   integer     

The ID of the business. Example: 1

document_id   integer     

The ID of the document. Example: 1

Body Parameters
file   file     

Supplier invoice PDF, JPEG or PNG, up to 10240 KB. Example: /path/to/file

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.
 

Request      

GET api/v1/businesses/{business_id}/documents/{document_id}/attachments/{attachment_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

The ID of the business. Example: 1

document_id   integer     

The ID of the document. Example: 1

attachment_id   integer     

The ID of the attachment. Example: 1

Query Parameters
preview   boolean  optional    

Use 1 to display the PDF or image inline instead of downloading it. Authentication and tenant checks still apply. Example: true

Body Parameters
preview   boolean  optional    

Example: false

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
 

Request      

DELETE api/v1/businesses/{business_id}/documents/{document_id}/attachments/{attachment_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

The ID of the business. Example: 1

document_id   integer     

The ID of the document. Example: 1

attachment_id   integer     

The ID of the attachment. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/payments

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
direction   string     

received for customer payments, made for supplier payments. Example: received

from   string  optional    

Earliest payment date. Example: 2026-09-01

to   string  optional    

Latest payment date. Example: 2026-09-30

contact_id   integer  optional    

Only this customer's or supplier's payments. Example: 42

is_advance   boolean  optional    

1 for advances only, 0 to leave advances out. Example: true

unapplied   boolean  optional    

1 for advances that still have money left to apply. Example: true

per_page   integer  optional    

Page size, up to 100. Example: 20

Body Parameters
direction   string     

Example: received

Must be one of:
  • received
  • made
from   string  optional    

Must be a valid date. Example: 2026-01-15

to   string  optional    

Must be a valid date. Must be a date after or equal to from. Example: 2026-01-15

contact_id   integer  optional    

Example: 16

is_advance   boolean  optional    

Example: false

unapplied   boolean  optional    

Example: false

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 22

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/payments

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
is_advance   boolean  optional    

Record an advance against the party rather than a payment against a bill. Example: false

contact_id   integer  optional    

The customer (received) or supplier (made) who paid or was paid the advance. Required when is_advance is true; ignored otherwise. Example: 42

invoice_id   integer  optional    

The sales invoice being settled. Required for a received payment that is not an advance. Example: 16

business_document_id   integer  optional    

The purchase invoice being settled. Required for a made payment that is not an advance. Example: 16

amount   number     

Must not be greater than 999999999. Example: 22

paid_on   string     

Must be a valid date. Example: 2026-01-15

method   string     

Example: cash

Must be one of:
  • cash
  • bank
  • upi
  • card
  • cheque
  • other
payment_account_id   integer  optional    

Example: 16

reference   string  optional    

Must not be greater than 64 characters. Example: n

notes   string  optional    

Must not be greater than 1000 characters. Example: g

direction   string     

received for a customer payment, made for a supplier payment. Example: received

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."
}
 

Request      

GET api/v1/businesses/{business}/payments/{payment_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

payment_id   integer     

The ID of the payment. Example: 1

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."
        ]
    }
}
 

Request      

PATCH api/v1/businesses/{business}/payments/{payment_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

payment_id   integer     

The ID of the payment. Example: 1

payment   integer     

The allocated payment ID. Example: 9

Body Parameters
is_advance   boolean  optional    

prohibited The original designation is fixed. Example: false

contact_id   integer  optional    

prohibited The original party is fixed.

invoice_id   integer  optional    

prohibited The original allocation is fixed.

business_document_id   integer  optional    

prohibited The original allocation is fixed.

amount   number  optional    

Payment amount in rupees, greater than zero. Example: 1000

paid_on   string  optional    

Payment date. Example: 2026-09-30

method   string  optional    

cash, bank, upi, card, cheque or other. Example: upi

payment_account_id   integer  optional    

Compatible account belonging to this business. A different account must be active. Example: 3

reference   string  optional    

Optional transaction reference, up to 64 characters. Example: UTR-123

notes   string  optional    

Optional note, up to 1000 characters. Example: Corrected receipt

direction   string  optional    

prohibited The original direction is fixed.

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]"
            }
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/payments/{payment_id}/email

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

payment_id   integer     

The ID of the payment. Example: 1

Body Parameters
recipients   string[]     

A recipient key: primary for the party's primary contact, or person-{id} for an additional contact. Must match the regex /^(primary|person-\d+)$/.

recipient_emails   string[]  optional    

Must be a valid email address. Must not be greater than 254 characters.

subject   string     

Email subject line. Must not be greater than 150 characters. Example: Sri Lakshmi Traders | Purchase order: PO-0007

message   string     

Message body. Blank lines separate paragraphs. The PDF is attached and a button links to the signed public page. Must not be greater than 2000 characters. Example: Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.

from   string  optional    

Ledger statements only: start date, inclusive. Must be a valid date. Example: 2026-04-01

to   string  optional    

Ledger statements only: end date, inclusive. Must be on or after from. Must be a valid date. Must be a date after or equal to from. Example: 2027-03-31

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/payments/{payment_id}/apply

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

payment_id   integer     

The ID of the payment. Example: 1

payment   integer     

The advance payment. Example: 9

Body Parameters
invoice_id   integer  optional    

The customer's sales invoice. Required for an advance received. Example: 16

business_document_id   integer  optional    

The supplier's purchase invoice. Required for an advance made. Example: 16

amount   number  optional    

Rupees to apply, up to the smaller of the unused advance and the bill balance. Example: 1500

applied_on   string  optional    

Date the advance was set against the bill. Defaults to today. Example: 2026-09-25

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
    }
}
 

Request      

GET api/v1/businesses/{business}/expenses

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
q   string  optional    

Case-insensitive search across category, payee, or description. Example: internet

category   string  optional    

Filter by an exact category. Example: Utilities

payment_account_id   integer  optional    

Filter by the payment account used. Example: 9

from   string  optional    

Include expenses on or after this date (YYYY-MM-DD). Example: 2026-08-01

to   string  optional    

Include expenses on or before this date (YYYY-MM-DD). Example: 2026-08-31

status   string  optional    

Filter by record status. Example: active

Must be one of:
  • active
  • voided
per_page   integer  optional    

Results per page, from 1 to 100. Example: 20

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));

Request      

POST api/v1/businesses/{business}/expenses

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
category   string     

Must not be greater than 64 characters. Example: b

payee   string  optional    

Must not be greater than 128 characters. Example: n

amount   number     

Must not be greater than 999999999. Example: 7

occurred_on   string     

Must be a valid date. Example: 2026-01-15

payment_method   string     

Example: cash

Must be one of:
  • cash
  • bank
  • upi
  • card
  • other
payment_account_id   integer  optional    

Example: 16

note   string     

Must not be greater than 1000 characters. Example: n

idempotency_key   string  optional    

Stable UUID used to make creation retries safe. Example: 6d61f406-f07d-482d-a284-3e06edfd7f55

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."
}
 

Request      

GET api/v1/businesses/{business}/expenses/{expense_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

expense_id   integer     

The ID of the expense. Example: 1

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));

Request      

POST api/v1/businesses/{business}/expenses/{expense_id}/void

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

expense_id   integer     

The ID of the expense. Example: 1

Body Parameters
reason   string     

Must be at least 3 characters. Must not be greater than 255 characters. Example: b

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."
}
 

Request      

GET api/v1/businesses/{business}/pos/items

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
q   string  optional    

Barcode, SKU, or item name to search for. Example: atta

contact   integer  optional    

Customer id. Adds party_price to every item, so the counter shows what this party pays before the sale is rung up. Example: 42

Body Parameters
q   string  optional    

Must not be greater than 128 characters. Example: b

contact   integer  optional    

Must be at least 1. Example: 22

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."
}
 

Request      

GET api/v1/businesses/{business}/pos/upi

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
amount   number     

Must not be greater than 10000000. Example: 1

payment_account_id   integer  optional    

Example: 16

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."
}
 

Request      

GET api/v1/businesses/{business}/pos/carts

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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));

Request      

POST api/v1/businesses/{business}/pos/carts

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
name   string     

Must not be greater than 96 characters. Example: b

contact_id   integer  optional    

Example: 16

lines   object[]     

Must have at least 1 items. Must not have more than 100 items.

item_id   integer     

Example: 16

item_batch_id   integer  optional    

Example: 16

serial_numbers   string[]  optional    

Must not be greater than 64 characters.

quantity   number     

Example: 4326.41688

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));

Request      

POST api/v1/businesses/{business}/pos/checkout

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
contact_id   integer     

Example: 16

idempotency_key   string     

Must be a valid UUID. Example: a4855dc5-0acb-33c3-b921-f4291f719ca0

warehouse_id   integer  optional    

The warehouse this sale's stock leaves; the default warehouse when left out. Must be at least 1. Example: 66

cart_id   integer  optional    

Example: 16

lines   object[]     

Must have at least 1 items. Must not have more than 100 items.

item_id   integer     

Example: 16

item_batch_id   integer  optional    

A batch-tracked item says which lot it is sold from; anything else must not. Example: 16

serial_numbers   string[]  optional    

Must not be greater than 64 characters.

quantity   number     

Example: 4326.41688

discount   number  optional    

Must be at least 0. Example: 77

payments   object[]  optional    

Must not have more than 4 items.

method   string     

Example: cash

Must be one of:
  • cash
  • upi
  • card
  • bank
  • cheque
payment_account_id   integer  optional    

Example: 16

amount   number     

Example: 4326.41688

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."
}
 

Request      

GET api/v1/businesses/{business}/cash-register

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
cash_register_id   integer  optional    

Example: 16

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));

Request      

POST api/v1/businesses/{business}/cash-register/open

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
cash_register_id   integer  optional    

Example: 16

opening_float   number     

Must be at least 0. Must not be greater than 999999999. Example: 22

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));

Request      

POST api/v1/businesses/{business}/cash-register/movements

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
type   string     

Example: cash_in

Must be one of:
  • cash_in
  • cash_out
cash_register_id   integer  optional    

Example: 16

amount   number     

Must not be greater than 999999999. Example: 22

notes   string     

Must not be greater than 255 characters. Example: g

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));

Request      

POST api/v1/businesses/{business}/cash-register/close

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
counted_cash   number     

Must be at least 0. Must not be greater than 999999999. Example: 1

cash_register_id   integer  optional    

Example: 16

closing_notes   string  optional    

Must not be greater than 1000 characters. Example: n

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."
        ]
    }
}
 

Request      

PATCH api/v1/businesses/{business}/cash-register/printer

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
cash_register_id   integer     

Counter whose printer settings change. Must match an existing stored value. Example: 1

paper   string  optional    

Roll width: 58mm (2-inch, 32 characters a line) or 80mm (3-inch, 48 characters a line). Example: 80mm

Must be one of:
  • 58mm
  • 80mm
auto_print   boolean  optional    

Print the receipt as soon as a sale is saved, without asking. Example: true

cut   boolean  optional    

Cut the paper after each receipt. Turn off for printers without a cutter. Example: true

open_drawer   boolean  optional    

Open the cash drawer connected to the printer after a sale with a cash payment. Example: true

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"
    }
}
 

Request      

GET api/v1/businesses/{business}/cash-register/printer/test

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
cash_register_id   integer  optional    

Counter the receipt prints at. Its saved printer settings (paper, cut, cash drawer) apply. Omit to use the defaults: 80mm, cut, drawer opens for cash sales. Must match an existing stored value. Example: 1

paper   string  optional    

Overrides the counter's paper width: 58mm (32 characters a line) or 80mm (48 characters a line). Example: 58mm

Must be one of:
  • 58mm
  • 80mm
cut   boolean  optional    

Overrides whether the paper is cut at the end: 1 or 0. Example: false

open_drawer   boolean  optional    

Overrides whether the cash drawer opens: 1 always opens it, 0 never does. When omitted, the drawer opens only if the counter allows it and the sale has a cash payment. Example: false

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."
}
 

Request      

GET api/v1/businesses/{business}/reports

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
from   string  optional    

Must be a valid date. Example: 2026-01-15

to   string  optional    

Must be a valid date. Must be a date after or equal to from. Example: 2026-01-15

warehouse   integer  optional    

Must be at least 1. Example: 22

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."
        ]
    }
}
 

Request      

GET api/v1/businesses/{business}/reports/export

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
report   string     

A report slug from the catalog. Example: profit-loss

format   string     

pdf or xlsx. Example: pdf

from   string     

Start date, YYYY-MM-DD. Example: 2026-04-01

to   string     

End date, YYYY-MM-DD, on or after from. Example: 2026-10-03

warehouse   integer  optional    

Optional warehouse of this business; narrows stock-summary, stock-valuation and low-stock. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/reports/expiring

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
within_days   integer  optional    

How far ahead to look, 1-365. Defaults to 30. Example: 60

Body Parameters
within_days   integer  optional    

Must be at least 1. Must not be greater than 365. Example: 1

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."
            }
        ]
    }
}
 

Request      

GET api/v1/businesses/{business_id}/compliance

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

The ID of the business. Example: 1

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."
            }
        ]
    }
}
 

Request      

PUT api/v1/businesses/{business_id}/compliance/profile

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

The ID of the business. Example: 1

Body Parameters
state_code   string     

Two-character GST state/UT code. Example: 29

category_ids   integer[]     

Must match an existing stored value.

constitution   string     

Legal constitution of the business. Example: proprietorship

Must be one of:
  • proprietorship
  • partnership
  • llp
  • private_limited
  • public_limited
  • opc
  • huf
  • trust_society
  • cooperative
  • government
  • other
pan   string  optional    

PAN. Required for a business marked not registered under GST. Omit to keep the saved PAN. Must match the regex /^[A-Z]{5}[0-9]{4}[A-Z]$/. Must be 10 characters. Example: ABCDE1234F

district   string  optional    

District containing the principal place of business. Must not be greater than 96 characters. Example: Bengaluru Urban

local_body   string  optional    

Municipality, corporation or panchayat. Must not be greater than 128 characters. Example: BBMP

premises_type   string     

How the principal premises is occupied. Example: rented

Must be one of:
  • owned
  • rented
  • leased
  • home
  • mobile
  • virtual
  • other
supply_type   string     

Whether the business supplies goods, services or both. Example: both

Must be one of:
  • goods
  • services
  • both
gst_registration_status   string     

Declared GST registration status. Example: not_registered

Must be one of:
  • not_assessed
  • not_registered
  • pending
  • active
  • suspended
  • cancelled
previous_year_turnover   number     

Previous financial-year PAN-wide aggregate turnover in rupees. Must be at least 0. Must not be greater than 999999999999.99. Example: 1800000

other_pan_turnover   number     

Current-year PAN-wide turnover not recorded in this workspace, in rupees. Must be at least 0. Must not be greater than 999999999999.99. Example: 250000

employee_count   integer     

Direct employee count. Must be at least 0. Must not be greater than 1000000. Example: 4

contract_worker_count   integer     

Contract worker count. Must be at least 0. Must not be greater than 1000000. Example: 0

female_employee_count   integer     

Women employees, not exceeding employee_count. Must be at least 0. Must not be greater than 1000000. Example: 2

interstate_sales   boolean     

Example: false

ecommerce_sales   boolean     

Example: true

imports   boolean     

Example: false

exports   boolean     

Example: false

manufactures   boolean     

Example: false

food_business   boolean     

Example: true

uses_weighing_scale   boolean     

Example: true

packs_or_imports_goods   boolean     

Example: false

pharmacy   boolean     

Example: false

serves_alcohol   boolean     

Example: false

fire_risk   boolean     

Example: false

pollution_activity   boolean     

Example: false

reminder_days   integer[]  optional    

Must be at least 0. Must not be greater than 365.

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
    }
}
 

Request      

POST api/v1/businesses/{business_id}/compliance/assess

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

The ID of the business. Example: 1

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"
    }
}
 

Request      

POST api/v1/businesses/{business_id}/compliance/gst-registrations

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

The ID of the business. Example: 1

Body Parameters
registration_type   string     

GST taxpayer/registration profile. Example: composition

Must be one of:
  • normal
  • composition
  • casual
  • non_resident
  • isd
  • tds
  • tcs
  • sez_unit
  • sez_developer
  • oidar
  • uin
  • temporary
registration_status   string     

Current portal status. Example: active

Must be one of:
  • pending
  • active
  • suspended
  • cancelled
gstin   string  optional    

15-character GSTIN. Required except for a UIN record. This field is required unless registration_type is in uin. Must match the regex /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/. Must be 15 characters. Example: 29ABCDE1234F1Z5

uin   string  optional    

Unique Identity Number for UIN records. This field is required when registration_type is uin. Must not be greater than 32 characters.

state_code   string  optional    

Two-character state/UT code; must match the GSTIN prefix. Example: 29

is_primary   boolean     

Whether this is the primary invoicing registration. Example: true

valid_from   string  optional    

Must be a valid date. Example: 2026-04-01

valid_until   string  optional    

Must be a valid date. Must be a date after or equal to valid_from.

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
 

Request      

DELETE api/v1/businesses/{business_id}/compliance/gst-registrations/{gstRegistration_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

The ID of the business. Example: 1

gstRegistration_id   integer     

The ID of the gstRegistration. Example: 1

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"
            }
        ]
    }
}
 

Request      

GET api/v1/businesses/{business_id}/compliance/obligations/{complianceObligation_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

The ID of the business. Example: 1

complianceObligation_id   integer     

The ID of the complianceObligation. Example: 1

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"
            }
        ]
    }
}
 

Request      

PATCH api/v1/businesses/{business_id}/compliance/obligations/{complianceObligation_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

The ID of the business. Example: 1

complianceObligation_id   integer     

The ID of the complianceObligation. Example: 1

Body Parameters
status   string     

Owner-tracked status. not_applicable requires notes. Example: obtained

Must be one of:
  • action_required
  • in_progress
  • obtained
  • not_applicable
  • expired
  • no_longer_applicable
reference_number   string  optional    

Application, registration or licence number. Must not be greater than 128 characters. Example: LIC-2026-1001

issued_on   string  optional    

Must be a valid date. Example: 2026-04-01

due_on   string  optional    

Must be a valid date.

expires_on   string  optional    

Must be a valid date. Must be a date after or equal to issued_on. Example: 2027-03-31

notes   string  optional    

Must not be greater than 5000 characters. Example: Renewal filed by the accountant.

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"
            }
        ]
    }
}
 

Request      

POST api/v1/businesses/{business_id}/compliance/obligations/{complianceObligation_id}/documents

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters
business_id   integer     

The ID of the business. Example: 1

complianceObligation_id   integer     

The ID of the complianceObligation. Example: 1

Body Parameters
document   file     

Private PDF/JPEG/PNG/WebP evidence, maximum 10 MB. Must be a file. Must not be greater than 10240 kilobytes. Example: /path/to/file

document_number   string  optional    

Certificate or licence number. Must not be greater than 128 characters. Example: FSSAI-10010022000123

issued_on   string  optional    

Must be a valid date. Example: 2026-04-01

expires_on   string  optional    

Must be a valid date. Must be a date after or equal to issued_on. Example: 2027-03-31

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."
}
 

Request      

GET api/v1/businesses/{business_id}/compliance/documents/{complianceDocument_id}/download

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

The ID of the business. Example: 1

complianceDocument_id   integer     

The ID of the complianceDocument. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/gst

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/gst/gstr1

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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."
}
 

Request      

POST api/v1/businesses/{business}/gst/gstr2b

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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.
 

Request      

GET api/v1/businesses/{business}/gst/gstr3b

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
period   string     

The month, as YYYY-MM. Example: 2026-08

revision   integer  optional    

A specific revision. Defaults to the latest. Example: 1

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.
 

Request      

GET api/v1/businesses/{business}/gst/gstr3b/{period}/export

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

period   string     

The month, as YYYY-MM. Example: 2026-08

Query Parameters
format   string  optional    

json or xlsx. Defaults to json. Example: xlsx

revision   integer  optional    

A specific revision. Defaults to the latest. Example: 1

Body Parameters
format   string  optional    

Example: json

Must be one of:
  • json
  • xlsx
revision   integer  optional    

Must be at least 1. Example: 16

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.
 

Request      

POST api/v1/businesses/{business}/gst/gstr3b/{period}/lock

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

period   string     

The month, as YYYY-MM. Example: 2026-08

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.
 

Request      

POST api/v1/businesses/{business}/gst/gstr3b/{period}/amend

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

period   string     

The month, as YYYY-MM. Example: 2026-08

Body Parameters
reason   string  optional    

Why the period is being amended, up to 255 characters. Example: Missed a purchase invoice

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.
 

Request      

GET api/v1/businesses/{business}/gst/gstr9

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
financial_year   string     

The year, as YYYY-YY, starting in April. Example: 2026-27

revision   integer  optional    

A specific revision. Defaults to the latest. Example: 1

Body Parameters
financial_year   string     

Must not be greater than 7 characters. Example: bngzmiy

revision   integer  optional    

Must be at least 1. Example: 16

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.
 

Request      

GET api/v1/businesses/{business}/gst/gstr9/{financialYear}/export

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

financialYear   string     

The year, as YYYY-YY. Example: 2026-27

Query Parameters
format   string  optional    

json or xlsx. Defaults to json. Example: xlsx

revision   integer  optional    

A specific revision. Defaults to the latest. Example: 1

Body Parameters
format   string  optional    

Example: json

Must be one of:
  • json
  • xlsx
revision   integer  optional    

Must be at least 1. Example: 16

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.
 

Request      

POST api/v1/businesses/{business}/gst/gstr9/{financialYear}/lock

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

financialYear   string     

The year, as YYYY-YY. Example: 2026-27

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.
 

Request      

POST api/v1/businesses/{business}/gst/gstr9/{financialYear}/amend

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

financialYear   string     

The year, as YYYY-YY. Example: 2026-27

Body Parameters
reason   string  optional    

Why the year is being amended, up to 255 characters. Example: Credit note saved after filing

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": []
    }
}
 

Request      

GET api/v1/businesses/{business}/billing

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/billing/payments

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
per_page   integer  optional    

Results per page. Example: 20

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."
}
 

Request      

GET api/v1/businesses/{business}/billing/payments/{subscriptionPayment_id}/invoice

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

subscriptionPayment_id   integer     

The ID of the subscriptionPayment. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/billing/continue-free

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
subscription_id   integer     

The current free_transition.subscription_id from the business resource. Example: 3

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/billing/change

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Idempotency-Key        

Example: upgrade-2026-08-25-01

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
plan_id   string     

The active plan ID selected from the billing discovery endpoint. Must match an existing stored value. Example: 3

billing_interval   string     

The selected billing frequency. Example: monthly

Must be one of:
  • monthly
  • yearly
idempotency_key   string  optional    

Optional body equivalent of the Idempotency-Key header for safely retrying a prorated upgrade. Must be at least 8 characters. Must not be greater than 128 characters. Example: upgrade-2026-08-25-01

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/billing/checkout

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
plan_id   string     

The active plan ID selected from the billing discovery endpoint. Must match an existing stored value. Example: 3

billing_interval   string     

The selected billing frequency. Example: monthly

Must be one of:
  • monthly
  • yearly

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));

Request      

POST api/v1/businesses/{business}/billing/checkouts/{billingCheckout_id}/confirm

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

billingCheckout_id   integer     

The ID of the billingCheckout. Example: 1

Body Parameters
razorpay_payment_id   string     

Must match the regex /^pay_[A-Za-z0-9]+$/. Must not be greater than 191 characters. Example: b

razorpay_subscription_id   string     

Must match the regex /^sub_[A-Za-z0-9]+$/. Must not be greater than 191 characters. Example: n

razorpay_signature   string     

Must match the regex /^[a-f0-9]{64}$/i. Must be 64 characters. Example: gzmiyvdljnikhwaykcmyuwpwlvqwrsitcpscqldzsnrwtujwvlxjklqppwqbewtn

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."
}
 

Request      

POST api/v1/businesses/{business}/billing/cancel

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/billing/pos-seats

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
pos_seats   integer     

The total number of POS logins to pay for. Example: 2

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"
    }
}
 

Request      

GET api/v1/referrals/codes/{code}

Headers
Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
code   string     

The referral code. Example: K7M2QX9A

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."
}
 

Request      

GET api/v1/referrals

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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
    }
}
 

Request      

GET api/v1/referrals/history

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
status   string  optional    

Filter by one status. Example: reward_earned

per_page   integer  optional    

Results per page, from 1 to 50. Example: 20

Body Parameters
status   string  optional    

Example: architecto

per_page   integer  optional    

Must be at least 1. Must not be greater than 50. Example: 22

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
        }
    ]
}
 

Request      

GET api/v1/referrals/rewards

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
status   string  optional    

Filter by available, active or consumed. Example: available

Body Parameters
status   string  optional    

Example: architecto

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."
        ]
    }
}
 

Request      

POST api/v1/referrals/rewards/apply

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
business_id   integer  optional    

An active workspace the signed-in account owns. Example: 17

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
        }
    }
}
 

Request      

GET api/v1/admin/referral-settings

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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."
        ]
    }
}
 

Request      

PUT api/v1/admin/referral-settings

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
enabled   boolean     

Turn the referral program on or off. While off, new sign-ups are not attributed and no new rewards are earned; rewards already earned still apply. Example: true

reward_plan_id   integer     

The active paid plan a reward grants when the referrer has never paid. Referrers who paid before get the plan they last paid for. Example: 2

reward_months   integer     

Free months granted per successful referral, from 1 to 24. Must be at least 1. Must not be greater than 24. Example: 1

whatsapp_message   string     

The share message. {link}, {code} and {name} are replaced with the referrer's link, code and name; the link is appended when {link} is missing. Must not be greater than 1000 characters. Example: Hey! Check out Dukanam. You can register using my referral link: {link}

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
    }
}
 

Request      

GET api/v1/admin/referrals

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
status   string  optional    

Filter by registered, subscription_pending, subscription_purchased, reward_earned or reward_applied. Example: reward_earned

search   string  optional    

Match the referrer's or referred person's name or email, or the code. Example: priya

per_page   integer  optional    

Results per page, from 1 to 50. Example: 25

Body Parameters
status   string  optional    

Example: architecto

search   string  optional    

Must not be greater than 100 characters. Example: n

per_page   integer  optional    

Must be at least 1. Must not be greater than 50. Example: 7

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
    }
}
 

Request      

GET api/v1/admin/referral-rewards

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
status   string  optional    

Filter by available, active or consumed. Example: active

search   string  optional    

Match the receiving account's name or email. Example: priya

per_page   integer  optional    

Results per page, from 1 to 50. Example: 25

Body Parameters
status   string  optional    

Example: architecto

search   string  optional    

Must not be greater than 100 characters. Example: n

per_page   integer  optional    

Must be at least 1. Must not be greater than 50. Example: 7

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."
        ]
    }
}
 

Request      

GET api/v1/businesses/{business}/exports

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
type   string     

Export type, or several comma-separated. Example: invoices

format   string  optional    

Output format, csv or xlsx. Defaults to csv. Example: xlsx

from   string  optional    

date Start of the period, for exports that carry a date. Example: 2026-04-01

to   string  optional    

date End of the period, for exports that carry a date. Example: 2027-03-31

async   boolean  optional    

Generate on the queue and return the export to poll, whatever its size. Example: true

Body Parameters
type   string[]  optional    
format   string  optional    

Example: csv

Must be one of:
  • csv
  • xlsx
from   string  optional    

Must be a valid date. Example: 2026-01-15

to   string  optional    

Must be a valid date. Must be a date after or equal to from. Example: 2026-01-15

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."
        ]
    }
}
 

Request      

GET api/v1/businesses/{business}/exports/tally

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
mode   string  optional    

What to export: masters, vouchers or both. Defaults to both. Example: both

from   string  optional    

date Start of the period. Required unless mode=masters. Example: 2026-04-01

to   string  optional    

date End of the period. Required unless mode=masters. Example: 2027-03-31

async   boolean  optional    

Generate on the queue and return the export to poll, whatever its size. Example: true

Body Parameters
mode   string  optional    

Example: masters

Must be one of:
  • masters
  • vouchers
  • both
from   string  optional    

Must be a valid date. Example: 2026-01-15

to   string  optional    

Must be a valid date. Must be a date after or equal to from. Example: 2026-01-15

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."
}
 

Request      

GET api/v1/businesses/{business}/exports/queued

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/exports/queued/{dataExport_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

dataExport_id   integer     

The ID of the dataExport. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/exports/queued/{dataExport_id}/download

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

dataExport_id   integer     

The ID of the dataExport. Example: 1

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
        }
    }
}
 

Request      

GET api/v1/businesses/{business}/team

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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"
        }
    }
}
 

Request      

POST api/v1/businesses/{business}/team/invitations

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
email   string     

The invited person's email. Example: [email protected]

role   string     

One of admin, manager, cashier, accountant, staff, or viewer. The pos role is not invitable: POS logins are bought per seat and created with POST /businesses/{business}/team/pos-logins. Example: cashier

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/team/invitations/{teamInvitation_id}/resend

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

teamInvitation_id   integer     

The ID of the teamInvitation. Example: 1

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."
}
 

Request      

DELETE api/v1/businesses/{business}/team/invitations/{teamInvitation_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

teamInvitation_id   integer     

The ID of the teamInvitation. Example: 1

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."
        ]
    }
}
 

Request      

PATCH api/v1/businesses/{business}/team/members/{member_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

member_id   integer     

The ID of the member. Example: 1

Body Parameters
role   string     

One of admin, manager, cashier, accountant, staff, or viewer. Example: manager

status   string     

Either active or suspended. Example: active

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
 

Request      

DELETE api/v1/businesses/{business}/team/members/{member_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

member_id   integer     

The ID of the member. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/team/pos-logins

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
name   string     

The counter staff member's name. Example: Ravi Kumar

email   string     

The email they sign in with. Example: [email protected]

password   string     

At least eight characters. Example: counter-password

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."
}
 

Request      

PATCH api/v1/businesses/{business}/team/pos-logins/{member_id}/password

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

member_id   integer     

The ID of the member. Example: 1

Body Parameters
password   string     

The new password, at least eight characters. Example: new-counter-password

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."
        ]
    }
}
 

Request      

POST api/v1/team-invitations/{token}/accept

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
token   string     

The opaque token from the invitation email. Example: 4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa

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"
}
 

Request      

GET api/v1/team-invitations/{token}

Headers
Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
token   string     

The opaque token from the invitation email. Example: 4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa

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."
        ]
    }
}
 

Request      

POST api/v1/team-invitations/{token}/register-and-accept

Headers
Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
token   string     

The opaque token from the invitation email. Example: 4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa

Body Parameters
name   string     

The new user's name. Example: Priya Rao

password   string     

The new user's password; minimum eight characters. Example: secret-pass-123

device_name   string     

The name for the new API token. Example: Priya's phone

password_confirmation   string     

Password confirmation. Example: secret-pass-123

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
    }
}
 

Request      

GET api/v1/businesses/{business_id}/testimonials

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

Workspace ID. Example: 17

Query Parameters
per_page   integer  optional    

Results per page, from 1 to 24. Example: 12

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
    }
}
 

Request      

POST api/v1/businesses/{business_id}/testimonials

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters
business_id   integer     

Workspace ID. Example: 17

Body Parameters
store_type   string     

Industry page that should receive the testimonial after approval. Accepts configured industry slugs, including electrical-store, paint-store, sanitaryware-store and pesticide-store. Example: pharmacy

Must be one of:
  • kirana-store
  • pharmacy
  • supermarket
  • clothing-store
  • footwear-store
  • hardware-store
  • electronics-store
  • mobile-store
  • stationery-store
  • book-store
  • bakery
  • cosmetics-store
  • wholesale-business
  • auto-parts-store
  • home-appliance-store
  • gift-shop
  • electrical-store
  • paint-store
  • sanitaryware-store
  • pesticide-store
feedback   string     

First-hand feedback, 40 to 1,200 characters. Must be at least 40 characters. Must not be greater than 1200 characters. Example: Dukanam made our daily medicine counter billing easier to review, and the purchase and stock records now stay together for closing.

photo   file     

JPEG, PNG or WebP shop photo, 320×320 to 6000×6000 pixels, maximum 5 MB. Must be an image. Must not be greater than 5120 kilobytes. Example: /path/to/file

consent   boolean     

Must be true to confirm content rights, consent from people shown, and permission to moderate and publish the submitted name, shop, photo and feedback. Must be accepted. Example: true

website   string  optional    

Spam-trap field. Omit it or leave it empty.

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
    }
}
 

Request      

GET api/v1/testimonials

Headers
Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
store_type   string  optional    

Filter by one configured industry slug, including electrical-store, paint-store, sanitaryware-store and pesticide-store. Example: electrical-store

per_page   integer  optional    

Results per page, from 1 to 24. Example: 12

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
    }
}
 

Request      

GET api/v1/admin/testimonials

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
status   string  optional    

Filter by pending, approved or rejected. Example: pending

store_type   string  optional    

Filter by one configured industry slug, including electrical-store, paint-store, sanitaryware-store and pesticide-store. Example: electrical-store

per_page   integer  optional    

Results per page, from 1 to 50. Example: 24

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."
        ]
    }
}
 

Request      

POST api/v1/admin/testimonials

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

Body Parameters
business_id   integer     

Existing customer business ID. Required on creation; prohibited on editing. Owners or businesses with uxcrafts.com or subdomain emails are excluded. Must match an existing stored value. Example: 17

store_type   string  optional    

Optional configured industry page slug. An unassigned draft can be saved but requires a page before approval. Example: pharmacy

Must be one of:
  • kirana-store
  • pharmacy
  • supermarket
  • clothing-store
  • footwear-store
  • hardware-store
  • electronics-store
  • mobile-store
  • stationery-store
  • book-store
  • bakery
  • cosmetics-store
  • wholesale-business
  • auto-parts-store
  • home-appliance-store
  • gift-shop
  • electrical-store
  • paint-store
  • sanitaryware-store
  • pesticide-store
owner_name   string  optional    

Optional display-name correction. Defaults to the business owner on creation and keeps the current name on editing. Must be at least 1 character. Must not be greater than 120 characters. Example: Kavitha Reddy

shop_name   string  optional    

Optional business display name; defaults to the registered business name. Must be at least 1 character. Must not be greater than 160 characters. Example: Aarogya Medical Store

location   string  optional    

Optional public city/region, up to 160 characters. Defaults to the business city on creation. Must not be greater than 160 characters. Example: Hyderabad, Telangana

feedback   string     

Customer-supported wording, 40–1200 characters. Always saved pending; editing unpublishes the previous version. Must be at least 40 characters. Must not be greater than 1200 characters. Example: We have had a positive experience using Dukanam for our pharmacy in Hyderabad.

photo   file  optional    

Optional real shop photo. JPEG, PNG or WebP, 320–6000 pixels per dimension, maximum 5 MB. Omit to keep the existing photo on editing. Must be an image. Must not be greater than 5120 kilobytes. Example: /path/to/file

remove_photo   boolean  optional    

Remove the existing photo on editing; cannot be combined with a new photo. Example: false

consent   boolean  optional    

Confirm permission for the exact wording, attribution, location and any photo. False or omitted clears consent; drafts without consent cannot be approved. Example: true

consent_note   string  optional    

Private source/evidence of permission, maximum 1000 characters. Required when consent is true; never public. This field is required when consent is 1 or true. Must not be greater than 1000 characters. Example: Owner approved this wording and public attribution in our customer conversation.

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
    }
}
 

Request      

PUT api/v1/admin/testimonials/{public_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters
public_id   string     

Public testimonial UUID. Example: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29

Body Parameters
store_type   string  optional    

Optional configured industry page slug. An unassigned draft can be saved but requires a page before approval. Example: pharmacy

Must be one of:
  • kirana-store
  • pharmacy
  • supermarket
  • clothing-store
  • footwear-store
  • hardware-store
  • electronics-store
  • mobile-store
  • stationery-store
  • book-store
  • bakery
  • cosmetics-store
  • wholesale-business
  • auto-parts-store
  • home-appliance-store
  • gift-shop
  • electrical-store
  • paint-store
  • sanitaryware-store
  • pesticide-store
owner_name   string  optional    

Optional display-name correction. Defaults to the business owner on creation and keeps the current name on editing. Must be at least 1 character. Must not be greater than 120 characters. Example: Kavitha Reddy

shop_name   string  optional    

Optional business display name; defaults to the registered business name. Must be at least 1 character. Must not be greater than 160 characters. Example: Aarogya Medical Store

location   string  optional    

Optional public city/region, up to 160 characters. Defaults to the business city on creation. Must not be greater than 160 characters. Example: Hyderabad, Telangana

feedback   string     

Customer-supported wording, 40–1200 characters. Always saved pending; editing unpublishes the previous version. Must be at least 40 characters. Must not be greater than 1200 characters. Example: We have had a positive experience using Dukanam for our pharmacy in Hyderabad.

photo   file  optional    

Optional real shop photo. JPEG, PNG or WebP, 320–6000 pixels per dimension, maximum 5 MB. Omit to keep the existing photo on editing. Must be an image. Must not be greater than 5120 kilobytes. Example: /path/to/file

remove_photo   boolean  optional    

Remove the existing photo on editing; cannot be combined with a new photo. Example: false

consent   boolean  optional    

Confirm permission for the exact wording, attribution, location and any photo. False or omitted clears consent; drafts without consent cannot be approved. Example: true

consent_note   string  optional    

Private source/evidence of permission, maximum 1000 characters. Required when consent is true; never public. This field is required when consent is 1 or true. Must not be greater than 1000 characters. Example: Owner approved this wording and public attribution in our customer conversation.

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
    }
}
 

Request      

PATCH api/v1/admin/testimonials/{public_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
public_id   string     

Public testimonial UUID. Example: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29

Body Parameters
status   string     

Moderation decision. Must be approved or rejected. Example: approved

Must be one of:
  • approved
  • rejected
review_note   string  optional    

Private moderation note. Must not be greater than 1000 characters. Example: Photo and first-hand statement verified.

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
    }
}
 

Request      

GET api/v1/businesses/{business_id}/feedback

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

Workspace ID. Example: 17

Query Parameters
per_page   integer  optional    

Results per page, from 1 to 24. Example: 12

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
    }
}
 

Request      

POST api/v1/businesses/{business_id}/feedback

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters
business_id   integer     

Workspace ID. Example: 17

Body Parameters
feedback_type   string     

Improvement, complaint or suggestion. Example: suggestion

Must be one of:
  • improvement
  • complaint
  • suggestion
message   string     

Private product feedback, 20 to 2,000 characters. Must be at least 20 characters. Must not be greater than 2000 characters. Example: Please add an option to export the daily counter summary directly from the mobile dashboard.

photo   file  optional    

Optional JPEG, PNG or WebP screenshot or shop photo, 320×320 to 6000×6000 pixels, maximum 5 MB. Must be an image. Must not be greater than 5120 kilobytes. Example: /path/to/file

website   string  optional    

Spam-trap field. Omit it or leave it empty.

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
    }
}
 

Request      

GET api/v1/admin/feedback

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
status   string  optional    

Filter by pending, reviewed or resolved. Example: pending

feedback_type   string  optional    

Filter by improvement, complaint or suggestion. Example: complaint

per_page   integer  optional    

Results per page, from 1 to 50. Example: 24

Body Parameters
status   string  optional    
feedback_type   string  optional    
per_page   integer  optional    

Must be at least 1. Must not be greater than 50. Example: 1

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"
    }
}
 

Request      

PATCH api/v1/admin/feedback/{public_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
public_id   string     

Public feedback UUID. Example: 82fdb616-df56-4547-9e09-5f58f4740acd

Body Parameters
status   string     

Feedback workflow status. Must be reviewed or resolved. Example: reviewed

Must be one of:
  • reviewed
  • resolved
review_note   string  optional    

Private administrator response or handling note. Must not be greater than 1000 characters. Example: Added to the mobile dashboard backlog.

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."
}
 

Request      

POST api/v1/apple/subscription/verify

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
business_id   integer     

The business receiving this entitlement. Example: 1

signedTransactionInfo   string  optional    

StoreKit 2 transaction JWS. Required without transactionId. Example: eyJhbGciOiJFUzI1NiIsIng1YyI6WyIuLi4iXX0.eyJ0cmFuc2FjdGlvbklkIjoiMjAwMDAwMTIzNDU2Nzg5MCJ9.signature

transactionId   string  optional    

App Store transaction ID. Required without signedTransactionInfo. Example: 2000001234567890

environment   string  optional    

Required with transactionId. Must be sandbox or production. Example: sandbox

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."
}
 

Request      

POST api/v1/apple/subscription/sync

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
business_id   integer     

The current business. Example: 1

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."
}
 

Request      

GET api/v1/me/subscription

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
business_id   integer     

The current business. Example: 1

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."
}
 

Request      

POST api/v1/apple/notifications

Headers
Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
signedPayload   string     

Apple's App Store Server Notifications V2 JWS. Example: eyJhbGciOiJFUzI1NiIsIng1YyI6WyIuLi4iXX0.eyJub3RpZmljYXRpb25UeXBlIjoiVEVTVCJ9.signature

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"
    }
}
 

Request      

GET api/v1/account/deletion

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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."
        ]
    }
}
 

Request      

POST api/v1/account/deletion

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
current_password   string     

The signed-in account password. Example: correct-horse-battery

confirmation   string     

Must be the word DELETE. Example: DELETE

reason   string  optional    

An optional reason, kept only until the purge runs. Example: Closing the shop

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."
}
 

Request      

DELETE api/v1/account/deletion

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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.

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."
        ]
    }
}
 

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."
        ]
    }
}
 

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."
}
 

Request      

POST api/v1/webhooks/razorpay

Headers
X-Razorpay-Signature        

Example: string required Razorpay webhook HMAC signature. Example: 0123456789abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

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."
        ]
    }
}
 

Request      

GET api/v1/businesses/{business}/settings/payment-details

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

business_id   integer     

Workspace ID. Example: 17

Query Parameters
payment_account_id   integer  optional    

An active payment account of this workspace to show instead of the default one. Example: 8

Body Parameters
payment_account_id   integer  optional    

Example: 16

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."
}
 

Request      

GET api/v1/plugin/session

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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."
}
 

Request      

GET api/v1/businesses/{business}/invoices/{invoice}/expenses

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

invoice   integer     

The invoice. Example: 1

Query Parameters
per_page   integer  optional    

From 1 to 100. Example: 20

Body Parameters
per_page   integer  optional    

Must be between 1 and 100. Example: 2

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));

Request      

POST api/v1/businesses/{business}/invoices/{invoice}/expenses

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

invoice   integer     

The invoice. Example: 1

Body Parameters
category   string     

Category, up to 64 characters. Example: Transport

payee   string  optional    

Payee, up to 128 characters. Example: Local transport

amount_paise   integer     

Positive expense amount in paise. Example: 30000

occurred_on   string     

Expense date, Y-m-d. Example: 2026-10-04

note   string     

Description, up to 1000 characters. Example: Transport for this invoice

paid   boolean  optional    

Whether paid at creation; defaults true. False accrues an expense payable without moving cash. Example: true

payment_method   string  optional    

Required if paid. Example: bank

Must be one of:
  • cash
  • bank
  • upi
  • card
  • other
payment_account_id   integer  optional    

Active compatible payment account.

idempotency_key   string     

Stable UUID; retain for retries. Example: fdf5dfdf-aead-473b-8c98-dcfbbda6c02a

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."
}
 

Request      

GET api/v1/businesses/{business}/documents/{document}/expenses

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

document   string     

The document. Example: architecto

Query Parameters
per_page   integer  optional    

From 1 to 100. Example: 20

Body Parameters
per_page   integer  optional    

Must be between 1 and 100. Example: 2

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));

Request      

POST api/v1/businesses/{business}/documents/{document}/expenses

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

document   string     

The document. Example: architecto

Body Parameters
category   string     

Category, up to 64 characters. Example: Transport

payee   string  optional    

Payee, up to 128 characters. Example: Local transport

amount_paise   integer     

Positive expense amount in paise. Example: 30000

occurred_on   string     

Expense date, Y-m-d. Example: 2026-10-04

note   string     

Description, up to 1000 characters. Example: Transport for this invoice

paid   boolean  optional    

Whether paid at creation; defaults true. False accrues an expense payable without moving cash. Example: true

payment_method   string  optional    

Required if paid. Example: bank

Must be one of:
  • cash
  • bank
  • upi
  • card
  • other
payment_account_id   integer  optional    

Active compatible payment account.

idempotency_key   string     

Stable UUID; retain for retries. Example: fdf5dfdf-aead-473b-8c98-dcfbbda6c02a

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));

Request      

POST api/v1/businesses/{business}/expenses/{expense}/payments

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

expense   string     

The expense. Example: architecto

Body Parameters
amount_paise   integer     

Positive paise within the outstanding amount. Example: 10000

paid_on   string     

Payment date, Y-m-d. Example: 2026-10-04

payment_method   string     

Example: bank

Must be one of:
  • cash
  • bank
  • upi
  • card
  • other
payment_account_id   integer  optional    

Active compatible account.

reference   string  optional    

Payment reference, up to 128 characters.

idempotency_key   string     

Stable UUID for retries. Example: fdf5dfdf-aead-473b-8c98-dcfbbda6c02b

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));

Request      

POST api/v1/businesses/{business}/expenses/{expense}/payments/{payment}/void

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

expense   string     

The expense. Example: architecto

payment   string     

The payment. Example: architecto

Body Parameters
reason   string     

Correction reason, up to 1000 characters. Example: Wrong transfer

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."
}
 

Request      

GET api/v1/businesses/{business_id}/settings/invoice-branding

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

Workspace ID. Example: 17

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."
        ]
    }
}
 

Request      

PUT api/v1/businesses/{business_id}/settings/invoice-branding

PATCH api/v1/businesses/{business_id}/settings/invoice-branding

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

Workspace ID. Example: 17

Body Parameters
invoice_template   string  optional    

One of classic, compact, accent, or custom. Example: accent

invoice_accent_colour   string  optional    

Any seven-character hex, #RRGGBB. The accents list returned by the GET is a set of suggestions, not the limit. Send null to return to the default teal. Example: #047857

invoice_layout   object  optional    

The knobs behind the custom template. Every key is optional and merges over what is saved, so one switch can be moved on its own. Send null to reset the whole layout to its defaults.

header   string  optional    

ruled, band, or minimal. Example: band

density   string  optional    

roomy, normal, or compact. Example: compact

table   string  optional    

ruled, flat, or zebra. Example: flat

logo   object  optional    
height   integer  optional    

Logo height on A4 in pixels, 24-96. An 80mm roll keeps its own size. Example: 64

align   string  optional    

left, center, or right. Example: center

columns   object  optional    
hsn   boolean  optional    

Print the HSN/SAC code under each line. A GST tax invoice must carry it above the turnover threshold. Example: true

uqc   boolean  optional    

Print the unit beside the quantity. Example: true

discount   boolean  optional    

Print the per-line discount column. The discount total still prints either way. Example: true

invoice_footer_note   string  optional    

Terms, a returns policy, or a thank-you line, up to 1000 characters. Example: Goods once sold are not returnable.

bank_details   string  optional    

Payment instructions printed above the signature, up to 1000 characters. Example: HDFC Bank 5010 1234 5678, IFSC HDFC0000123

authorized_signatory   string  optional    

The name printed under the signature, up to 128 characters. Example: Priya Sharma

show_upi_qr_on_invoice   boolean  optional    

Print an exact-amount UPI QR on the bill. Requires a UPI ID on the workspace. Example: true

show_logo_on_invoice   boolean  optional    

Print the shop logo on invoices and documents. On by default. The image is the workspace logo captured during onboarding, so this only controls whether it appears on the paper. Example: true

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."
        ]
    }
}
 

Request      

GET api/v1/businesses/{business_id}/settings/invoice-branding/preview

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

Workspace ID. Example: 17

Query Parameters
template   string  optional    

Preview this template instead of the saved one. Example: accent

accent   string  optional    

Preview this accent instead of the saved one, as any seven-character hex. Example: #047857

layout   object  optional    

Preview these layout knobs instead of the saved ones, as layout[header]=band&layout[density]=compact. Merged over what is saved.

format   string  optional    

Paper size: a4 (default) or 80mm. Example: 80mm

as   string  optional    

Response body: pdf (default) or html. Example: html

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business_id}/settings/invoice-branding/{kind}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters
business_id   integer     

Workspace ID. Example: 17

kind   string     

Either logo or signature. Example: logo

Body Parameters
image   file     

The PNG or JPEG to store, up to 1 MB. Example: /path/to/file

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
    }
}
 

Request      

DELETE api/v1/businesses/{business_id}/settings/invoice-branding/{kind}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

Workspace ID. Example: 17

kind   string     

Either logo or signature. Example: signature

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"
}
 

Request      

GET api/v1/businesses/{business_id}/invoices/{invoice_id}/preview

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

Workspace ID. Example: 17

invoice_id   integer     

Invoice ID. Example: 482

Query Parameters
template   string  optional    

Render this template instead of the invoice's own. Example: compact

accent   string  optional    

Render this accent instead of the invoice's own, as any seven-character hex. Example: #9A3412

layout   object  optional    

Render these layout knobs instead of the invoice's own, as layout[table]=zebra. Merged over what the workspace has saved.

format   string  optional    

Paper size: a4 (default) or 80mm. Example: 80mm

as   string  optional    

Response body: pdf (default) or html. Example: html

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."
}
 

Request      

GET api/v1/businesses/{business}/loan-accounts

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
status   string  optional    

Filter by record status. Example: active

Must be one of:
  • active
  • closed
type   string  optional    

Filter by kind. Example: loan

Must be one of:
  • loan
  • credit_card
per_page   integer  optional    

Results per page, from 1 to 100. Example: 20

Body Parameters
status   string  optional    

Example: active

Must be one of:
  • active
  • closed
type   string  optional    

Example: loan

Must be one of:
  • loan
  • credit_card
per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/loan-accounts

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
name   string     

Must not be greater than 120 characters. Example: b

type   string  optional    

Example: loan

Must be one of:
  • loan
  • credit_card
lender_name   string  optional    

Must not be greater than 120 characters. Example: n

account_last_four   string  optional    

Must not be greater than 8 characters. Example: gzmiyvdl

principal   number     

Must be at least 0. Must not be greater than 999999999. Example: 19

interest_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 17

emi_amount   number  optional    

Must not be greater than 999999999. Example: 5

emi_day_of_month   integer  optional    

Must be between 1 and 31. Example: 1

tenure_months   integer  optional    

Must be between 1 and 360. Example: 2

started_on   string     

Must be a valid date. Example: 2026-01-15

first_emi_on   string  optional    

Must be a valid date. Must be a date after or equal to started_on. Example: 2026-01-15

disbursed_to_payment_account_id   integer  optional    

Example: 16

notes   string  optional    

Must not be greater than 1000 characters. Example: n

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."
}
 

Request      

GET api/v1/businesses/{business}/loan-accounts/{loanAccount_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

loanAccount_id   integer     

The ID of the loanAccount. Example: 1

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));

Request      

PUT api/v1/businesses/{business}/loan-accounts/{loanAccount_id}

PATCH api/v1/businesses/{business}/loan-accounts/{loanAccount_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

loanAccount_id   integer     

The ID of the loanAccount. Example: 1

Body Parameters
name   string     

Must not be greater than 120 characters. Example: b

lender_name   string  optional    

Must not be greater than 120 characters. Example: n

account_last_four   string  optional    

Must not be greater than 8 characters. Example: gzmiyvdl

interest_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 19

emi_amount   number  optional    

Must not be greater than 999999999. Example: 17

emi_day_of_month   integer  optional    

Must be between 1 and 31. Example: 1

tenure_months   integer  optional    

Must be between 1 and 360. Example: 1

first_emi_on   string  optional    

Must be a valid date. Example: 2026-01-15

status   string  optional    

Example: active

Must be one of:
  • active
  • closed
notes   string  optional    

Must not be greater than 1000 characters. Example: h

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."
}
 

Request      

GET api/v1/businesses/{business}/loan-accounts/{loanAccount_id}/schedule

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

loanAccount_id   integer     

The ID of the loanAccount. Example: 1

Query Parameters
status   string  optional    

Filter by instalment status. Example: pending

Must be one of:
  • pending
  • paid
Body Parameters
status   string  optional    

Example: pending

Must be one of:
  • pending
  • paid

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."
}
 

Request      

GET api/v1/businesses/{business}/loan-accounts/{loanAccount_id}/payments

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

loanAccount_id   integer     

The ID of the loanAccount. Example: 1

Query Parameters
per_page   integer  optional    

Results per page, from 1 to 100. Example: 20

Body Parameters
per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/loan-accounts/{loanAccount_id}/payments

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

loanAccount_id   integer     

The ID of the loanAccount. Example: 1

Body Parameters
loan_emi_schedule_id   integer  optional    

Example: 16

paid_on   string     

Must be a valid date. Example: 2026-01-15

amount   number  optional    

Must not be greater than 999999999. Example: 22

principal   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 7

interest   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 16

charges   number  optional    

Must be at least 0. Must not be greater than 999999999. Example: 17

payment_method   string     

Example: cash

Must be one of:
  • cash
  • bank
  • upi
  • card
  • cheque
  • other
payment_account_id   integer  optional    

Example: 16

reference   string  optional    

Must not be greater than 64 characters. Example: n

note   string  optional    

Must not be greater than 255 characters. Example: g

idempotency_key   string  optional    

Stable UUID that makes a retry safe. Example: 6d61f406-f07d-482d-a284-3e06edfd7f55

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));

Request      

POST api/v1/businesses/{business}/loan-accounts/{loanAccount_id}/payments/{loanPayment_id}/void

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

loanAccount_id   integer     

The ID of the loanAccount. Example: 1

loanPayment_id   integer     

The ID of the loanPayment. Example: 1

Body Parameters
reason   string     

Must be at least 3 characters. Must not be greater than 255 characters. Example: b

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."
}
 

Request      

GET api/v1/businesses/{business}/other-incomes

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
q   string  optional    

Case-insensitive search across category, payer, or description. Example: rent

category   string  optional    

Filter by an exact category. Example: Rent received

payment_account_id   integer  optional    

Filter by the account the money landed in. Example: 9

from   string  optional    

Include receipts on or after this date (YYYY-MM-DD). Example: 2026-08-01

to   string  optional    

Include receipts on or before this date (YYYY-MM-DD). Example: 2026-08-31

status   string  optional    

Filter by record status. Example: active

Must be one of:
  • active
  • voided
per_page   integer  optional    

Results per page, from 1 to 100. Example: 20

Body Parameters
q   string  optional    

Must not be greater than 100 characters. Example: b

category   string  optional    

Must not be greater than 64 characters. Example: n

payment_account_id   integer  optional    

Example: 16

from   string  optional    

Must be a valid date. Example: 2026-01-15

to   string  optional    

Must be a valid date. Must be a date after or equal to from. Example: 2026-01-15

status   string  optional    

Example: active

Must be one of:
  • active
  • voided
per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 22

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));

Request      

POST api/v1/businesses/{business}/other-incomes

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
category   string     

Must not be greater than 64 characters. Example: b

payer   string  optional    

Must not be greater than 128 characters. Example: n

amount   number     

Must not be greater than 999999999. Example: 7

occurred_on   string     

Must be a valid date. Example: 2026-01-15

payment_method   string     

Example: cash

Must be one of:
  • cash
  • bank
  • upi
  • card
  • cheque
  • other
payment_account_id   integer  optional    

Example: 16

note   string     

Must not be greater than 1000 characters. Example: n

idempotency_key   string  optional    

Stable UUID that makes a retry safe. Example: 6d61f406-f07d-482d-a284-3e06edfd7f55

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."
}
 

Request      

GET api/v1/businesses/{business}/other-incomes/{otherIncome_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

otherIncome_id   integer     

The ID of the otherIncome. Example: 1

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));

Request      

POST api/v1/businesses/{business}/other-incomes/{otherIncome_id}/void

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

otherIncome_id   integer     

The ID of the otherIncome. Example: 1

Body Parameters
reason   string     

Must be at least 3 characters. Must not be greater than 255 characters. Example: b

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
    }
}
 

Request      

GET api/v1/businesses/{business}/lookups

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
source   string     

Lookup source. The items, contacts, customers, and suppliers sources return only records that are still active, since a lookup is someone choosing what to put on a new document. Example: customers

q   string  optional    

Case-insensitive search text, up to 100 characters. Example: priya

method   string  optional    

Payment method used to filter compatible payment accounts. Example: upi

direction   string  optional    

Document or account direction. Example: received

contact   integer  optional    

Contact id. Narrows outstanding documents to a single customer or supplier, prices item results with that customer's rate card, and on contact sources keeps that party in the results even when it is no longer active, so a form can still show the party it already holds. Example: 42

item   integer  optional    

Item id. Required for the item_batches source; returns that item's sellable lots, first-expiry-first-out. Example: 12

supplier   string  optional    

Supplier contact id, or preferred. On the items source, adds supplier_price, supplier_price_source, and supplier_name: that supplier's agreed rate or, failing that, the rate on their last purchase invoice. preferred uses each item's preferred supplier. Only for callers who may see purchase prices. Example: 17

warehouse   integer  optional    

Warehouse id. On the items source, the pre-selected lot is the earliest-expiring one with stock in that warehouse; on item_batches, only lots with stock there are listed, with the quantity held there. Example: 1

ids   integer[]  optional    

Item ids to price or re-read, up to 200. Every named item is returned, including items that are no longer active, so a form can reprice the lines it already holds in one request.

category_id   integer  optional    

Tenant-owned item category. Example: 1

subcategory_id   integer  optional    

Tenant-owned subcategory; combine with category_id. Example: 2

brand_id   integer  optional    

Tenant-owned item brand. Example: 3

manufacturer_id   integer  optional    

Tenant-owned manufacturer. Example: 4

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.

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"
}
 

Request      

GET api/v1/partners/team

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

Query Parameters
period   string  optional    

7d, 30d, 90d or all. Example: 30d

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."
        ]
    }
}
 

Request      

POST api/v1/partners/team/members

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

X-CSRF-TOKEN        

Example: {SESSION_CSRF_TOKEN}

Body Parameters
name   string     

Example: Anita Rao

phone   string     

Indian WhatsApp mobile. Example: 9123456789

email   string  optional    

Example: [email protected]

status   string  optional    

active or suspended. Example: active

team_role   string     

associate or employee. Example: associate

account_type   string  optional    
master_partner_id   string  optional    
commission_type   string  optional    
commission_flat_amount   string  optional    
commission_percent   string  optional    

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."
}
 

Request      

GET api/v1/partners/team/members/{member_id}

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

URL Parameters
member_id   integer     

The ID of the member. Example: 1

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
    }
}
 

Request      

PUT api/v1/partners/team/members/{member_id}

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

X-CSRF-TOKEN        

Example: {SESSION_CSRF_TOKEN}

URL Parameters
member_id   integer     

The ID of the member. Example: 1

Body Parameters
name   string  optional    

Example: Anita Rao

phone   string  optional    

Example: 9123456789

email   string  optional    

Example: [email protected]

status   string  optional    

active or suspended. Example: suspended

team_role   string     

associate or employee. Example: employee

account_type   string  optional    
master_partner_id   string  optional    
commission_type   string  optional    
commission_flat_amount   string  optional    
commission_percent   string  optional    

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"
    }
}
 

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
    }
}
 

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
        }
    ]
}
 

Request      

GET api/v1/partners/team/programs

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

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."
        ]
    }
}
 

Request      

POST api/v1/partners/team/programs

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

X-CSRF-TOKEN        

Example: {SESSION_CSRF_TOKEN}

Body Parameters
name   string     

Example: Shop referrals

description   string     

Example: Refer shops and earn rewards.

is_active   boolean     

Example: true

audience   string     

all or selected within my team. Example: all

partner_ids   integer[]  optional    

Required for selected; only my member IDs.

starts_at   string  optional    

Nullable inclusive start.

ends_at   string  optional    

Nullable exclusive end, after start if supplied.

commission_type   string     

flat or percent of master commission. Example: flat

flat_amount   number  optional    

Positive rupees, required for flat. Example: 200

percent   number  optional    

Positive percent below 100, required for percent. Example: 50

hold_days   integer     

0–180. Example: 7

owner_master_partner_id   string  optional    

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
    }
}
 

Request      

PUT api/v1/partners/team/programs/{program_id}

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

X-CSRF-TOKEN        

Example: {SESSION_CSRF_TOKEN}

URL Parameters
program_id   integer     

The ID of the program. Example: 1

Body Parameters
name   string     

Must not be greater than 80 characters. Example: b

description   string     

Must not be greater than 2000 characters. Example: Et animi quos velit et fugiat.

is_active   boolean     

Example: false

audience   string     

Example: all

Must be one of:
  • all
  • selected
partner_ids   integer[]     

Must match an existing stored value.

starts_at   string  optional    

Must be a valid date. Example: 2026-01-15

ends_at   string  optional    

Must be a valid date. Example: 2026-01-15

commission_type   string     

Example: flat

Must be one of:
  • flat
  • percent
flat_amount   number  optional    

This field is required when commission_type is flat. Must be at least 0.01. Must not be greater than 1000000. Example: 22

percent   number  optional    

This field is required when commission_type is percent. Must be at least 0.01. Must not be greater than 99.99. Example: 7

hold_days   integer     

Must be at least 0. Must not be greater than 180. Example: 16

owner_master_partner_id   string  optional    

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
        }
    ]
}
 

Request      

GET api/v1/partners/team/settlements

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

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"
        }
    }
}
 

Request      

GET api/v1/partners/team/settlements/{settlement_id}

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

URL Parameters
settlement_id   integer     

The ID of the settlement. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/partners/team/members/{member_id}/settlements

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

X-CSRF-TOKEN        

Example: {SESSION_CSRF_TOKEN}

URL Parameters
member_id   integer     

The ID of the member. Example: 1

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"
    }
}
 

Request      

POST api/v1/partners/team/settlements/{settlement_id}/mark-paid

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

X-CSRF-TOKEN        

Example: {SESSION_CSRF_TOKEN}

URL Parameters
settlement_id   integer     

The ID of the settlement. Example: 1

Body Parameters
reference   string     

Bank/UPI transfer reference, max 100. Example: UTR123456

paid_at   string  optional    

Optional payment date/time, cannot be future.

notes   string  optional    

Optional notes, max 2000. Example: Paid by UPI

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"
    }
}
 

Request      

POST api/v1/partners/team/settlements/{settlement_id}/cancel

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

X-CSRF-TOKEN        

Example: {SESSION_CSRF_TOKEN}

URL Parameters
settlement_id   integer     

The ID of the settlement. Example: 1

Body Parameters
notes   string  optional    

Optional reason, max 2000. Example: Wrong account; prepare again

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
        }
    }
}
 

Request      

GET api/v1/partners/me

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

Query Parameters
period   string  optional    

7d, 30d, 90d or all. Example: 30d

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"
            }
        }
    ]
}
 

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
    }
}
 

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."
}
 

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
}
 

Request      

GET api/v1/partners/me/commissions

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

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"
        }
    ]
}
 

Request      

GET api/v1/partners/me/payments

Headers
Accept        

Example: application/json

Content-Type        

Example: application/json

Cookie        

Example: dukanam-session={PARTNER_SESSION}

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"
        }
    ]
}
 

Request      

GET api/v1/admin/partner-programs

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
per_page   integer  optional    

Page size, from 1 to 50. Example: 25

Body Parameters
per_page   integer  optional    

Must be at least 1. Must not be greater than 50. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/admin/partner-programs

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
name   string     

Program name (up to 80 characters). Example: Creator campaign

description   string     

Partner-facing details (up to 2000 characters). Example: Help shops discover Dukanam.

is_active   boolean     

Enable availability during the scheduled window. Example: true

audience   string     

all or selected. Example: selected

partner_ids   integer[]  optional    

Required and nonempty for selected; existing individual/master IDs, no team members or duplicates.

starts_at   string  optional    

Optional start timestamp, inclusive; null for immediate availability.

ends_at   string  optional    

Optional end timestamp, exclusive; must follow starts_at when both are supplied.

commission_type   string     

flat, percent or first_month. Named program terms override the partner's personal deal for program links. Example: percent

flat_amount   number  optional    

Required for flat, positive rupees up to 1000000. Example: 250

percent   number  optional    

Required for percent, positive percentage up to 100. Example: 25

hold_days   integer     

Refund hold in days, from 0 to 180. Example: 15

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."
}
 

Request      

GET api/v1/admin/partner-programs/{program_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
program_id   integer     

The ID of the program. Example: 1

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."
        ]
    }
}
 

Request      

PUT api/v1/admin/partner-programs/{program_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
program_id   integer     

The ID of the program. Example: 1

Body Parameters
name   string     

Program name (up to 80 characters). Example: Creator campaign

description   string     

Partner-facing details (up to 2000 characters). Example: Help shops discover Dukanam.

is_active   boolean     

Enable availability during the scheduled window. Example: true

audience   string     

all or selected. Example: selected

partner_ids   integer[]  optional    

Required and nonempty for selected; existing individual/master IDs, no team members or duplicates.

starts_at   string  optional    

Optional start timestamp, inclusive; null for immediate availability.

ends_at   string  optional    

Optional end timestamp, exclusive; must follow starts_at when both are supplied.

commission_type   string     

flat, percent or first_month. Named program terms override the partner's personal deal for program links. Example: percent

flat_amount   number  optional    

Required for flat, positive rupees up to 1000000. Example: 250

percent   number  optional    

Required for percent, positive percentage up to 100. Example: 25

hold_days   integer     

Refund hold in days, from 0 to 180. Example: 15

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
        }
    }
}
 

Request      

GET api/v1/admin/partner-program

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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."
        ]
    }
}
 

Request      

PUT api/v1/admin/partner-program

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
enabled   boolean     

Example: true

commission_type   string     

flat, percent or first_month (one month of the bought plan's current monthly price, however the shop pays). Example: flat

flat_amount   number     

Rupees per paid account for flat deals. Example: 200

percent   number     

Percent of the first payment (before GST) for percent deals. Example: 20

hold_days   integer     

Days a commission is held before it can be paid, 0–180. Example: 30

minimum_payout   number     

Rupees a partner must have approved before a payout is prepared. Example: 500

tds_percent   number     

Tax deducted at source on each payout, 0–30. Example: 2

share_message   string     

Up to 1000 characters; {link}, {code} and {name} are filled in. Example: Try Dukanam free: {link}

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
    }
}
 

Request      

GET api/v1/admin/partners

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
search   string  optional    

Match the name, email or a link code. Example: ravi

status   string  optional    

active or suspended. Example: active

period   string  optional    

7d, 30d, 90d or all. Defaults to 30d. Example: 30d

per_page   integer  optional    

Results per page, from 1 to 50. Example: 25

Body Parameters
search   string  optional    

Must not be greater than 100 characters. Example: b

status   string  optional    
period   string  optional    
per_page   integer  optional    

Must be at least 1. Must not be greater than 50. Example: 22

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."
        ]
    }
}
 

Request      

POST api/v1/admin/partners

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
account_type   string  optional    

individual or master. Masters need a separate commission deal. Example: master

master_partner_id   string  optional    
commission_hold_days   integer  optional    

Nullable master hold override, 0–180. Example: 7

payout_minimum_amount   number  optional    

Nullable master minimum payout in rupees, 0–1000000. Example: 1000

payout_tds_percent   number  optional    

Nullable master TDS override, 0–30. Example: 2

name   string     

Example: Ravi Kumar

phone   string     

Indian mobile number they sign in with. Example: 9876543210

email   string  optional    

Example: [email protected]

status   string  optional    

Example: active

Must be one of:
  • active
  • suspended
commission_type   string  optional    

Their own deal: flat, percent or first_month. Omit or null for the program default. Example: flat

commission_flat_amount   number  optional    

Rupees per paid account, required for flat. Example: 250

commission_percent   number  optional    

Percent of the first payment, required for percent. Example: 25

notes   string  optional    

Internal notes. Example: YouTube, 80k subscribers

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
    }
}
 

Request      

GET api/v1/admin/partners/{id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
id   integer     

The ID of the partner. Example: 1

Query Parameters
period   string  optional    

7d, 30d, 90d or all. Defaults to 30d. Example: 30d

Body Parameters
period   string  optional    

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
        }
    }
}
 

Request      

PATCH api/v1/admin/partners/{id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
id   integer     

The ID of the partner. Example: 1

Body Parameters
account_type   string  optional    

individual or master; no downgrading masters with members or changing member roles. Example: master

master_partner_id   string  optional    
commission_hold_days   integer  optional    

Nullable master hold override, 0–180. Example: 7

payout_minimum_amount   number  optional    

Nullable master minimum payout in rupees. Example: 1000

payout_tds_percent   number  optional    

Nullable master TDS override, 0–30. Example: 2

name   string  optional    

Example: Ravi Kumar

phone   string  optional    

Example: 9876543210

email   string  optional    

Example: [email protected]

status   string  optional    

active or suspended. Example: suspended

commission_type   string  optional    

flat, percent, first_month or null. Example: percent

commission_flat_amount   number  optional    

Example: 250

commission_percent   number  optional    

Example: 25

notes   string  optional    

Example: Paused while the campaign is reviewed

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."
        ]
    }
}
 

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"
    }
}
 

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
    }
}
 

Request      

GET api/v1/admin/partners/{partner_id}/referrals

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
partner_id   integer     

The ID of the partner. Example: 1

Query Parameters
status   string  optional    

registered, subscription_pending or paid. Example: paid

per_page   integer  optional    

Results per page, from 1 to 50. Example: 25

Body Parameters
status   string  optional    
per_page   integer  optional    

Must be at least 1. Must not be greater than 50. Example: 1

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
    }
}
 

Request      

POST api/v1/admin/partners/{partner_id}/adjustments

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
partner_id   integer     

The ID of the partner. Example: 1

Body Parameters
amount   number     

Rupees; negative to claw back. Example: 1500

description   string     

Shown to the partner. Example: Diwali campaign fee

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
    }
}
 

Request      

GET api/v1/admin/partner-commissions

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
partner_id   integer  optional    

Only this partner. Example: 3

status   string  optional    

pending, approved, paid or rejected. Example: pending

per_page   integer  optional    

Results per page, from 1 to 50. Example: 25

Body Parameters
partner_id   integer  optional    

Example: 16

status   string  optional    
per_page   integer  optional    

Must be at least 1. Must not be greater than 50. Example: 22

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."
        ]
    }
}
 

Request      

POST api/v1/admin/partner-commissions/{commission_id}/approve

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
commission_id   integer     

The ID of the commission. Example: 1

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
    }
}
 

Request      

POST api/v1/admin/partner-commissions/{commission_id}/reject

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
commission_id   integer     

The ID of the commission. Example: 1

Body Parameters
reason   string     

Example: Subscription refunded

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
    }
}
 

Request      

GET api/v1/admin/partner-payouts

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
partner_id   integer  optional    

Only this partner. Example: 3

status   string  optional    

processing, paid or cancelled. Example: processing

per_page   integer  optional    

Results per page, from 1 to 50. Example: 25

Body Parameters
partner_id   integer  optional    

Example: 16

status   string  optional    
per_page   integer  optional    

Must be at least 1. Must not be greater than 50. Example: 22

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": []
    }
}
 

Request      

POST api/v1/admin/partner-payouts/prepare

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
partner_ids   integer[]  optional    

Only these partners. Omit for everyone.

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
 

Request      

GET api/v1/admin/partner-payouts/export

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
status   string  optional    

processing (default), paid or cancelled. Example: processing

Body Parameters
status   string  optional    

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."
        ]
    }
}
 

Request      

POST api/v1/admin/partner-payouts/{payout_id}/mark-paid

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
payout_id   integer     

The ID of the payout. Example: 1

Body Parameters
reference   string  optional    

The bank or UPI reference. Example: UTR123456789

paid_on   date  optional    

Defaults to today; not in the future. Example: 2026-09-29

notes   string  optional    

Example: Paid by NEFT

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"
    }
}
 

Request      

POST api/v1/admin/partner-payouts/{payout_id}/cancel

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
payout_id   integer     

The ID of the payout. Example: 1

Body Parameters
notes   string  optional    

Example: Account closed; partner is adding a new one

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.
 

Request      

GET api/v1/businesses/{business}/contacts/{contact}/profitability/export

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

contact   integer     

The contact. Example: 1

Query Parameters
format   string     

Example: pdf

Must be one of:
  • pdf
  • xlsx
  • csv
period   string  optional    

Example: this-financial-year

Must be one of:
  • today
  • this-month
  • last-month
  • this-financial-year
  • last-financial-year
  • custom
from   string  optional    

Custom start Y-m-d.

to   string  optional    

Inclusive custom end Y-m-d.

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.
 

Request      

GET api/v1/businesses/{business}/workers/{worker}/profitability/export

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

worker   string     

The worker. Example: architecto

Query Parameters
format   string     

Example: xlsx

Must be one of:
  • pdf
  • xlsx
  • csv
period   string  optional    

Example: this-financial-year

Must be one of:
  • today
  • this-month
  • last-month
  • this-financial-year
  • last-financial-year
  • custom
from   string  optional    

Custom start Y-m-d.

to   string  optional    

Inclusive custom end Y-m-d.

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.
 

Request      

GET api/v1/businesses/{business}/workers/{worker}/statement/export

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

worker   string     

The worker. Example: architecto

Query Parameters
format   string     

Example: csv

Must be one of:
  • pdf
  • xlsx
  • csv
period   string  optional    

Example: this-financial-year

Must be one of:
  • today
  • this-month
  • last-month
  • this-financial-year
  • last-financial-year
  • custom
from   string  optional    

Custom start Y-m-d.

to   string  optional    

Inclusive custom end Y-m-d.

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."
}
 

Request      

GET api/v1/businesses/{business}/reports/party-profitability

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
period   string  optional    

Example: this-financial-year

Must be one of:
  • today
  • this-month
  • last-month
  • this-financial-year
  • last-financial-year
  • custom
from   string  optional    

Custom start Y-m-d.

to   string  optional    

Inclusive custom end Y-m-d.

q   string  optional    

Party name, up to 100 characters.

status   string  optional    

Example: all

Must be one of:
  • active
  • archived
  • all. Default all
sort   string  optional    

Example: profit

Must be one of:
  • profit
  • sales
  • name. Default profit
per_page   integer  optional    

From 1 to 100. Example: 20

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.
 

Request      

GET api/v1/businesses/{business}/reports/party-profitability/export

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
format   string     

Example: xlsx

Must be one of:
  • pdf
  • xlsx
  • csv
period   string  optional    

Example: this-financial-year

Must be one of:
  • today
  • this-month
  • last-month
  • this-financial-year
  • last-financial-year
  • custom
from   string  optional    

Custom start Y-m-d.

to   string  optional    

Inclusive custom end Y-m-d.

q   string  optional    

Party name, up to 100 characters.

status   string  optional    

Example: all

Must be one of:
  • active
  • archived
  • all. Default all
sort   string  optional    

Example: profit

Must be one of:
  • profit
  • sales
  • name. Default profit

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."
}
 

Request      

GET api/v1/businesses/{business}/contacts/{contact}/profitability

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

contact   integer     

The contact. Example: 1

Query Parameters
period   string  optional    

Example: this-financial-year

Must be one of:
  • today
  • this-month
  • last-month
  • this-financial-year
  • last-financial-year
  • custom
from   string  optional    

Custom start Y-m-d; required with custom period.

to   string  optional    

Inclusive custom end Y-m-d; required with custom period.

per_page   integer  optional    

From 1 to 100. Example: 20

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."
}
 

Request      

GET api/v1/businesses/{business}/workers/performance

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
period   string  optional    

Example: this-financial-year

Must be one of:
  • today
  • this-month
  • last-month
  • this-financial-year
  • last-financial-year
  • custom
from   string  optional    

Custom start Y-m-d.

to   string  optional    

Inclusive custom end Y-m-d.

q   string  optional    

Referrer name or trade, up to 100 characters.

status   string  optional    

Example: all

Must be one of:
  • active
  • archived
  • all. Default all
worker_type   string  optional    

Example: referral

Must be one of:
  • referral
  • cook
  • carpenter
  • painter
  • electrician
  • plumber
  • doctor
  • rmp
  • contractor
  • sales_agent
  • other
sort   string  optional    

Example: profit

Must be one of:
  • profit
  • sales
  • referrals
  • name. Default profit
per_page   integer  optional    

From 1 to 100. Example: 20

Body Parameters
q   string  optional    

Must not be greater than 100 characters. Example: b

status   string  optional    

Example: active

Must be one of:
  • active
  • archived
  • all
worker_type   string  optional    

Example: architecto

sort   string  optional    

Example: profit

Must be one of:
  • profit
  • sales
  • referrals
  • name
per_page   integer  optional    

Must be between 1 and 100. Example: 2

page   integer  optional    

Must be at least 1. Example: 67

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."
}
 

Request      

GET api/v1/businesses/{business}/workers/{worker}/profitability

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

worker   string     

The worker. Example: architecto

Query Parameters
period   string  optional    

Example: this-financial-year

Must be one of:
  • today
  • this-month
  • last-month
  • this-financial-year
  • last-financial-year
  • custom
from   string  optional    

Custom start Y-m-d.

to   string  optional    

Inclusive custom end Y-m-d.

per_page   integer  optional    

From 1 to 100. Example: 20

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."
}
 

Request      

GET api/v1/businesses/{business}/price-lists

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/businesses/{business}/price-lists

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
name   string     

Name of the rate card, unique within the business. Must not be greater than 96 characters. Example: Wholesale

description   string  optional    

Must not be greater than 255 characters. Example: Eius et animi quos velit et.

price_includes_tax   boolean  optional    

Whether the fixed rates on this card already include GST. Applies only to rows carrying a fixed rate; a percentage row follows the item master. Example: false

is_active   boolean  optional    

Example: false

valid_from   string  optional    

First day the card prices anything. Leave empty for no start date. Must be a valid date. Example: 2026-04-01

valid_to   string  optional    

Last day the card prices anything. Outside the window the item master applies. Must be a valid date. Must be a date after or equal to valid_from. Example: 2027-03-31

items   object[]  optional    

The rows of the card. Sending this key replaces every existing row; omitting it leaves the rows untouched. Must not have more than 500 items.

item_id   integer     

Example: 16

sale_price   number  optional    

This field is required when items.*.discount_percent is not present. Must be at least 0. Must not be greater than 999999999. Example: 455

discount_percent   number  optional    

Must be at least 0. Must not be greater than 100. Example: 5

min_quantity   number  optional    

Must be at least 0. Must not be greater than 999999. Example: 25

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."
}
 

Request      

GET api/v1/businesses/{business}/price-lists/{id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

id   integer     

The ID of the price list. Example: 1

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));

Request      

PUT api/v1/businesses/{business}/price-lists/{id}

PATCH api/v1/businesses/{business}/price-lists/{id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

id   integer     

The ID of the price list. Example: 1

Body Parameters
name   string     

Name of the rate card, unique within the business. Must not be greater than 96 characters. Example: Wholesale

description   string  optional    

Must not be greater than 255 characters. Example: Eius et animi quos velit et.

price_includes_tax   boolean  optional    

Whether the fixed rates on this card already include GST. Applies only to rows carrying a fixed rate; a percentage row follows the item master. Example: false

is_active   boolean  optional    

Example: false

valid_from   string  optional    

First day the card prices anything. Leave empty for no start date. Must be a valid date. Example: 2026-04-01

valid_to   string  optional    

Last day the card prices anything. Outside the window the item master applies. Must be a valid date. Must be a date after or equal to valid_from. Example: 2027-03-31

items   object[]  optional    

The rows of the card. Sending this key replaces every existing row; omitting it leaves the rows untouched. Must not have more than 500 items.

item_id   integer     

Example: 16

sale_price   number  optional    

This field is required when items.*.discount_percent is not present. Must be at least 0. Must not be greater than 999999999. Example: 455

discount_percent   number  optional    

Must be at least 0. Must not be greater than 100. Example: 5

min_quantity   number  optional    

Must be at least 0. Must not be greater than 999999. Example: 25

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."
        ]
    }
}
 

Request      

DELETE api/v1/businesses/{business}/price-lists/{id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

id   integer     

The ID of the price list. Example: 1

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."
}
 

Request      

GET api/v1/businesses/{business}/payment-accounts

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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));

Request      

POST api/v1/businesses/{business}/payment-accounts

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

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));

Request      

PATCH api/v1/businesses/{business}/payment-accounts/{paymentAccount_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

paymentAccount_id   integer     

The ID of the paymentAccount. Example: 1

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));

Request      

POST api/v1/businesses/{business}/payment-accounts/transfer

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
from_payment_account_id   integer     

Example: 16

to_payment_account_id   integer     

The value and from_payment_account_id must be different. Example: 16

amount   number     

Example: 4326.41688

transferred_on   string     

Must be a valid date. Example: 2026-01-15

reference   string  optional    

Must not be greater than 64 characters. Example: m

notes   string  optional    

Must not be greater than 255 characters. Example: i

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."
}
 

Request      

GET api/v1/businesses/{business}/payment-accounts/{paymentAccount_id}/reconciliation

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

paymentAccount_id   integer     

The ID of the paymentAccount. Example: 1

Body Parameters
statement_date   string  optional    

Must be a valid date. Must be a date before or equal to today. Example: 2026-01-15

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));

Request      

POST api/v1/businesses/{business}/payment-accounts/{paymentAccount_id}/reconciliation

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

paymentAccount_id   integer     

The ID of the paymentAccount. Example: 1

Body Parameters
statement_date   string     

Must be a valid date. Must be a date before or equal to today. Example: 2026-01-15

statement_balance   number     

Must be between -999999999 and 999999999. Example: -999999998

transaction_ids   integer[]  optional    
notes   string  optional    

Must not be greater than 255 characters. Example: n

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));

Request      

POST api/v1/businesses/{business}/payment-accounts/{paymentAccount_id}/reconcile/{journalLine_id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

paymentAccount_id   integer     

The ID of the paymentAccount. Example: 1

journalLine_id   integer     

The ID of the journalLine. Example: 1

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."
}
 

Request      

GET api/v1/admin/notifications

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
per_page   integer  optional    

Results per page, from 1 to 50. Example: 24

Body Parameters
per_page   integer  optional    

Must be at least 1. Must not be greater than 50. Example: 1

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."
        ]
    }
}
 

Request      

GET api/v1/admin/notifications/audience

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters
state_codes   string[]  optional    

GST state codes to target, for example 33 for Tamil Nadu. Omit to reach every state.

city   string  optional    

City to target, matched case-insensitively against the workspace address. Must not be greater than 120 characters. Example: Coimbatore

store_types   string[]  optional    

Store type slugs to target. Omit to reach every store type.

plan_ids   integer[]  optional    

Plans whose subscribers should be targeted, matched on each workspace's newest subscription.

subscription_statuses   string[]  optional    

Subscription statuses to target: active, trialing, past_due, paused, ended, or none for workspaces that never subscribed.

workspace_status   string  optional    

active, suspended or any. Defaults to active. Example: any

Must be one of:
  • any
  • active
  • suspended
recipients   string  optional    

owners to reach only the business owner, members to reach every active team member. Defaults to owners. Example: owners

Must be one of:
  • owners
  • members

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."
        ]
    }
}
 

Request      

POST api/v1/admin/notifications

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

Body Parameters
state_codes   string[]  optional    

GST state codes to target, for example 33 for Tamil Nadu. Omit to reach every state.

city   string  optional    

City to target, matched case-insensitively against the workspace address. Must not be greater than 120 characters. Example: Coimbatore

store_types   string[]  optional    

Store type slugs to target. Omit to reach every store type.

plan_ids   integer[]  optional    

Plans whose subscribers should be targeted, matched on each workspace's newest subscription.

subscription_statuses   string[]  optional    

Subscription statuses to target: active, trialing, past_due, paused, ended, or none for workspaces that never subscribed.

workspace_status   string  optional    

active, suspended or any. Defaults to active. Example: active

Must be one of:
  • any
  • active
  • suspended
recipients   string  optional    

owners to reach only the business owner, members to reach every active team member. Defaults to owners. Example: owners

Must be one of:
  • owners
  • members
title   string     

Notification title, up to 120 characters. Must not be greater than 120 characters. Example: GST filing window opens tomorrow

body   string     

Notification message, up to 1,000 characters. Must not be greater than 1000 characters. Example: GSTR-3B for this month can be prepared from the GST workspace starting tomorrow.

image   file  optional    

Optional JPEG, PNG or WebP banner, 200x200 to 4000x4000 pixels, maximum 1 MB. Sent as multipart/form-data. Must be an image. Must not be greater than 1024 kilobytes. Example: /path/to/file

image_url   string  optional    

Optional HTTPS URL of an already-hosted banner. Cannot be combined with image. Must be a valid URL. Must not be greater than 2048 characters. Example: https://cdn.example.com/banners/gst.png

link_url   string  optional    

Optional URL delivered as data.url for the client to open when the push is tapped. Must be a valid URL. Must not be greater than 2048 characters. Example: https://app.dukanam.test/gst

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."
        ]
    }
}
 

Request      

GET api/v1/admin/businesses/{business_id}/device-usage

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

Business ID. Example: 1

Query Parameters
from   string  optional    

Start date, YYYY-MM-DD. Example: 2026-09-01

to   string  optional    

End date, YYYY-MM-DD. Example: 2026-09-27

Body Parameters
from   string  optional    

Must be a valid date in the format Y-m-d. Example: 2026-01-15

to   string  optional    

Must be a valid date in the format Y-m-d. Example: 2026-01-15

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."
        ]
    }
}
 

Request      

GET api/v1/admin/businesses/{id}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
id   integer     

Business ID to inspect. Example: 17

Query Parameters
days   integer  optional    

Calendar days including today: 7, 30 or 90. Defaults to 30. Example: 7

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."
        ]
    }
}
 

Request      

GET api/v1/admin/insights/{section}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
section   string     

today, growth, engagement or revenue. Example: engagement

Query Parameters
from   string  optional    

First calendar day, YYYY-MM-DD. Defaults to 29 days before to. Ignored by the today section. Must be a valid date in the format Y-m-d. Example: 2026-01-15

to   string  optional    

Last calendar day, YYYY-MM-DD. Defaults to today. The range may cover at most 366 days. Must be a valid date in the format Y-m-d. Example: 2026-01-15

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."
}
 

Request      

POST api/v1/fcm-test

Headers
Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters
fcm_token   string     

The device registration token to push to. Example: eY2x9_example_fcm_registration_token

title   string     

Notification title, max 120 characters. Example: Stock alert

body   string     

Notification message, max 1000 characters. Example: Rice is running low.

data   object  optional    

optional Flat key/value data payload. Values are delivered as strings.

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."
}
 

Request      

GET api/v1/businesses/{business}/invoices/{invoice}/workers

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

invoice   integer     

The invoice. Example: 1

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
}
 

Request      

GET api/v1/businesses/{business}/workers

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Query Parameters
q   string  optional    

Search name, trade or phone. Example: carpenter

status   string  optional    

Example: active

Must be one of:
  • active
  • archived
  • all
worker_type   string  optional    

Example: referral

Must be one of:
  • referral
  • cook
  • carpenter
  • painter
  • electrician
  • plumber
  • doctor
  • rmp
  • contractor
  • sales_agent
  • other
per_page   integer  optional    

From 1 to 100. Example: 20

Body Parameters
q   string  optional    

Must not be greater than 100 characters. Example: b

status   string  optional    

Example: active

Must be one of:
  • active
  • archived
  • all
worker_type   string  optional    

Example: architecto

per_page   integer  optional    

Must be between 1 and 100. Example: 2

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."
}
 

Request      

GET api/v1/businesses/{business}/workers/{worker}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

worker   string     

The worker. Example: architecto

Query Parameters
period   string  optional    

Example: this-financial-year

Must be one of:
  • today
  • this-month
  • last-month
  • this-financial-year
  • last-financial-year
  • custom
from   string  optional    

Custom period start, Y-m-d; both dates required.

to   string  optional    

Inclusive custom end, Y-m-d.

page   integer  optional    

Statement page. Example: 1

per_page   integer  optional    

From 1 to 100. Example: 20

Body Parameters
per_page   integer  optional    

Must be between 1 and 100. Example: 2

page   integer  optional    

Must be at least 1. Example: 22

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"
}
 

Request      

GET api/v1/businesses/{business}/workers/{worker}/photo

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

worker   string     

The worker. Example: architecto

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
    }
}
 

Request      

POST api/v1/businesses/{business}/workers

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

Body Parameters
name   string     

Referrer's name. Example: Ravi

worker_type   string  optional    

Defaults to referral when omitted. Example: carpenter

Must be one of:
  • referral
  • cook
  • carpenter
  • painter
  • electrician
  • plumber
  • doctor
  • rmp
  • contractor
  • sales_agent
  • other
trade   string  optional    

Trade or referral role. Example: Carpenter

phone   string  optional    

Phone number. Example: 9876543210

email   string  optional    

Email address. Example: [email protected]

aadhaar_number   string  optional    

Optional 12 digits.

pan_number   string  optional    

Optional PAN, 5 letters + 4 digits + 1 letter; trimmed and uppercased. Blank retains the saved value. Example: ABCDE1234F

clear_pan   boolean  optional    

Remove the saved PAN. Example: false

bank_account   object  optional    

Optional payout bank details. Omit or leave all fields blank to retain; partial updates merge with saved details.

bank_name   string  optional    

Bank name, up to 128 characters; required for a new bank account.

account_holder   string  optional    

Account holder, up to 128 characters; required for a new bank account.

account_number   string  optional    

6–34 digits as a string, preserving leading zeroes; required for a new bank account.

ifsc   string  optional    

Valid 11-character IFSC, trimmed and uppercased; required for a new bank account.

branch   string  optional    

Optional branch, up to 128 characters; blank clears the branch when other bank fields are provided.

clear_bank_account   boolean  optional    

Remove all saved bank details. Example: false

contact_id   integer  optional    

Customer account for the referrer's own purchases.

commission_rate_basis_points   integer  optional    

Default percentage, 0 to 10000. Example: 1000

notes   string  optional    

Internal notes, up to 2000 characters.

photo   file  optional    

Private JPEG, PNG or WebP, at most 2 MB and 8192 pixels per side. Omit to retain.

remove_photo   boolean  optional    

Set true to remove the saved photo; cannot be combined with an upload. Example: false

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));

Request      

PUT api/v1/businesses/{business}/workers/{worker}

PATCH api/v1/businesses/{business}/workers/{worker}

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

worker   string     

The worker. Example: architecto

Body Parameters
name   string     

Referrer's name. Example: Ravi

worker_type   string  optional    

Omit to retain the saved type. Example: carpenter

Must be one of:
  • referral
  • cook
  • carpenter
  • painter
  • electrician
  • plumber
  • doctor
  • rmp
  • contractor
  • sales_agent
  • other
trade   string  optional    

Trade or referral role. Example: Carpenter

phone   string  optional    

Phone number. Example: 9876543210

email   string  optional    

Email address. Example: [email protected]

aadhaar_number   string  optional    

Optional 12 digits.

pan_number   string  optional    

Optional PAN, 5 letters + 4 digits + 1 letter; trimmed and uppercased. Blank retains the saved value. Example: ABCDE1234F

clear_pan   boolean  optional    

Remove the saved PAN. Example: false

bank_account   object  optional    

Optional payout bank details. Omit or leave all fields blank to retain; partial updates merge with saved details.

bank_name   string  optional    

Bank name, up to 128 characters; required for a new bank account.

account_holder   string  optional    

Account holder, up to 128 characters; required for a new bank account.

account_number   string  optional    

6–34 digits as a string, preserving leading zeroes; required for a new bank account.

ifsc   string  optional    

Valid 11-character IFSC, trimmed and uppercased; required for a new bank account.

branch   string  optional    

Optional branch, up to 128 characters; blank clears the branch when other bank fields are provided.

clear_bank_account   boolean  optional    

Remove all saved bank details. Example: false

clear_aadhaar   boolean  optional    

Remove the stored identifier. Example: false

contact_id   integer  optional    

Linked own customer account.

commission_rate_basis_points   integer  optional    

Default rate, 0 to 10000. Example: 1000

is_active   boolean  optional    

Whether new referrals can be assigned. Example: true

notes   string  optional    

Internal notes.

photo   file  optional    

Private JPEG, PNG or WebP, at most 2 MB and 8192 pixels per side. Omit to retain.

remove_photo   boolean  optional    

Set true to remove the saved photo; cannot be combined with an upload. Example: false

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));

Request      

POST api/v1/businesses/{business}/workers/{worker}/payouts

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

worker   string     

The worker. Example: architecto

Body Parameters
amount_paise   integer     

Positive paise, no more than current commission due. Example: 30000

occurred_on   string     

Payment date, Y-m-d. Example: 2026-10-04

payment_method   string     

Example: bank

Must be one of:
  • cash
  • bank
  • upi
  • card
  • other
payment_account_id   integer  optional    

Active compatible payment account in this business.

reference   string  optional    

Transfer reference, up to 128 characters.

idempotency_key   string     

Stable UUID for retries. Example: e0e7b8fb-96bb-44bd-95f8-aad1eb6cf473

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));

Request      

POST api/v1/businesses/{business}/workers/{worker}/payouts/{entry}/void

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

worker   string     

The worker. Example: architecto

entry   string     

Example: architecto

Body Parameters
reason   string     

Why the payment is being corrected, up to 1000 characters. Example: Incorrect bank transfer

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"
    }
}
 

Request      

POST api/v1/businesses/{business}/invoices/{invoice}/worker

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

invoice   integer     

The invoice. Example: 1

Body Parameters
attribution_basis_points   integer     

Owner-entered single-referrer share; must be 10000. Example: 10000

worker_id   integer     

Active referrer in this business. Example: 1

rate_basis_points   integer  optional    

Optional override from 0 to 10000; omitted uses the referrer default. Example: 1000

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));

Request      

PUT api/v1/businesses/{business}/invoices/{invoice}/workers

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business   integer     

The business. Example: 1

invoice   integer     

The invoice. Example: 1

Body Parameters
workers   object[]     

Complete assignment set, or [] to remove all.

worker_id   integer     

Active referrer in this business. Example: 1

commission_kind   string     

Example: percentage

Must be one of:
  • percentage
  • fixed
rate_basis_points   integer  optional    

Percentage from 0 to 10000; omitted retains the saved rate or uses the referrer default for a new assignment. Example: 1000

fixed_amount_paise   integer  optional    

Required for fixed commission, 0 to 99999999900.

attribution_basis_points   integer     

Owner-entered share from 1 to 10000; shares total 10000. Example: 6000

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."
}
 

Request      

POST api/v1/businesses/{business_id}/usage-report

Headers
Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters
business_id   integer     

Business ID. Example: 1

Body Parameters
report_id   string     

Unique report UUID. Example: 9b2f6c1e-4a7d-4f0e-9a51-3d2c8e7b6a10

installation_id   string     

Random installation UUID, never a hardware ID. Example: 5d8a1f3c-7e20-4b6a-8c11-0f9e2d4b7a33

reported_at   string     

ISO 8601 timestamp with offset. Example: 2026-09-27T09:14:05+05:30

period_start   string  optional    

Nullable ISO 8601 timestamp with offset. Example: 2026-09-26T08:02:11+05:30

mode   string     

local or server. Example: local

app   object     

Application metadata.

version   string     

Version, maximum 32 characters. Example: 1.1.1

build   integer     

Nonnegative build number. Example: 20

platform   string     

android or ios. Example: android

os_version   string  optional    

Must match the regex /^[a-zA-Z0-9._+-]+$/. Must not be greater than 32 characters. Example: g

locale   string  optional    

Must match the regex /^[a-zA-Z_-]+$/. Must not be greater than 32 characters. Example: en_MT

theme   string  optional    

Must match the regex /^[a-z_]+$/. Must not be greater than 32 characters. Example: m

totals   object  optional    

Running record counts, each integer 0–10000000.

money   object  optional    

Running sums in paise, each integer -100000000000000–100000000000000.

milestones   object  optional    

Nullable ISO timestamps: earliest first_ and onboarding_completedat, latest last.

first_invoice_at   string  optional    

Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D. Example: 2026-01-15

last_invoice_at   string  optional    

Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D. Example: 2026-01-15

first_payment_at   string  optional    

Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D. Example: 2026-01-15

last_payment_at   string  optional    

Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D. Example: 2026-01-15

first_record_at   string  optional    

Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D. Example: 2026-01-15

last_record_at   string  optional    

Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D. Example: 2026-01-15

onboarding_completed_at   string  optional    

Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D. Example: 2026-01-15

first_share_at   string  optional    

Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D. Example: 2026-01-15

events   object  optional    

At most 300 action deltas, each integer 0–100000.