📌 Inbound Webhook — custom_api channel

Lekhapal Actions API

Push accounting events from your platform into Lekhapal using a single HMAC-secured webhook endpoint. All transaction types — sales, purchases, payments, expenses and parties — are routed by an event field in the JSON body.

POST aujarmart.pasaledai.com/api/omnichannel/webhook/custom_api
💡
This endpoint is rate-limited to 120 requests per minute. No Bearer token is needed — the HMAC signature in X-Webhook-Signature is the only authentication mechanism required.
01 Authentication

Requests are authenticated using a shared webhook secret configured on the Lekhapal platform connection. There is no Bearer token; the HMAC signature in the X-Webhook-Signature header serves as the sole credential.

🔒
Fail-closed policy: If the connection does not have a secret configured, or if the signature header is missing, the request is rejected with 401 Unauthorized. There is no unauthenticated fallback.
HeaderRequiredDescription
Content-Type Required Must be application/json
X-Webhook-Signature Required HMAC-SHA256 hex digest of the raw request body, keyed with your webhook secret. See §02 for computation.
X-Lekhapal-Event Optional Explicit event name (e.g. sale.created). Falls back to the event field in the request body if omitted.

02 HMAC Signature

The signature authenticates the request body and prevents replay or tampering. Compute it by running HMAC-SHA256 over the exact raw body bytes you will send, using the webhook secret as the key, then encode the result as a lowercase hexadecimal string.

1

Serialise the body

Produce the exact JSON string you will send as the request body. Do not pretty-print unless you send those exact bytes — any whitespace difference will break the signature.

2

Compute HMAC-SHA256

Key: your webhook_secret. Message: the raw body string. Output: lowercase hex digest (no prefix).

3

Set the header

Add X-Webhook-Signature: <hex-digest> to the request. No sha256= prefix is needed.

JavaScript / Node.js
import crypto from 'crypto'; const secret = 'YOUR_WEBHOOK_SECRET'; const body = JSON.stringify(payload); // exact bytes you will send const sig = crypto .createHmac('sha256', secret) .update(body) .digest('hex'); await fetch('https://aujarmart.pasaledai.com/api/omnichannel/webhook/custom_api', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Webhook-Signature': sig, }, body, });
PHP
$secret = 'YOUR_WEBHOOK_SECRET'; $payload = json_encode($data); // exact bytes to send $sig = hash_hmac('sha256', $payload, $secret); $ch = curl_init('https://aujarmart.pasaledai.com/api/omnichannel/webhook/custom_api'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-Webhook-Signature: ' . $sig, ], CURLOPT_RETURNTRANSFER => true, ]); $response = curl_exec($ch);
Python
import hmac, hashlib, json, requests secret = 'YOUR_WEBHOOK_SECRET' body = json.dumps(payload, separators=(',', ':')) # compact JSON sig = hmac.new( secret.encode('utf-8'), body.encode('utf-8'), hashlib.sha256, ).hexdigest() requests.post( 'https://aujarmart.pasaledai.com/api/omnichannel/webhook/custom_api', data=body, headers={ 'Content-Type': 'application/json', 'X-Webhook-Signature': sig, }, )
Postman — Pre-request Script
// Global variable: webhook_secret const secret = pm.globals.get('webhook_secret'); if (!secret) throw new Error('webhook_secret is missing!'); let body = pm.request.body.raw; if (!body) throw new Error('Request body is empty!'); body = body.trim(); // Compute HEX HMAC-SHA256 const sig = CryptoJS.HmacSHA256(body, secret).toString(CryptoJS.enc.Hex); // Inject header automatically pm.request.headers.upsert({ key: 'X-Webhook-Signature', value: sig, });
⚠️
Compute the signature from the exact bytes sent over the wire. Signing a re-serialised or pretty-printed copy of the payload will produce a mismatch and a 401 response.

03 Request Format

All actions share the same endpoint URL. The event field in the JSON body determines which Lekhapal action is triggered. Supported events are listed below.

