Zeyora Documentation

Zeyora API reference for developers

Use these endpoint notes and payload examples to connect websites, external systems and finance workflows to Zeyora.

Website integration APIs

Developer integration guide Reference updated: 7 October 2026

Connect your own website design to Zeyora without an iframe. These three integrations store data in different modules: enquiries in CRM Leads, CV applications in HRM, and newsletter contacts in Marketing. Multiple website forms can use the same authorised integration; give each form a descriptive form_name.

Backend only. Browser → your website backend → Zeyora. Never put API keys or secrets in frontend JavaScript, HTML, public repositories or screenshots. Your backend must verify CAPTCHA and consent before signing requests. A signed request is not a substitute for spam prevention.

Start here: what you need

This page is the technical handoff; no separate instruction email is needed. The tenant owner must privately provide only: the tenant HTTPS origin, an authorised API key and secret, which integrations are enabled, and a test contact/submission policy. Do not embed credentials in this page's URL. Your own website backend must have a CAPTCHA secret and, for newsletters, an email-confirmation delivery mechanism.

  1. Choose the module below. Save credentials on your backend, not in the website's frontend build environment.
  2. For careers, fetch the schema first. Map your form to the documented payload and validate it on your backend.
  3. Verify CAPTCHA/consent, create one UUID, serialize once, sign the exact bytes and submit.
  4. Check the HTTP status and module response; confirm the record with the tenant owner. Handle retries without creating a new UUID.

Sharing: send this page link, or the Leads, Careers or Subscriptions section link. Send secrets through a separate secure channel. Public documentation does not grant API access or automatically enable a tenant.

Use your tenant's HTTPS domain, not the public documentation website. Example tenant: https://cla.zeyora.app. Replace it with your own tenant domain. Keep the /crm or /hrm prefix shown below; do not add both.

MethodFull pathPurpose
POST/crm/api/v1/leads/websiteCreate a CRM website lead
GET/hrm/career_api/formRead tenant career fields and upload rules
POST/hrm/career_api/submitSubmit candidate details and CV
POST/crm/marketing_api/subscribeRecord newsletter consent / verified subscription
POST/crm/marketing_api/unsubscribeRecord opt-out / suppression

Tenant setup & authentication

  1. A tenant administrator creates an active API key in existing CRM Settings → API Keys. Send the key and secret privately to a trusted backend developer.
  2. For careers, open HRM Career Form Settings → Website Integration, select the existing CRM key and enable/save the integration. The settings page is /hrm/career_form_settings. Shared CRM keys do not require HRM_CAREER_API_KEYS.
  3. For subscriptions, an authorised manager enables Marketing → Email Subscribers. The server administrator adds the permitted key to the comma-separated PHP environment variable MARKETING_SUBSCRIPTION_API_KEYS and reloads PHP. This allowlist is still required for Marketing.
  4. Store secrets in your website backend's secret manager/environment. Configure HTTPS, clock synchronisation, CAPTCHA hostname validation, honeypot checks and request/IP limits. Never trust browser-supplied “verified” flags.

Enabling optional integrations can initialise their tenant-specific settings/receipt tables. API requests do not enable an unused module. No separate Career API SQL file is needed when using the shared-key setup; deploy the matching application release first. These are shared legacy credentials, not automatically restricted to one module: only give them to trusted server-side integrations. Revoking the CRM key stops its API access.

Every request includes these headers. The timestamp must be UTC Unix seconds within 300 seconds of server time. The signature is a lowercase 64-character hexadecimal HMAC-SHA256, keyed with the API secret.

X-Zeyora-Key: YOUR_API_KEY
X-Zeyora-Timestamp: UNIX_TIMESTAMP_SECONDS
X-Zeyora-Signature: LOWERCASE_HEX_HMAC_SHA256
OperationExact string to sign
Lead POSTTIMESTAMP.EXACT_JSON_BODY
Career schema GETschema.TIMESTAMP..-
Career submission POSTsubmit.TIMESTAMP.EXACT_PAYLOAD_JSON.CV_SHA256
Subscribe POSTmarketing.subscribe.TIMESTAMP.EXACT_JSON_BODY
Unsubscribe POSTmarketing.unsubscribe.TIMESTAMP.EXACT_JSON_BODY

