Edara API v3: Guide
Read this page before any reference page. It covers how to call the API and the behaviours that most often cause wrong results. Last verified against the live API on 2026-09-28.
Hosts
| URL | |
|---|---|
| API | https://edara-api.edara.io/v3/... (HTTPS only) |
| Interactive docs | https://edara-api.edara.io/swagger/ui/index |
| OpenAPI spec (Swagger 2.0) | https://edara-api.edara.io/swagger/docs/v3 |
https://api.edara.io is the separate v2 API (/v2.0/...). It has a different response
format. Do not mix the two.
Authentication
Send the token in the Authorization header. Two kinds of token work:
- Integration token. Create it in Edara under Data, then Integration, then Rest APIs.
The value you copy already starts with
Bearer, so send it as is. - A signed-in user's Edara access token (JWT). Send it as
Bearer <token>and add theTenantIdheader with the organization's id.
GET /v3/customers?offset=0&limit=100 HTTP/1.1
Host: edara-api.edara.io
Authorization: Bearer <token>
TenantId: <organization id> # only with a user JWT
Accept-Language: ar # optional: localizes error detail text
Content-Type: application/json
Each endpoint requires one permission, shown on its reference entry (for example
read:customer). A 403 response names the missing permission in detail.
Request and response format
- JSON only. Property names are camelCase.
- Enums are sent and returned as names (
"Confirmed"), not numbers. - Ids are integers. Requests refer to other records by id (
customerId,warehouseId). To look up by code, use the explicitcode/{code}routes. - Responses embed related records as reference objects:
{ "id": 1, "code": "...", "name": "..." }. - Null values are included in responses.
- A successful create returns 201 with a
Locationheader and the created object.
Paging
{ "items": [], "totalCount": 142, "page": 2, "pageSize": 100, "totalPages": 2 }
offsetdefaults to 0.limitdefaults to 100 and the maximum is 1000.- Use offsets that are multiples of
limit. - Some endpoints reject a
limitoutside 1 to 1000 with a 400. Others silently fall back to 100. Each reference table says which. - A few endpoints have a different cap, and a few are not paged at all. Check the endpoint.
- Always loop until
offset >= totalCount. Do not stop because a page came back short.
Errors
Errors use application/problem+json:
{ "title": "Conflict", "status": 409, "detail": "localized text",
"instance": "/v3/brands", "traceId": "...", "errorCode": "DupplicatedName",
"errors": { "name": ["..."] } }
| Status | Meaning | What to do |
|---|---|---|
| 400 | Validation failed (errors lists each field), or a referenced id does not exist |
Fix the input |
| 401 | Missing or invalid token, or a user JWT without TenantId |
Sign in again or add the header |
| 403 | The token lacks the permission named in detail |
Use a token that has it |
| 404 | Record not found, or the route does not exist | Check the path spelling first |
| 409 | A business rule refused the request | Match on errorCode, listed per endpoint |
| 429 | Rate limit reached | Wait for Retry-After seconds |
| 500 | Unexpected error | Report it with the traceId |
- Match on
errorCode, never ondetail. Thedetailtext is localized. - Error codes keep their original spelling:
DupplicatedNameandDupplicatedCodehave a double p. PhantomRead(409) means the record changed while you were editing it. Reload and retry.- Batch endpoints return 200 with
{ "succeeded": [], "failed": { "<key>": "message" } }. Partial success is normal, so always inspectfailed.
Rate limits
- 5 requests per second, 250 per minute and 5,500 per hour.
- A limit can apply per user or per organization, so several integrations in one organization can share one budget. Leave room for the others.
- Every response carries
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-ResetandX-RateLimit-Scope. The scope names the limit closest to being reached. - On 429, wait for the number of seconds in
Retry-After. Do not send large parallel bursts.
Behaviours that cause wrong results
Filters that are on by default
Send these explicitly. If you omit them, results shrink without any error.
GET /v3/sales-orders:onlyMyOrdersdefaults totrue, which returns only orders the calling user created.warehouseOnlydefaults totrue, which leaves out sales-store (point of sale) orders. Sales returns appear in the same list.GET /v3/work-orders:onlyMyOrdersdefaults totrue, including forcodeandpaperNumberlookups. To find one document reliably, query withfalse, then withtrue, and remove duplicates by id.GET /v3/journal-entriesandGET /v3/accounts/{id}/balance:isPostedOnlydefaults totrue. Journal entries created through the API are saved unposted, so they do not appear until they are posted.
Matching differs per endpoint
Never assume items[0] is the record you asked for.
GET /v3/customers:code,name,phone,mobileandemailmatch partially.externalIdmatches exactly.updateDatereturns records changed strictly after the value.searchbehaves differently: active customers only, at most 50 matches per field.GET /v3/stock-items: only one filter applies per call, in this order:code,sku,partNumber,externalId. A filtered call ignoresoffsetandlimitand returns every match. The unfiltered list returns 404 when there are no items. Inactive items are included.GET /v3/stock-items/search: returns at most 1000 items, in no guaranteed order, with nototalCount. Inactive items are left out.
An empty list can mean missing data permissions
Lists of warehouses, customers, stock items and sales orders are filtered by the calling user's data permissions in Edara. A user without them gets an empty 200, not a 403. If a list is unexpectedly empty, check that user's data permissions first.
Date range filters
Most ...To filters compare a full date and time. dateTo=2026-09-22 means midnight at
the start of that day, so the whole day is left out. Send 2026-09-22T23:59:59.997.
Slow calls
GET /v3/warehouses/{id}/balanceandGET /v3/sales-ordersare slow on large data. Do not use them to copy data in bulk.- For balances, prefer
GET /v3/warehouses/stock-items-balancesorGET /v3/stock-items/warehouse-summary(limit up to 5000).
Writing data
- PUT replaces the whole record. Fields you omit are cleared or reset. For example, a
tax update without
activedeactivates the tax. Always GET the record, change it, then PUT it. - Unknown ids in a create or update often return 400 without an
errorCode, or sometimes 500, instead of 404. Validate ids before you write. - Exchange rate. If you omit it on a work order, it is 1, even for a foreign currency.
On a journal entry, it is today's rate, not the rate on the document date. More than 6
decimals is refused with 409
ExchangeRateDecimalsExceedLimit. - Always send
externalIdwhen you create a sales order. A retry with the sameexternalIdis refused with a 400 that has noerrorCode. Treat that as "already created" and look the order up. WithoutexternalId, a retry creates a duplicate. - Sales-order line prices are not filled in from the item. A missing price becomes 0, which is refused unless the organization allows zero prices.
- Sales-order status calls do not check the current status. Issue, un-issue and status updates change the status only, and issue does not create a stock issue or an invoice. Enforce the allowed transitions in your own code.
- Confirm deletes. After deleting or deactivating a stock item, read it again to confirm. A 204 does not always mean the item changed.
Known response issues
GET /v3/stock-items/serialsreturnsserialNumberas null.GET /v3/sales-orders/brand-saleswith an unknown brand name returns all brands.- Items in
GET /v3/customerscarrybalance: 0. UseGET /v3/customers/{id}/balance.
Shared shapes
PagedResult<T>: see Paging.BatchResult:{ "succeeded": [], "failed": {} }, see Errors.ProblemDetails: see Errors.*Reference(for exampleCustomerReference,WarehouseReference):{ id, code, name }.