sale.created
sale.updated
sale.approved
sale.voided
sale_return.created
sale_return.updated
sale_return.approved
sale_return.voided
purchase.created
purchase.updated
purchase.approved
purchase.voided
purchase_return.created
purchase_return.updated
purchase_return.approved
purchase_return.voided
customer_payment.created
customer_payment.updated
customer_payment.approved
customer_payment.voided
supplier_payment.created
supplier_payment.updated
supplier_payment.approved
supplier_payment.voided
expense.created
expense.updated
expense.approved
expense.voided
party.created
party.updated

04 Lifecycle States

All transactional resources (sales, purchases, payments, expenses) follow a common lifecycle. Only approved records affect accounting ledgers. A voided record is soft-deleted and cannot be modified further.

pending
approved
or
pending
voided
Event suffixActionLedger impact
.created Creates a new record in pending status None — draft only
.updated Updates fields on an existing record. Pass the record's id. None while pending
.approved Finalises the record — posts to accounting ledgers ✓ Posts journal entries
.voided Soft-deletes the record with an optional void_reason Reverses any posted entries

Sales

Create and manage sales invoices with line-item detail. The details array must contain at least one product line.

sale.created

FieldTypeRequiredDescription
eventstringRequired"sale.created"
datestringRequiredInvoice date in YYYY-MM-DD format
client_iduuidRequiredID of the customer (party) record
warehouse_idstringRequiredWarehouse identifier
currency_iduuidRequiredCurrency record ID
exchange_rate_to_basenumberRequiredExchange rate to base currency. Use 1 for base currency.
GrandTotalnumberRequiredTotal including tax and shipping, excluding discounts already applied
statutstringRequired"pending" or "approved"
tax_ratenumberRequiredTax rate percentage (e.g. 13 for 13%)
TaxNetnumberRequiredTotal tax amount
discountnumberRequiredHeader-level discount value
discount_typestringRequired"percentage" or "fixed"
shippingnumberRequiredShipping charge amount
detailsarrayRequiredLine items array — see detail fields below
notesstringOptionalFree-text notes
credit_terms_iduuid|nullOptionalPayment credit terms
is_export_salesbooleanOptionalFlag for export sales
tds_applicablebooleanOptionalWhether TDS is applicable

details[ ] — Line Item Fields

FieldTypeDescription
product_iduuidLekhapal product ID
product_variant_iduuid|nullVariant ID, or null if no variant
quantitynumberQuantity sold
Unit_pricenumberUnit selling price (before tax)
Net_pricenumberPrice after discount, before tax
tax_percentnumberTax percentage for this line
tax_methodstring"1" = inclusive, "2" = exclusive
discountnumberLine-level discount value
discount_Methodstring"fixed" or "percentage"
DiscountNetnumberComputed discount amount
subtotalstringLine total as string (e.g. "275.00")
sale_unit_iduuidUnit of measure for sale
detail_idnumberSequence number within this invoice (1, 2, 3…)
Example — sale.created
{ "event": "sale.created", "date": "2026-04-28", "client_id": "07d6046a-e773-4a77-8bac-78d679aa9c29", "warehouse_id": "1", "currency_id": "f363d18d-2a45-4def-80fe-3865f589358f", "exchange_rate_to_base": 1, "GrandTotal": 275, "statut": "pending", "tax_rate": 13, "TaxNet": 0, "discount": 0, "discount_type": "percentage", "shipping": 0, "notes": "", "credit_terms_id": null, "is_export_sales": false, "tds_applicable": false, "details": [ { "product_id": "9f3cac5d-3c49-4072-a959-d2cca7259054", "product_variant_id": null, "quantity": 1, "Unit_price": 275, "Net_price": 275, "tax_percent": 0, "tax_method": "2", "discount": 0, "discount_Method": "fixed", "DiscountNet": 0, "sale_unit_id": "9ef4e768-61ed-4d69-a5b8-69787c9db626", "detail_id": 1, "subtotal": "275.00" } ] }