Dots above are literal separators. Serialize JSON once, sign that exact string and send it unchanged. Career signatures use the JSON string in the multipart payload field and the lowercase SHA-256 of the actual CV bytes—not the entire multipart body. The GET schema signature contains two consecutive dots and ends with a hyphen.

Use a UUID as idempotency_key for each distinct action. On a timeout or transient failure, keep the same UUID, exact JSON and CV bytes, but regenerate the timestamp and signature. Changed payloads or a confirmation/opt-out action require a new UUID. Career and Marketing reject conflicting UUID reuse with HTTP 409.

CRM: website leads

POST /crm/api/v1/leads/website · Content-Type: application/json · maximum JSON body 20 KB.

{
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000",
  "form_name": "Website Contact Us",
  "full_name": "Sample Customer",
  "email": "customer@example.com",
  "phone": "+971501234567",
  "company": "Example Company",
  "message": "Please contact me about your services.",
  "custom_fields": [
    {"label": "Service interest", "value": "Audit & Assurance"},
    {"label": "Preferred contact", "value": ["Email", "Phone"]}
  ],
  "spam_protection": {
    "captcha_verified": true,
    "honeypot_filled": false
  }
}

idempotency_key, form_name (up to 120 characters) and full_name (up to 150) are required. Provide a valid email address or phone number. Optional tenant-specific lead_source_id, lead_status_id and assigned_to must be valid IDs from that tenant; obtain them from the tenant administrator, not another tenant. If omitted, source/status use 0 and the assignee defaults to the key's creating user; that user must still be a valid active tenant employee.

Additional lead limits: UUID/key up to 100 characters, email 255, phone 80, company 255. Length checks in these APIs are byte-based; multi-byte Unicode text may reach a limit sooner than the character count suggests.

Additional text, textarea, dropdown, radio and checkbox values can be sent in custom_fields; they are appended as labelled information to the lead description. No lead file upload is supported here. Maximum 50 custom fields, label 150 characters, scalar value 2,000 characters, or up to 20 array items of 255 characters each. The combined custom-field text is limited to 6,000 characters; the separate message accepts up to 3,000.

Successful creation returns HTTP 201 with status: "created", lead_id and request_id. Duplicate requests return HTTP 200 with status: "duplicate". A short-window email/phone duplicate check also applies; do not assume every separate submission creates another lead.

{
  "result": "Lead created successfully.",
  "status": "created",
  "lead_id": 123,
  "request_id": 456
}

The numeric IDs above are illustrative response values, not input IDs. Duplicate responses may omit lead_id and request_id; do not require these fields to recognise an accepted duplicate.

HRM: career applications with CV upload

1. Read the tenant's form

Send a signed GET /hrm/career_api/form with no request body. A successful response includes status: true, fields, max_upload_mb, allowed_extensions and submit_url. Build the website using each field's field_key, type, required flag, choices and validation limits. The tenant schema is authoritative: required fields and dropdown choices can differ between tenants.

2. Submit details and one CV

Send POST /hrm/career_api/submit as multipart/form-data, with exactly a payload JSON string and a resume file. Let your HTTP library generate the multipart boundary. Do not send a CRM lead payload or a JSON-only request.

Career payload JSON is limited to 20,000 bytes, with up to 50 entries in fields. Career and Marketing idempotency keys must be 16–100 ASCII letters, numbers, underscores or hyphens; standard UUIDs are suitable. Form names are limited to 120 bytes. Submit career field values as strings; use "1" for checked privacy consent and numeric strings for number fields.

