Skip to content
Zenoheld

Developers

External API v1 · updated 8 October 2026

Connect Zenoheld to your own systems: 201 resources, a versioned contract that doesn't break, an OpenAPI document and signed webhook events.

Quick start

1. Create a key. In the app, open a new key on the Admin › Integration › API Keys screen and choose the integration's name and roles. The key is shown only once; only its hash is stored on the server.

2. Send your first request.

curl "https://ozkaymak-erp-dotnet-rvfootuq6a-ew.a.run.app/api/v1/customers?size=2" \
  -H "X-Api-Key: ozk_…"

3. The response. Every list comes back in the same envelope:

{
  "items": [
    { "id": 482, "name": "…", "type": "customer", "tax_number": "…",
      "balance": { "amount": 0.00, "currency": "TRY" }, "active": true }
  ],
  "page": 1, "size": 2, "total": 57
}

All endpoints, fields and examples are in the API reference (OpenAPI 3) — client code can be generated from it.

Authentication

  • Send the X-Api-Key header with every request. No key or an unrecognised key returns 401.
  • A key works with the permissions of the administrator who created it and can't exceed them: the roles chosen are limited to those the administrator can grant; if the administrator is deactivated, the key stops working too.
  • A key works only on /api/v1 endpoints; it can't reach the screens' internal endpoints. It sees only your own company's (and your group companies') data.
  • A key becomes invalid the moment you deactivate it. The screen shows when each key was last used.

Company selection

If your account has several companies, choose the company by company code — the same code as the company value in the screen address:

-H "X-Company: 0002"
  • When reading, the selection narrows the result to that company (and its subsidiaries); when writing, it stamps the record with that company.
  • In a multi-company account, a write request without a company writes nothing and returns 400. An unknown code returns 400; a company you can't access returns 403.
  • In a single-company account the header isn't needed.

Pagination, sorting, search

ParameterMeaningDefault
pagePage number, starting at 11
sizePage size; values above 200 are capped at 20050
sortSort field (external field name, e.g. name). An unrecognised field returns 400 — it isn't silently ignoreddepends on the resource
orderasc or descasc
qFree-text search (name, code, tax number …)—

Creating and updating records

  • Idempotency: add the Idempotency-Key header to the POST request (at most 80 characters). If you resend with the same key after a timeout, no second record is created; the first result itself is returned.
  • Update: PUT changes the fields you send; it doesn't touch fields you leave out or send as null. Clearing a field's value isn't supported in this version.
  • Delete: DELETE doesn't delete the record permanently, it deactivates it; the response is 204. Deleting again also returns 204.
  • Resources open for writing today: customers, suppliers, business-partners, invoices, employees, sales-orders, webhook-subscriptions, accounting-source-transactions, payroll-change-packages, payroll-results, external-datasets, custom-objects. The rest are read-only.

Money, dates, field names

  • Amounts aren't bare numbers; they come back with their currency: { "amount": 150000.00, "currency": "TRY" }.
  • Dates are ISO 8601 (2026-10-08), timestamps are UTC (2026-10-08T09:46:41Z).
  • Field names and option values are English and snake_case: tax_number, "type": "customer".

Errors

Errors follow RFC 7807; type is the class of error and detail is an English explanation. Every response includes a trace_id you can quote in a support ticket.

{
  "type": "/errors/Forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "The API key's user has no access to company '0002'.",
  "trace_id": "0HNP543SOILEB:00000001"
}
CodeWhen
400Invalid parameter or body — field-level details in errors
401Key missing or not recognised
403The key's permissions aren't enough, or it has no access to the company
404Record not found — another company's record also returns 404 (its existence isn't revealed)
409Conflict: a record with the same tax number, or a concurrent update. The machine-readable code is in the code field
422The body is valid but a business rule rejected it
429Rate limit exceeded — wait as long as the Retry-After header says

Rate limits

1,000 requests per minute per company (sliding window). Going over returns 429, and the Retry-After header says how long to wait. For bulk transfers, paginate with size=200.

Versioning

The version is in the address: /api/v1. Within this version no breaking changes are made:

  • Adding a field to a response, adding an optional field to a request and adding a new error code happen within the same version — your client should ignore fields it doesn't recognise.
  • Removing or renaming a field, changing its type, making an optional field required, or changing the default sort or page size happens only in a new version (/api/v2).
  • An old version isn't shut down without its closing date being announced in advance.

Webhooks