sale.updated

Same structure as sale.created plus a required id field (the Lekhapal sale UUID to update) and the event set to "sale.updated". Pass the full details array.

sale.approved & sale.voided

Approve
{ "event": "sale.approved", "id": "a1a6c688-f1e7-4b74-a6cd-9b2b5e92f968" }
Void
{ "event": "sale.voided", "id": "a1a6c688-f1e7-4b74-a6cd-9b2b5e92f968", "void_reason": "voided" }

Sales Return

Record a credit note against an existing approved sale. Must reference the original sale_id.

FieldTypeRequiredDescription
eventstringRequired"sale_return.created"
datestringRequiredReturn date YYYY-MM-DD
client_iduuidRequiredCustomer ID
sale_iduuidRequiredOriginal sale UUID being returned
warehouse_idstringRequiredWarehouse
currency_iduuidRequiredCurrency
exchange_rate_to_basenumberRequiredExchange rate
GrandTotalnumberRequiredReturn total
statutstringRequired"pending"
tax_ratenumberRequiredTax rate %
TaxNetnumberRequiredTax amount
discountnumberRequiredDiscount
discount_typestringRequired"fixed" or "percentage"
shippingnumberRequiredShipping charge
detailsarrayRequiredReturned line items (same structure as sale details)
Approve / Void — Sale Return
// Approve { "event": "sale_return.approved", "id": "a1a6e2f5-4a1d-4af5-9ec2-ad1c709b8670" } // Void { "event": "sale_return.voided", "id": "a1a6e2f5-4a1d-4af5-9ec2-ad1c709b8670", "void_reason": "voided" }

Purchase

Create and manage purchase invoices from suppliers. Uses supplier_id and cost fields (Unit_cost, Net_cost) instead of client and price fields.

FieldTypeRequiredDescription
eventstringRequired"purchase.created"
datestringRequiredYYYY-MM-DD
supplier_iduuidRequiredSupplier (party) ID
warehouse_idstringRequiredWarehouse
currency_iduuidRequiredCurrency
exchange_rate_to_basenumberRequiredExchange rate
GrandTotalnumberRequiredInvoice total
statutstringRequired"pending"
referencestringOptionalSupplier invoice / reference number
tax_ratenumberRequiredTax rate %
TaxNetnumberRequiredTax amount
discountnumberRequiredDiscount
discount_typestringRequired"percentage" or "fixed"
shippingnumberRequiredFreight / shipping
is_import_purchasesbooleanOptionalFlag for import purchases
detailsarrayRequiredLine items — use Unit_cost, Net_cost
Approve / Void — Purchase
// Approve { "event": "purchase.approved", "id": "a1a6d45b-5c84-47fa-9fa5-4711fdc7357a" } // Void { "event": "purchase.voided", "id": "a1a6d45b-5c84-47fa-9fa5-4711fdc7357a", "void_reason": "voided" }

Purchase Return

Record returns of goods back to a supplier (debit note) against an existing purchase. Requires the original purchase_id.

FieldTypeRequiredDescription
eventstringRequired"purchase_return.created"
datestringRequiredYYYY-MM-DD
supplier_iduuidRequiredSupplier ID
purchase_iduuidRequiredOriginal purchase UUID
warehouse_idstringRequiredWarehouse
currency_iduuidRequiredCurrency
exchange_rate_to_basenumberRequiredExchange rate
GrandTotalnumberRequiredReturn total
statutstringRequired"pending"
tax_rate / TaxNet / discount / shippingnumberRequiredSame as Purchase
detailsarrayRequiredReturned line items
Approve / Void — Purchase Return
// Approve { "event": "purchase_return.approved", "id": "a19ebf0c-41dd-44ec-be74-b9576c8864b8" } // Void { "event": "purchase_return.voided", "id": "a19ebf0c-41dd-44ec-be74-b9576c8864b8", "void_reason": "voided" }

Customer Payment

