How to Retrieve Sales Information Using the Fortis Public API Key
This article explains how to generate a Public API key in the Fortis portal and use it to retrieve completed (paid) sales data, including line items, quantities, totals, and sales location.
For the full technical reference, see: Fortis API Documentation
Section 1: Create a Public API Key
- Log in to your Fortis portal: Fortis Portal
- Go to Settings.
- From the left menu, select Public API.
- Click Generate New API Key.
- Copy the key and store it securely.
This API key is used to retrieve your sales information. It is already scoped to your company, so you do not need to send a company ID with your requests.
Section 2: How to use the API Key to Retrieve Sales Information
Integration guide for automated daily sales reporting.
1. Production API base URL
https://api.fortis.world/api/v1
The /api/v1 prefix is part of the base URL; all paths below are relative to it. Production is the only environment available to external consumers. HTTPS only.
2. Authentication
The Public API key is not sent as a request header directly. First exchange it for a JWT access token:
POST https://api.fortis.world/api/v1/auth/tokens
Content-Type: application/json
{"api_key": "fk_live_YOUR_API_KEY"}The response contains access_token, refresh_token, expires_in (access-token lifetime in seconds) and refresh_expires_in. Then call every endpoint with the header:
Authorization: Bearer <access_token>
The access token is short-lived (see expires_in). For a nightly batch job the simplest pattern is to request a fresh token at the start of each run; if a run outlives the token, either re-exchange the API key or call POST /auth/tokens/refresh with {"refresh_token": "..."}.
Note: your API key is already scoped to your company. Do not send any company_id parameter — the company is resolved from the token.
3. Endpoint: paid sales for a date range
GET /sales?status=completed&initiated_at_from=<ISO 8601>&initiated_at_to=<ISO 8601>
- status=completed is required (currently the only accepted value; completed sales are finalized, paid sales).
- initiated_at_from / initiated_at_to are inclusive on both ends, ISO 8601 date-time with timezone.
- Optional order_by (default initiated_at_desc).
4. Order line items and quantities
Line items are included directly in each sale returned by GET /sales — no second call is needed. Each sale contains:
- positions[] — line items with: name, quantity (value, exponent, uom_id, uom_name, uom_symbol — real quantity = value / 10^exponent), codes[] (barcodes/SKUs), catalog_good_id, catalog_good_variation_id, source, vat, unit_price_including_tax, total_price, discount_amount, taxable_amount, tax_percentage.
- totals — sale-level total, tax, taxable_amount and discount.
All money fields are objects {"value": <integer minor units>, "currency_code": "AED"} — e.g. value: 52500 = AED 525.00.
For your reporting:
| Metric | How to compute |
|---|---|
| Total revenue | sum of totals. total.value across returned sales |
| Paid order count | number of sales returned for the day |
| Product quantities | positions[].quantity (value / 10^exponent), grouped by catalog_good_id / catalog_good_variation_id or codes[] |
| Sales location | point_of_sale_id on each sale (see section 6) |
5. Pagination
Pagination is cursor-based with a fixed page size of 20 items — it cannot be increased. There is no limit on the total number of records; you retrieve everything by looping:
- Call GET /sales?status=completed&... — the response contains items and cursor.
- While cursor is not null, repeat the same request adding &cursor=<value from previous response>.
- Stop when cursor is null.
For a typical day of sales, this is a handful of sequential requests. If you receive HTTP 429, back off and retry: when the response includes a Retry-After header, wait that many seconds; otherwise wait 30–60 seconds before retrying. An invalid or expired cursor returns HTTP 400 with code invalid_cursor — restart the loop from the first page.
6. Sales location identification
Each sale has point_of_sale_id — the UUID of the sales location (the entity shown as Locations in your Fortis back office). Resolve location names via:
GET /business-structure/point-of-sales (paginated, same cursor mechanism)
GET /business-structure/point-of-sales/{id}Locations change rarely — fetch the list once per run and cache the id-to-name mapping. Each sale also has initiated_by_id (the employee who initiated it), resolvable via GET /business-structure/employees if you need cashier-level reporting.
Note: identification of the individual POS terminal/device that processed a sale is not currently available in the Public API — the sales location is the most granular "where" dimension exposed. If per-device reporting is important for you, please let us know, and we will consider it for a future API version.
7. Sample cURL: one day of sales
Example: 5 October 2026, Dubai time (UTC+4).
# 1) Exchange the API key for an access token
ACCESS_TOKEN=$(curl -s -X POST "https://api.fortis.world/api/v1/auth/tokens" \
-H "Content-Type: application/json" \
-d '{"api_key":"fk_live_YOUR_API_KEY"}' | jq -r '.access_token')
# 2) First page of completed sales for the day
# (note: "+" in the timezone offset must be URL-encoded as %2B)
curl -s "https://api.fortis.world/api/v1/sales?status=completed\
&initiated_at_from=2026-10-05T00:00:00%2B04:00\
&initiated_at_to=2026-10-05T23:59:59%2B04:00" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# 3) Next pages: repeat the same request with &cursor=<cursor from the
# previous response> until the response returns "cursor": null8. Errors and support
Errors are returned as application/problem+json with a trace_id field. Please include the trace_id when contacting support about a failed request — it lets us locate the exact request in our logs.