{
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440010",
  "form_name": "Website Careers - Let's Connect",
  "fields": {
    "first_name": "Sample",
    "last_name": "Candidate",
    "email_address": "candidate@example.com",
    "mobile_number": "+971501234567",
    "current_location": "Dubai",
    "nationality": "Example nationality",
    "current_employer": "Example Company",
    "current_position": "Accountant",
    "years_of_experience": "5",
    "service_line_interested_in": "Audit & Assurance",
    "linkedin_profile_url": "https://www.linkedin.com/in/example",
    "cover_note": "I would like to join your team.",
    "availability_to_join": "1 month",
    "expected_salary": "AED 12000",
    "source": "LinkedIn",
    "privacy_consent": "1"
  },
  "spam_protection": {
    "captcha_verified": true,
    "honeypot_filled": false
  }
}

This example matches common career fields, not a fixed schema. Replace choices with exact values returned by the form endpoint. Default service lines include Audit & Assurance, Tax, Accounting & Business Advisory, Corporate Services, Digital & Advisory and Administration & Support Functions.

Mapping an existing website form

Website inputZeyora field
First / last namefields.first_name / fields.last_name
Email / country code + phonefields.email_address / fields.mobile_number (combine dial code and number)
Location / employer / position / experiencecurrent_location, current_employer, current_position, years_of_experience inside fields
Department / service interestfields.service_line_interested_in
LinkedIn / additional informationfields.linkedin_profile_url / fields.cover_note
Availability / salary / source / consentavailability_to_join, expected_salary, source, privacy_consent inside fields
CV fileMultipart file field resume, outside the JSON

Custom dropdown components must submit their selected values from component state; visual controls alone may not have HTML input names. Unknown fields are not a substitute for configuring the tenant schema.

Upload security & storage

Accept only the extensions returned by the schema (PDF, DOC and/or DOCX as configured), at the returned upload limit, never over the API's 20 MB ceiling. The server validates file type/content and scans files; antivirus availability must be verified before go-live. Files go to the configured private S3 bucket under the tenant's career_cvs prefix. Block public access and use authorised staff downloads; never expose permanent public CV URLs.

The tenant's S3 access and upload/scanner configuration must be ready before testing uploads. Retention is not automatic: an authorised retention policy and cleanup process must be configured separately. Existing-email duplicate protection can preserve the earlier candidate/CV rather than replacing it. This endpoint is a general CV collection, not automatic submission to a specific vacancy.

Creation normally returns HTTP 201; duplicates return 200. Inspect status, state and the returned submission details. Keep the exact JSON and file for retries. A reused UUID with different JSON or CV bytes returns 409.

Marketing: email subscriptions

These contacts appear in Marketing → Email Subscribers, not CRM Leads or HRM. This release provides one tenant-owned list, deduplicated by normalised email, with form/source and consent audit history. Separate subscriber groups, campaigns and automatic confirmation emails are not included.

1. Record initial consent

POST /crm/marketing_api/subscribe · JSON maximum 8 KB. Require an affirmative marketing consent checkbox and send the actual displayed wording.

{
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440020",
  "form_name": "Website Newsletter",
  "full_name": "Sample Subscriber",
  "email": "subscriber@example.com",
  "consent": true,
  "consent_text": "I agree to receive marketing email updates.",
  "privacy_version": "2026-10",
  "double_opt_in_verified": false,
  "spam_protection": {
    "captcha_verified": true,
    "honeypot_filled": false
  }
}

Limits: name 190 characters, form name 120, email 254, consent text 1,000 and privacy version 100. Boolean values must be JSON booleans, not strings. Initial contacts are pending, not eligible for marketing sends.

2. Confirm email ownership

Your backend sends and verifies a short-lived, single-use email confirmation link. After confirmation, send another signed subscribe request with a new UUID, the same contact/consent information, and these fields:

{
  "double_opt_in_verified": true,
  "verified_at": "CURRENT_UTC_TIME_IN_YYYY-MM-DDTHH:MM:SSZ_FORMAT",
  "verification_reference": "your-non-secret-confirmation-receipt-id"
}