Record a payment received from a customer. Payments are applied to the customer's outstanding balance.

FieldTypeRequiredDescription
eventstringRequired"customer_payment.created"
datestringRequiredPayment date YYYY-MM-DD
client_iduuidRequiredCustomer (party) ID
warehouse_idstringRequiredWarehouse
account_iduuidRequiredBank / cash account to deposit into
amountnumberRequiredPayment amount
payment_modestringRequirede.g. "Cash", "Bank Transfer", "Cheque"
payment_referencestringOptionalCheque number, transfer reference, etc.
currency_iduuidRequiredCurrency
exchange_ratenumberRequiredExchange rate
bank_charge_applicablebooleanOptionalWhether a bank charge is deducted
bank_charge_amountnumber|nullOptionalBank charge amount
tds_applicablebooleanOptionalWhether TDS is withheld
tds_amountnumberOptionalTDS amount
notesstringOptionalFree-text note
Example — customer_payment.created
{ "event": "customer_payment.created", "date": "2026-04-24", "client_id": "07d6046a-e773-4a77-8bac-78d679aa9c29", "warehouse_id": "1", "account_id": "9ef68f61-8f9a-4409-9c6a-af1cf200bd50", "amount": 1500, "payment_mode": "Cash", "payment_reference": "REF", "currency_id": "f363d18d-2a45-4def-80fe-3865f589358f", "exchange_rate": 1, "bank_charge_applicable": false, "bank_charge_amount": null, "tds_applicable": false, "tds_amount": 0, "notes": "" }
Approve / Void — Customer Payment
// Approve { "event": "customer_payment.approved", "id": "a19eac94-fd0a-403c-b950-162bfe360164" } // Void { "event": "customer_payment.voided", "id": "a1a6c688-f1e7-4b74-a6cd-9b2b5e92f968", "void_reason": "voided" }

Supplier Payment

Record a payment made to a supplier. Identical structure to Customer Payment — uses supplier_id instead of client_id.

Example — supplier_payment.created
{ "event": "supplier_payment.created", "date": "2026-04-28", "supplier_id": "07d6046a-e773-4a77-8bac-78d679aa9c29", "warehouse_id": "1", "account_id": "9ef68f61-8f9a-4409-9c6a-af1cf200bd50", "amount": 1500, "payment_mode": "Cash", "payment_reference": "REF", "currency_id": "f363d18d-2a45-4def-80fe-3865f589358f", "exchange_rate": 1, "bank_charge_applicable": false, "bank_charge_amount": null, "tds_applicable": false, "tds_amount": 0, "notes": "" }
Approve / Void — Supplier Payment
// Approve { "event": "supplier_payment.approved", "id": "a1a6e5df-05d6-4ea2-84f8-59343a04706e" } // Void { "event": "supplier_payment.voided", "id": "a1a6e5df-05d6-4ea2-84f8-59343a04706e", "void_reason": "voided" }

Expense

Record business expenses categorised against chart-of-account lines. The details array maps each expense line to a chart-of-account entry.

FieldTypeRequiredDescription
eventstringRequired"expense.created"
datestringRequiredExpense date YYYY-MM-DD
due_datestringOptionalPayment due date YYYY-MM-DD
supplier_iduuidRequiredSupplier / vendor party ID
warehouse_idstringRequiredWarehouse
currency_iduuidRequiredCurrency
exchange_rate_to_basenumberRequiredExchange rate
referencestringOptionalBill / voucher reference
amountnumberRequiredTotal expense amount
TaxNetnumberRequiredTax amount
notestringOptionalFree-text note
detailsarrayRequiredExpense lines — see below

details[ ] — Expense Line Fields