Get notified instead of polling for events: invoice issued, order approved, shipment dispatched …

  • Subscription: POST /api/v1/webhook-subscriptions — target address (https), event filter (full name, invoice.* or *). The signing key (whsec_…) is returned only once, at creation and in the rotate-secret response.
  • Event catalogue: GET /api/v1/webhook-events. For testing, POST /{id}/test sends a signed test.ping; delivery history is at GET /{id}/deliveries.
  • Delivery: at least once; failed deliveries are retried with backoff, and after repeated failures the subscription is closed automatically. Store the X-Ozk-Delivery-Id header so you don't process the same delivery twice.
  • The target address must be on the public internet; private network, local and cloud metadata addresses aren't connected to, and redirects aren't followed.

Verify the signature — X-Ozk-Signature: t=<unix>,v1=<hex>, v1 = HMAC-SHA256(secret, "<t>.<raw body>"):

import crypto from "node:crypto";

function isValidSignature(header, rawBody, secret) {
  const p = Object.fromEntries(header.split(",").map((x) => x.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${p.t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(p.t)) < 300;   // reject signatures older than 5 minutes
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1));
}

Resources (201)

The discovery endpoint GET /api/v1 gives the list in machine-readable form. Each resource's fields are in the API reference.

customerssuppliersbusiness-partnersinvoicespaymentsemployeesstock-movementssales-ordersequipmentmaintenance-orderscost-centerspurchase-ordersbatchesfixed-assetsbank-accountsaccount-certificationspayment-cardscash-journalsjournal-entry-itemsinternal-orderscontrolling-areasactivity-typesoperating-concernsprofit-centerssettlement-profileschecksbills-of-exchangepos-terminalsaccountsleadsopportunitiesactivitiescontactsreferral-programsreferralsriskscontrolscontrol-testsinternal-auditsrisk-incidentsremediation-actionspoliciesrisk-assessmentssod-rulesinvestment-programsprojectswbs-elementsnetworksnetwork-activitiestime-sheet-entriescondition-recordssupplier-portal-accountssubcontracting-orderssupplier-groupsquota-arrangementspurchase-requisitionsgoods-receiptspurchase-contractspurchase-order-mass-changessupplier-evaluation-runssupplier-evaluation-configscustomer-returnsshipping-pointsreturn-reasonssales-inquiriessales-quotationsconsignment-ordersplantspurchasing-organizationslocationsorder-typesoutput-typesoutput-condition-recordsfleet-kpi-eventsoutput-historyexpense-reportsstorage-locationsstorage-binsmaterial-documentsmaterialsconsignment-stocksreturnable-packagingsphysical-inventory-documentsreservationsstock-transport-orderscycle-countswaveshandling-unitsload-confirmationspowers-of-attorneylegal-noticeslimitation-periodsfee-schedulescontractslearning-programsjob-applicationsjob-classificationsplanner-groupsbills-of-materialprocess-ordershaccp-hazardshaccp-critical-control-pointshaccp-plansmaintenance-plansmaintenance-strategieswork-centerscost-estimatescost-ratesroutingsrepetitive-manufacturing-profilesproduction-versionsengineering-changesmaster-inspection-characteristicsinspection-methodscorrective-actionsquality-action-planssampling-proceduresmanagement-reviewsretention-samplesbreakable-itemsconsumer-complaintssensory-evaluationslabel-verificationsnet-content-checksmaster-data-requestshierarchiesoutbound-deliveriesfreight-ordersfreight-order-stopsvehiclesdrivershubsvehicle-assignmentsvehicle-availabilitiesfuel-logsvehicle-incidentsvehicle-inspectionsgate-eventsdriver-certificatesdriver-licensesdirect-flow-rulescarrier-invoicestransit-laneseta-predictionsgeofencesreturn-deliveriescarrier-allocationscarriersforwarding-ordersfreight-unitsfreight-agreementsmultimodal-routesfreight-tenderscustoms-declarationscharge-typeschemicalssafety-auditswaste-recordssafety-incidentswork-permitsmortgagessd-sales-orderstax-codescondition-typesservice-mastersasset-classesdepreciation-keysdepreciation-areasdivisionspurchasing-groupsdepartmentsledgersunits-of-measurepublic-holidaysemployee-subgroupspersonnel-subareaspos-menu-mappingsprocessesprocess-templatesequipment-bom-itemswebhook-subscriptionswebhook-eventsgl-accountscredit-memosvariance-calculation-linescompaniesbanksuserstasksthird-party-orderspos-checkssegmentsaccounting-source-transactionsaccounting-source-systemsaccounting-rulesaccounting-runspayroll-providerspayroll-change-packagespayroll-resultsexternal-datasetscustom-objects

Close next month end without a reconciliation marathon.

Try it free for 15 days; no credit card needed. We reply to your questions within one business day.