The snippet above is merged into the complete subscribe payload. Use a real UTC timestamp from the last 24 hours (maximum 60 seconds future tolerance). Do not send confirmation tokens in the reference. Zeyora trusts the authenticated backend's attestation; CAPTCHA alone does not verify email ownership. Only contacts with subscribed status and a recorded verification time may be used for future sends.

3. Record opt-out

Send POST /crm/marketing_api/unsubscribe, using the unsubscribe signature:

{
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440021",
  "form_name": "Website Preferences",
  "email": "subscriber@example.com",
  "spam_protection": {
    "captcha_verified": true,
    "honeypot_filled": false
  }
}

Opt-out creates a suppression record even for an unknown email. Repeated signups do not reactivate unsubscribed/suppressed contacts. A separately authorised re-subscription workflow is not implemented. This website endpoint requires backend CAPTCHA verification; it is not an RFC one-click email unsubscribe endpoint. Keep your mailing provider's opt-out process available independently.

Successful subscribe/opt-out responses use HTTP 200 with {"status":true,"result":"Request received."}, deliberately not revealing whether a contact already exists. Do not treat this generic response as proof of a subscribed state.

Node.js backend examples

Node.js 20+ with built-in fetch/FormData. Set ZEYORA_BASE_URL to your tenant origin (without a module suffix), and set ZEYORA_API_KEY / ZEYORA_API_SECRET in the backend environment. These are example website-backend variable names, not additional Zeyora settings. Validate your website input and independently verify CAPTCHA before calling these helpers.