FieldTypeDescription
accountobjectChart-of-account object with at minimum id, name, type
amountstring|numberAmount for this expense line
tax_percentstringTax percentage or "N/A"
subtotalnumberLine subtotal
Example — expense.created
{ "event": "expense.created", "date": "2026-04-24", "due_date": "2026-04-24", "supplier_id": "9ef6979b-542a-4255-8443-c7430f025ee0", "warehouse_id": "1", "currency_id": "f363d18d-2a45-4def-80fe-3865f589358f", "exchange_rate_to_base": 1, "reference": "123", "amount": 1000, "TaxNet": 0, "note": "", "details": [ { "account": { "id": "32", "name": "Miscellaneous Expense", "type": "Chart Account", "parent_id": "15", "unique_id": "chart_account_32" }, "amount": "1000", "tax_percent": "N/A", "subtotal": 1000 } ] }
Approve / Void — Expense
// Approve { "event": "expense.approved", "id": "a19ebfb8-7768-480b-bfb5-e60a5f71ab80" } // Void { "event": "expense.voided", "id": "a19ebfb8-7768-480b-bfb5-e60a5f71ab80", "void_reason": "voided" }

Party

Create or update a party (customer or supplier) record in Lekhapal. Update requires the party's id.

FieldTypeRequiredDescription
eventstringRequired"party.created" or "party.updated"
iduuidUpdate onlyExisting party UUID — required for party.updated
namestringRequiredFull name of the party
emailstringOptionalEmail address
phonestringOptionalContact number
countrystringOptionalCountry name (e.g. "Nepal")
citystring|nullOptionalCity
addressstringOptionalStreet address
panstring|nullOptionalPAN / tax identification number
Examples — party.created & party.updated
// Create { "event": "party.created", "name": "Mohan Enterprises", "email": "mohan@example.com", "phone": "9800000000", "country": "Nepal", "city": "Kathmandu", "address": "New Road", "pan": null } // Update (requires id) { "event": "party.updated", "id": "a1a6c44e-d619-4de7-ae9f-36e16063bb3e", "name": "Mohan Enterprises Updated", "email": "mohan@example.com", "phone": "9800000000", "country": "Nepal", "city": null, "address": "New Road", "pan": null }

05 HTTP Responses

All responses are JSON. A 200 confirms the event was accepted and queued for processing — it does not guarantee the downstream action succeeded.

200 OK
Event accepted. Body: {"message":"Received."}
401 Unauthorized
Signature missing, invalid, or no secret configured on connection. Body: {"error":"Unauthorized."}
404 Not Found
Unknown channel slug or no active connection. Body: {"error":"Unknown channel."} or {"error":"No active connection for this channel."}
429 Too Many Requests
Rate limit exceeded (120 req/min). Retry after 60 seconds.

06 Rate Limits

The webhook endpoint enforces a sliding-window rate limit of 120 requests per 1-minute window per IP address.

💡
For bulk imports, batch your events and space them out. Implement exponential back-off when you receive a 429 response. The server does not send a Retry-After header — assume a 60-second cooldown.

07 Error Codes & Troubleshooting

Common rejection reasons and how to resolve them.

ReasonHTTPResolution
connection_has_no_secret 401 Ask the Lekhapal admin to configure a webhook secret on the custom_api connection.
missing_signature_header 401 The X-Webhook-Signature header was not sent. Ensure your signing logic sets this header on every request.
signature_mismatch 401 The computed HMAC does not match. Verify you are signing the exact raw body bytes — not a re-serialised or pretty-printed copy.
timestamp_too_old 401 If you send X-Webhook-Timestamp, the request must arrive within 5 minutes of that timestamp. Remove the header if you are not using the timestamp-based signing variant.
Unknown channel 404 The channel slug in the URL is wrong. Use custom_api exactly.
No active connection 404 No enabled platform connection exists for custom_api. Ask admin to activate the connection in Lekhapal settings.
⚠️
Optional timestamp signing: If you include an X-Webhook-Timestamp header (Unix seconds), the server switches to timestamp-prefixed signing: HMAC-SHA256("{timestamp}.{body}") and expects a sha256= prefix on the signature. The Postman collection does not use timestamps — simply sign the raw body without a prefix for the simplest integration.