import { createHmac, createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { basename } from 'node:path';

const base = process.env.ZEYORA_BASE_URL?.replace(/\/+$/, '');
const key = process.env.ZEYORA_API_KEY;
const secret = process.env.ZEYORA_API_SECRET;
if (!base || !key || !secret || !base.startsWith('https://')) {
  throw new Error('Configure the tenant HTTPS origin and backend credentials.');
}
function signedHeaders(message, timestamp) {
  return {
    'X-Zeyora-Key': key,
    'X-Zeyora-Timestamp': timestamp,
    'X-Zeyora-Signature': createHmac('sha256', secret)
      .update(message, 'utf8').digest('hex')
  };
}
async function result(response) {
  let data;
  try { data = await response.json(); }
  catch { throw new Error('Non-JSON response: HTTP ' + response.status); }
  if (!response.ok || data.status === false || data.error) {
    // Log only sanitised errors; never log secrets, CVs or full personal data.
    throw new Error('Zeyora request failed: HTTP ' + response.status);
  }
  return data;
}
export async function sendLead(rawJson) {
  const time = String(Math.floor(Date.now() / 1000));
  return result(await fetch(base + '/crm/api/v1/leads/website', {
    method: 'POST', signal: AbortSignal.timeout(20000),
    headers: { ...signedHeaders(time + '.' + rawJson, time),
      'Content-Type': 'application/json' },
    body: rawJson
  }));
}
export async function getCareerSchema() {
  const time = String(Math.floor(Date.now() / 1000));
  return result(await fetch(base + '/hrm/career_api/form', {
    headers: signedHeaders('schema.' + time + '..-', time),
    signal: AbortSignal.timeout(20000)
  }));
}
export async function sendCareer(rawPayloadJson, cvPath) {
  const cv = await readFile(cvPath);
  const digest = createHash('sha256').update(cv).digest('hex');
  const time = String(Math.floor(Date.now() / 1000));
  const form = new FormData();
  form.append('payload', rawPayloadJson);
  form.append('resume', new Blob([cv]), basename(cvPath));
  return result(await fetch(base + '/hrm/career_api/submit', {
    method: 'POST', signal: AbortSignal.timeout(60000),
    headers: signedHeaders('submit.' + time + '.' + rawPayloadJson + '.' + digest, time),
    body: form
  }));
}
export async function sendSubscription(operation, rawJson) {
  if (!['subscribe', 'unsubscribe'].includes(operation)) {
    throw new Error('Unsupported subscription operation');
  }
  const time = String(Math.floor(Date.now() / 1000));
  const message = 'marketing.' + operation + '.' + time + '.' + rawJson;
  return result(await fetch(base + '/crm/marketing_api/' + operation, {
    method: 'POST', signal: AbortSignal.timeout(20000),
    headers: { ...signedHeaders(message, time),
      'Content-Type': 'application/json' },
    body: rawJson
  }));
}
// Serialize each complete payload ONCE:
// const raw = JSON.stringify(validatedPayload);
// await sendLead(raw);
// On a retry, keep raw/idempotency_key unchanged.

Production callers must add bounded timeouts, exponential backoff and safe response handling. Preserve the submission UUID and serialized payload across retries; do not generate them inside the retry loop. Retry only transient failures. Do not forward browser Origin headers or call these APIs from a public frontend. The helpers above do not send anything until your backend calls them.

Run a backend smoke test

Save the helper code above as zeyora-api.mjs and the following script beside it as test-zeyora.mjs. Configure the three backend environment variables using your secret manager. Save a complete example payload from the relevant section as a private JSON file, replace the example values and use a fresh UUID for each new action. Submission tests create real records: use an authorised test tenant and non-sensitive test data. The schema test below is read-only.

import { readFile } from 'node:fs/promises';
import { sendLead, getCareerSchema, sendCareer, sendSubscription } from './zeyora-api.mjs';
const [operation, payloadPath, cvPath] = process.argv.slice(2);
let response;
if (operation === 'career-schema') {
  response = await getCareerSchema();
} else {
  if (!payloadPath) throw new Error('Provide a complete payload JSON file');
  const raw = await readFile(payloadPath, 'utf8');
  JSON.parse(raw); // Validate JSON locally; send the original bytes unchanged.
  if (operation === 'lead') response = await sendLead(raw);
  else if (operation === 'career') {
    if (!cvPath) throw new Error('Provide the test CV path');
    response = await sendCareer(raw, cvPath);
  } else if (['subscribe', 'unsubscribe'].includes(operation)) {
    response = await sendSubscription(operation, raw);
  } else throw new Error('Use career-schema, lead, career, subscribe or unsubscribe');
}
console.log(response); // Local test only; keep output out of public logs.
node test-zeyora.mjs career-schema
node test-zeyora.mjs lead ./lead.json
node test-zeyora.mjs career ./career.json ./sample-cv.pdf
node test-zeyora.mjs subscribe ./subscription.json
node test-zeyora.mjs unsubscribe ./unsubscribe.json

The smoke-test helpers do not verify CAPTCHA or send confirmation emails. For a test, attest verification only after your backend actually verifies the challenge; never bypass this in the production website. Do not send the placeholder timestamp shown in the confirmation snippet.

Responses, troubleshooting & go-live checks

HTTP statusMeaning / action
200 / 201Inspect the endpoint-specific response; duplicates can return 200.
400 / 422Invalid JSON, required fields, consent, choices or file. Correct the input; do not retry unchanged.
401 / 403Check active/authorised key, tenant, timestamp, exact signature and permission. Never put credentials in an error report.
405Wrong HTTP method.
409Career/Marketing UUID reused with different content or operation. Use a new UUID for a genuinely new action.
429Rate limit. Back off; also apply website/proxy protection.
503Disabled/unconfigured integration, busy server or unavailable storage/scanner. Check setup before bounded retries.

Career credentials not configured: verify the latest shared-key HRM release is deployed; enable Website Integration for the correct tenant and select/save its active CRM key. A key existing in CRM alone does not enable Career intake. Confirm S3/scanning prerequisites before uploading.

Marketing disabled: enable Email Subscribers for the correct tenant and configure the server-side key allowlist. A response with code: "subscriptions_not_enabled" means tenant setup is not enabled; public API calls will not create it.

Career and Marketing enforce authenticated request limits (including unfinished-attempt limits); HRM also has configurable submission/IP limits. Configure capacity for shared website-backend IPs. All endpoints need reverse-proxy limits for unauthenticated traffic, request body limits, monitoring and secure logs. No public form can be guaranteed completely spam-free.

Before publishing the website form

  • Test against a designated tenant/test environment with permission—not another company's credentials. Validate both successful and invalid submissions.
  • Check invalid/revoked keys, expired timestamps, changed JSON signatures, duplicate retries and different tenants. Confirm no cross-tenant records are created.
  • Confirm lead information appears in CRM; candidate fields and a private downloadable CV appear in HRM; subscriber pending/verified/opt-out states appear in Marketing.
  • Test career dropdown choices, missing required fields, upload size/type rejection and scanner/storage failures. Test Marketing consent, email confirmation and sticky opt-out.
  • Verify backend CAPTCHA token validation and expected hostname, honeypot, rate limiting, clocks, secret storage and generic public success/error messages.
  • When global CSRF is enabled, configure only the signed API routes as exceptions; never disable CSRF globally or exempt administrator settings screens.

This reference describes the matching implementation. A tenant's deployed release and settings determine availability; documentation alone does not confirm production readiness. For support, share the endpoint, HTTP status, sanitised error and request ID if returned—not credentials or candidate files.

Existing CRM, finance and webchat APIs

The following existing endpoints use their original authentication. Do not use this connection object for the signed website APIs documented above. Relative CRM paths below are appended to your tenant's CRM application base URL.

Zeyora API endpoints use tenant API credentials. Most endpoints expect a `connection` object with `api_key` and `api_secret`. Older ingestion endpoints accept a form field named `data` that contains a JSON string. Newer finance endpoints such as `create_invoice`, `create_credit_note` and `create_receipt` accept JSON payloads directly.

Legacy authentication object

{
  "connection": {
    "api_key": "YOUR_API_KEY",
    "api_secret": "YOUR_API_SECRET"
  }
}

Endpoint reference

MethodEndpointUseImportant fields
POST /api/insert_lead Queue a lead payload for processing. Form field `data` containing JSON with `connection` and lead/customer details.
POST /api/insert_ticket Queue a support ticket payload. Form field `data` containing JSON with `connection` and ticket details.
POST /api/insert_order Queue an e-commerce/order payload. `connection`, `customer`, `shipping_data`, `order_data`, `order_items`.
POST /api/insert_product Create a product if the name/SKU does not already exist. `product_data.product_name`, `category`, `assigned_person`; optional `sku`, `brand`, `model_number`.
POST /api/insert_invoice Legacy invoice ingestion and processing endpoint. `invoice_data.invoice_no` plus invoice payload and connection credentials.
POST /api/get_purchased_items_list Return purchased items for a date. `data.date` in a parseable date format.
POST /api/get_invoice_details Return customer and item details for an invoice number. `data.invoice_no`.
POST /api/get_last_purchased_item_cost Return the latest purchase cost for a SKU. `data.sku`.
POST /api/get_latest_task_activity Return today task timer activity for customer-related tasks. Authenticated `connection` object.
POST /api/create_invoice Create an invoice immediately from raw JSON. `data.customer_id`, `invoice_number`, `invoice_date`, `due_date`, `items[]`; warehouse may be required by tenant settings.
POST /api/create_credit_note Create a credit note against an existing invoice. `data.customer_id`, `invoice_number`, `credit_note_date`, `items[]`; optional `credit_note_number`.
POST /api/create_receipt Create a receipt and allocate it to invoices. `data.customer_id`, `payment_type`, `amount`, `invoices[]`; bank or cheque fields when required.
POST /mobile_field_sales/login Authenticate a field-sales mobile user and return a bearer token. `root_id`, `username`, `password`, optional device details.
GET /mobile_field_sales/status Return current mobile policy, latest location, open visit, today plan and today visits. Bearer token for the logged-in staff member.
GET /mobile_field_sales/assigned_leads Return active CRM leads assigned to the logged-in field user. Optional `q` and `limit` filters.
GET /mobile_field_sales/assigned_customers Return CRM customers assigned to or created by the logged-in field user. Optional `q` and `limit` filters.
POST /mobile_field_sales/ping Update current location and store route history when policy thresholds are met. `latitude`, `longitude`, `accuracy_meters`, optional speed, heading, battery and recorded time.
POST /mobile_field_sales/start_visit Start a customer or lead visit with GPS context. `lead_id` or `customer_id`, visit title, latitude, longitude and notes.
POST /mobile_field_sales/end_visit Complete a field visit with outcome, notes, follow-up date and optional proof files. `visit_id`, latitude, longitude, outcome, notes, optional `photo_urls`.
GET /mobile_field_sales/my_plan Return the logged-in user daily field plan. Optional `date` in `YYYY-MM-DD` format.
GET /mobile_field_sales/my_area_assignments Return assigned sales areas, time window, target visits and instructions. Optional `date` in `YYYY-MM-DD` format.
POST /webchat/init Initialize a public website chat session. `key`, optional visitor name/email/phone/page URL/referrer.
POST /webchat/send Send a visitor message into Omni Inbox. `key`, `session_id`, `conversation_id`, `message`.
GET /webchat/poll Poll for outbound widget messages. `key`, `session_id`, `conversation_id`, optional `after_id`.
POST /webchat/validate_socket Validate a widget realtime socket connection. `key`, `session_id`, `conversation_id`, `ws_token`.

Create invoice example

POST /api/create_invoice
Content-Type: application/json

{
  "connection": {
    "api_key": "YOUR_API_KEY",
    "api_secret": "YOUR_API_SECRET"
  },
  "data": {
    "customer_id": 125,
    "invoice_number": "INV-10045",
    "invoice_date": "2026-08-08",
    "due_date": "2026-08-22",
    "currency": "AED",
    "warehouse_id": 1,
    "tax_included": 1,
    "tax_percentage": 5,
    "items": [
      {
        "name": "CRM Implementation",
        "description": "Implementation and setup",
        "qty": 1,
        "rate": 2500,
        "unit": "Service"
      }
    ]
  }
}

Create credit note example

POST /api/create_credit_note
Content-Type: application/json

{
  "connection": {
    "api_key": "YOUR_API_KEY",
    "api_secret": "YOUR_API_SECRET"
  },
  "data": {
    "customer_id": 125,
    "invoice_number": "INV-10045",
    "credit_note_number": "CN-10045",
    "credit_note_date": "2026-08-09",
    "currency": "AED",
    "items": [
      {
        "name": "Service adjustment",
        "qty": 1,
        "rate": 250
      }
    ]
  }
}

Create receipt example

POST /api/create_receipt
Content-Type: application/json

{
  "connection": {
    "api_key": "YOUR_API_KEY",
    "api_secret": "YOUR_API_SECRET"
  },
  "data": {
    "customer_id": 125,
    "payment_type": 1,
    "amount": 2500,
    "currency": "AED",
    "invoices": [
      { "invoice_number": "INV-10045", "amount": 2500 }
    ]
  }
}

Webchat example

POST /webchat/init
Content-Type: application/json

{
  "key": "PUBLIC_WIDGET_KEY",
  "name": "Visitor Name",
  "email": "visitor@example.com",
  "page_url": "https://example.com/contact"
}

Error handling

Most responses include `result` and `error`. Finance endpoints may also return `status`, `request_id`, record IDs and totals. Treat non-empty `error` as a failed or pending integration action. Some finance requests intentionally return pending messages when a customer, warehouse, invoice or item master has not been synced yet.