{"openapi":"3.1.0","jsonSchemaDialect":"https://spec.openapis.org/oas/3.1/dialect/base","info":{"title":"Wanas Payroll API","version":"1.1.0","summary":"Employees, pay runs, results, payslips, reports, social insurance and commissions of an Egyptian payroll on Zoho People, with signed webhooks.","description":"Connect your HR, ERP, accounting or BI system to **Wanas Payroll**, the Egyptian payroll for Zoho People. The API reads employees, pay runs, per-employee results, payslips and reports, manages social insurance profiles, posts sales commissions into open pay runs, and sends signed **webhooks** when payroll events happen.\n\nThe REST API, webhooks, the MCP server for AI tools and custom reports are included in every Wanas Payroll plan.\n\n## Getting started\n\n1. **Create an API key.** An organization admin opens **Settings → Integrations → API and webhooks** ([open it](https://payroll.wanasapps.com/org/settings/api-webhooks?tab=keys)), tab **API keys**, and chooses a name, the scopes the integration needs and, optionally, an expiry date. The full key (`wpk_live_…`) is shown **once**: store it in your secret manager, never in source code.\n2. **Check the key** with `GET /me`, which answers with the organization and the scopes of the key:\n\n```bash\nexport WANAS_API_KEY=\"wpk_live_…\"\ncurl -s https://payroll.wanasapps.com/api/v1/me -H \"Authorization: Bearer $WANAS_API_KEY\"\n```\n\n3. **Read data**, for example the active employees and the latest pay runs:\n\n```bash\ncurl -s \"https://payroll.wanasapps.com/api/v1/employees?limit=50\" -H \"Authorization: Bearer $WANAS_API_KEY\"\ncurl -s \"https://payroll.wanasapps.com/api/v1/pay-runs?status=locked\" -H \"Authorization: Bearer $WANAS_API_KEY\"\n```\n\n4. **Subscribe to webhooks** so you do not have to poll: `POST /webhooks` with your HTTPS URL and the events you want (see [Webhooks](#description/webhooks)).\n\n## Base URL\n\n```\nhttps://payroll.wanasapps.com/api/v1\n```\n\nHTTPS only. Requests and responses are JSON (UTF-8) with snake_case keys; send `Content-Type: application/json` with a body. Every response carries an `X-Request-Id` header (`req_…`).\n\n## Authentication\n\nSend the key as a bearer token on every request:\n\n```http\nAuthorization: Bearer wpk_live_<key id>_<secret>\n```\n\n- The **organization is always the key's organization**: you never pass an organization id, and a key can never read another organization (another organization's ids answer 404).\n- Keys are created, listed and revoked by organization admins in **Settings → Integrations → API and webhooks**. Up to 20 active keys per organization; a key can have an expiry date.\n- Wanas stores only a salted hash of the secret. The key list shows a masked key (`wpk_live_<key id>_********abcd`) so you can recognise it.\n- `401` answers say why: `invalid_api_key` (missing, malformed or unknown), `api_key_revoked` or `api_key_expired`.\n\n## Scopes\n\nGive each integration only the scopes it needs. A call without the required scope answers `403 insufficient_scope` with `details.required_scope`. Every endpoint lists its scope.\n\n| Scope | Grants |\n|---|---|\n| `employees:read` | List and read employees (no national ID, bank account or salary). |\n| `payruns:read` | List and read pay runs, their totals and per-employee results. |\n| `payslips:read` | List payslips of released pay runs and download them as PDF. |\n| `reports:read` | Run the payroll reports as JSON or CSV. |\n| `webhooks:manage` | Create, change, test and delete webhook endpoints; read the delivery log. |\n| `si_profiles:read` | Read social insurance profiles, rates, defaults, assignment rules and what applies to an employee. |\n| `si_profiles:write` | Create SI profiles, schedule rate, default and rule changes, assign profiles to employees. |\n| `commissions:read` | Read commission profiles and posted commissions. |\n| `commissions:write` | Post sales commissions into open (draft or calculated) pay runs. |\n\n## Rate limits\n\nEach key has a token bucket of **120 requests a minute** by default (the admin can set 10 to 1200 per key); the bucket is also the burst size. Payslip PDFs, the expensive call, have an extra bucket of 30 a minute per key.\n\n| Header | Meaning |\n|---|---|\n| `X-RateLimit-Limit` | The key's requests per minute |\n| `X-RateLimit-Remaining` | Requests left in the bucket now |\n| `Retry-After` | On `429 rate_limited`: seconds to wait before the next try |\n\nBack off on `429` (wait `Retry-After` seconds) and spread bulk reads over time.\n\n## Pagination\n\nLists answer `{ \"data\": [...], \"next_cursor\": \"…\", \"has_more\": true }`. Pass `next_cursor` back as `?cursor=` with the same filters to get the next page; `next_cursor` is `null` on the last page. `?limit=` sets the page size (each endpoint documents its maximum). Cursors are opaque: do not build or parse them. A broken cursor answers `400 invalid_cursor`.\n\n```bash\ncursor=\"\"\nwhile :; do\n  page=$(curl -s \"https://payroll.wanasapps.com/api/v1/employees?limit=200${cursor:+&cursor=$cursor}\" -H \"Authorization: Bearer $WANAS_API_KEY\")\n  echo \"$page\" | jq -c '.data[]'\n  cursor=$(echo \"$page\" | jq -r '.next_cursor // empty')\n  [ -z \"$cursor\" ] && break\ndone\n```\n\n## Money, dates and numbers\n\n- **Money is an integer number of piastres** (1/100 EGP), always next to a `currency` field (`\"EGP\"`). `150000` is EGP 1,500.00. Divide by 100 only for display; never use floating point for sums.\n- **Dates** are ISO 8601: `2026-10-31` for days, `2026-10-31T08:00:00.000Z` for moments (UTC). Periods are `YYYY-MM`.\n- \"Today\" (defaults such as `as_of`) is the organization's calendar day in its time zone (Africa/Cairo unless set otherwise in Zoho People).\n- Rates are fractions: `0.11` is 11%. Commission percentages are percentages: `3.5` is 3.5%.\n\n## Errors\n\nEvery error has the same body and a matching HTTP status:\n\n```json\n{ \"error\": { \"code\": \"run_not_final\", \"message\": \"Results are available once the pay run is approved, locked or paid\", \"details\": { \"status\": \"calculated\" }, \"request_id\": \"req_4f1c9b2a7d3e8f60a1b2\" } }\n```\n\nBranch on `error.code` (stable), show `message` (human, may change), and quote `request_id` when you contact support.\n\n| Status | Typical codes | What to do |\n|---|---|---|\n| 400 | `invalid_limit`, `invalid_cursor`, `invalid_status`, `invalid_period`, `invalid_date`, `invalid_format`, `invalid_lang` | Fix the query parameter |\n| 401 | `invalid_api_key`, `api_key_revoked`, `api_key_expired` | Use a valid key |\n| 402 | `subscription_required` | The organization is read-only (no active subscription): reads work, writes do not |\n| 403 | `insufficient_scope`, `org_suspended`, `feature_disabled` | Add the scope to the key, or contact the organization admin |\n| 404 | `not_found` | Unknown id (or another organization's) |\n| 409 | `run_not_final`, `payslips_not_released`, `no_open_payrun`, `run_locked`, `run_approved`, `endpoint_limit`, `si_date_locked` | The record is not in the right state yet |\n| 422 | `invalid_input`, `invalid_url`, `invalid_events`, `employee_not_found`, `si_profile_merge_invalid`, `bulk_failed` | Fix the body (`details` says what) |\n| 429 | `rate_limited` | Wait `Retry-After` seconds |\n| 500 | `internal` | Retry later with backoff; report the `request_id` if it persists |\n\n## Idempotency and retries\n\n- `GET` and `DELETE` are safe to retry.\n- There is **no `Idempotency-Key` header**. `POST /commissions` and `POST /pay-runs/{id}/commissions` post again when retried: after a timeout, check `GET /commissions?payrun_id=…` for your `reference` before you send the same commission again.\n- Other writes are state changes (create a webhook endpoint, schedule a rate) and answer `409` or `422` when repeated where a duplicate would be wrong.\n- Webhook events keep the **same `id` on every retry**: store processed ids and skip repeats.\n\n## Data protection\n\nNational IDs and bank accounts (IBANs) are **never** returned, not even masked. Webhook bodies carry ids and statuses only (no salaries): fetch the details with the REST API. Pay run results need a run that is approved, locked or paid (`409 run_not_final` before); payslip PDFs are served only after the run's payslips are released to employees (`409 payslips_not_released` before). Every API read of results, reports and payslips is written to the organization's audit log with the key that made it.\n\n## Webhooks\n\nA webhook endpoint receives an HTTPS `POST` for each event it subscribes to. Manage endpoints with the [Webhook endpoints](#tag/webhook-endpoints) operations or in **Settings → Integrations → API and webhooks**. Rules: `https` on port 443 and a public host name; up to 10 endpoints per organization; the signing secret (`whsec_…`) is shown once, when the endpoint is created or its secret rotated.\n\n**Body**: `{ \"id\", \"type\", \"api_version\": \"2026-10-12\", \"created_at\", \"org_id\", \"data\" }`. **Headers**: `Wanas-Signature`, `Wanas-Event` (the type), `Wanas-Event-Id`, `Wanas-Delivery-Id`, `User-Agent: Wanas-Payroll-Webhooks/1.0`. The \"Send test event\" button and `POST /webhooks/{id}/test` send a `webhook.test` event.\n\n**Events**\n\n- `payrun.calculated`: A pay run was calculated (or recalculated).\n- `payrun.approved`: A pay run was approved.\n- `payrun.locked`: A pay run was locked (final; payslips exist).\n- `payrun.paid`: A pay run was marked as paid.\n- `payslip.released`: The payslips of a run became visible to employees (once per revision of the run).\n- `employee.synced`: The employee list was synchronised from Zoho People.\n- `ot.decided`: Overtime was approved or rejected.\n- `si_profile.created`: A social insurance profile was created (with its first rate).\n- `si_profile.rates_changed`: A social insurance profile's limits or rates change from a date (a rate change was added or cancelled). Open pay runs from that date are recalculated.\n- `si_profile.default_changed`: The organisation default SI profile changes from a date (set or cancelled). profile_id null = no default.\n- `si_profile.rules_changed`: The SI profile assignment rules change from a date (a new list, or a scheduled one cancelled).\n- `si_profile.assigned`: A profile was put on several employee salary packages at once (new salary versions from a date).\n\n**Answer with any 2xx within 10 seconds**, then do the work (queue it). Anything else, or no answer, is retried: 9 attempts in all, after 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h, 12 h (about 22.7 hours from the first). After 20 failed attempts in a row the endpoint is switched off and the organization admins are e-mailed; switch it back on with `PATCH /webhooks/{id}` `{ \"status\": \"active\" }`. The delivery log keeps 30 days. Events can arrive out of order: use `created_at` and fetch the current state.\n\n### Verify the signature\n\n`Wanas-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>`. Compute the HMAC over the **raw request bytes** (before any JSON parsing), compare in constant time, and reject timestamps more than 5 minutes away from your clock. Each attempt is signed afresh (new `t`) over the same body.\n\n```javascript\nimport crypto from 'node:crypto';\nimport express from 'express';\n\n// rawBody: the request body exactly as received (a Buffer or string), never re-serialised JSON.\nexport function verifyWanasSignature(secret, rawBody, header, toleranceSec = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n  const expected = crypto.createHmac('sha256', secret).update(t + '.').update(rawBody).digest();\n  const given = Buffer.from(parts.v1 ?? '', 'hex');\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n\nconst app = express();\napp.post('/hooks/wanas', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!verifyWanasSignature(process.env.WANAS_WEBHOOK_SECRET, req.body, req.get('Wanas-Signature') ?? '')) return res.sendStatus(400);\n  const event = JSON.parse(req.body.toString('utf8'));\n  // event.id is the same on every retry: skip ids you have already processed.\n  res.sendStatus(200);\n});\n```\n\n```python\nimport hashlib\nimport hmac\nimport os\nimport time\n\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n\ndef verify_wanas_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:\n    \"\"\"raw_body: the request body exactly as received (bytes), never re-serialised JSON.\"\"\"\n    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n    try:\n        t = int(parts[\"t\"])\n    except (KeyError, ValueError):\n        return False\n    if abs(time.time() - t) > tolerance:\n        return False\n    expected = hmac.new(secret.encode(), str(t).encode() + b\".\" + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n\n\n@app.post(\"/hooks/wanas\")\ndef wanas_webhook():\n    if not verify_wanas_signature(os.environ[\"WANAS_WEBHOOK_SECRET\"], request.get_data(), request.headers.get(\"Wanas-Signature\", \"\")):\n        return \"\", 400\n    event = request.get_json()  # event[\"id\"] is the same on every retry: skip ids you have already processed\n    return \"\", 200\n```\n\n## MCP server (AI tools)\n\nAI assistants (Claude, ChatGPT, Cursor, VS Code, Windsurf and other MCP clients) connect to Wanas Payroll through its **MCP server** at `https://payroll.wanasapps.com/mcp`, not with API keys: the person signs in with Zoho (OAuth 2.1 with PKCE) and approves the connection for one organization, and every action follows their role, the approval rules and the audit log.\n\n- Install page with per-client steps: [https://payroll.wanasapps.com/mcp](https://payroll.wanasapps.com/mcp)\n- Server card (machine-readable): [https://payroll.wanasapps.com/.well-known/mcp.json](https://payroll.wanasapps.com/.well-known/mcp.json)\n- Transport: Streamable HTTP, `POST https://payroll.wanasapps.com/mcp`\n\nUse the REST API for system-to-system integrations and the MCP server for people working with an AI assistant.\n\n## Versioning and changelog\n\nThe version is in the path (`/api/v1`). Within v1 we only make **additive** changes without notice: new endpoints, new optional parameters, new fields in responses, new event types and new error codes. Ignore fields and event types you do not know. A breaking change gets a new path version, announced in advance, with both versions served side by side for a transition period. Webhook bodies carry `api_version` (`2026-10-12`), bumped only for a breaking change of the event body.\n\n- **2026-10-18** (document 1.1.0): this reference at [https://payroll.wanasapps.com/api/reference](https://payroll.wanasapps.com/api/reference) with examples for every endpoint and event, response headers and error bodies; the document now also describes `held` on pay run results, the `profile` parameter of the `si-cost` report and the delivery attempts log.\n- **2026-10-15 to 2026-10-17**: social insurance profiles, defaults, rules and bulk assignment (`si_profiles:*`); commissions (`commissions:*`); more reports (social insurance cost and not insured, leave pay, service charge).\n- **2026-10-12**: v1: employees, pay runs and results, payslips, reports, webhooks and signed events.\n\n## Support\n\nE-mail [egypt@wanasapps.com](mailto:egypt@wanasapps.com) with the `request_id` of the call. The machine-readable OpenAPI 3.1 document is [https://payroll.wanasapps.com/api/v1/openapi.json](https://payroll.wanasapps.com/api/v1/openapi.json).","termsOfService":"https://wanasapps.com/terms","contact":{"name":"Wanas Apps FZ-LLC","email":"egypt@wanasapps.com","url":"https://payroll.wanasapps.com/api/reference"}},"externalDocs":{"description":"MCP server for AI tools (Claude, ChatGPT, Cursor, VS Code)","url":"https://payroll.wanasapps.com/mcp"},"servers":[{"url":"https://payroll.wanasapps.com/api/v1","description":"Production"}],"tags":[{"name":"Organization","description":"The organization and the scopes of the key you call with. Start here to check a key."},{"name":"Employees","description":"Employees synchronised from Zoho People. No national ID, bank account or salary is ever returned."},{"name":"Pay runs","description":"Monthly (main) and additional pay runs with their totals, and the per-employee results of runs that are approved, locked or paid. Statuses: `draft` → `calculated` → `approved` → `locked` → `paid`."},{"name":"Payslips","description":"Payslips of pay runs whose payslips are released to employees, as a list and as bilingual PDFs."},{"name":"Reports","description":"The payroll reports of the app (register, social insurance, tax, year-end, department cost ...) as JSON (money in piastres) or CSV."},{"name":"Social insurance","description":"Social insurance profiles (limits, rates per Law 148/2019 branch, insurable base) over time, the organization default, the assignment rules and what applies to an employee on a date. Changes are new rows from a date; earlier periods keep their terms, and nothing can start on or before the end of a locked or paid run."},{"name":"Commissions","description":"Commission profiles and sales commissions posted into open (draft or calculated) pay runs, for example from a CRM when a deal closes. Employees are matched by `employee_id`, `employee_code` or `employee_email`."},{"name":"Webhook endpoints","description":"Create, change, test and delete webhook endpoints, rotate their signing secret and read the delivery log. The events themselves and how to verify their signature are under **Webhooks** in the sidebar and in the introduction."},{"name":"Meta","description":"This OpenAPI document. Public: it describes the API, not any organization's data."}],"x-tagGroups":[{"name":"Core","tags":["Organization","Employees"]},{"name":"Payroll","tags":["Pay runs","Payslips","Reports","Social insurance","Commissions"]},{"name":"Integrations","tags":["Webhook endpoints","Meta"]}],"security":[{"ApiKey":[]}],"paths":{"/openapi.json":{"get":{"operationId":"getOpenApiDocument","summary":"This OpenAPI document","description":"The OpenAPI 3.1 description of the API (this reference). Cached for 5 minutes.\n\n**Public**: no API key needed.","tags":["Meta"],"security":[],"responses":{"200":{"description":"The document","content":{"application/json":{"schema":{"type":"object"},"example":{"openapi":"3.1.0","info":{"title":"Wanas Payroll API","version":"1.1.0"},"paths":{}}}}}}}},"/me":{"get":{"operationId":"whoami","summary":"Check a key: its organization and scopes","description":"The cheapest call: use it to check a new key and to see which scopes it has.\n\n**Scope:** `any` (any valid key).","tags":["Organization"],"security":[{"ApiKey":[]}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"},"example":{"organization":{"id":"org_7d2c41ab","name":"Nile Crest Trading"},"key":{"id":"key_5e8f20c1","name":"ERP integration","scopes":["employees:read","payruns:read","payslips:read","reports:read","webhooks:manage"]},"currency":"EGP","amount_unit":"piastre"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/employees":{"get":{"operationId":"listEmployees","summary":"List employees","description":"Employees synchronised from Zoho People, ordered by id. No national ID, bank account or salary.\n\n**Scope:** `employees:read`.","tags":["Employees"],"security":[{"ApiKey":["employees:read"]}],"parameters":[{"name":"status","in":"query","description":"Default `active`.","schema":{"type":"string","enum":["active","exited","all"],"default":"active"}},{"name":"department","in":"query","description":"Exact department name, as in Zoho People.","schema":{"type":"string"},"example":"Finance"},{"name":"limit","in":"query","description":"Page size (1 to 200, default 50).","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","description":"Opaque cursor: the `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"type":"object","required":["data","next_cursor","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Employee"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to get the next page; null on the last page."},"has_more":{"type":"boolean","description":"true when there is a next page."}}},"example":{"data":[{"id":"emp_1a2b3c4d","code":"NCT-0042","first_name":"Mona","last_name":"Hassan","name":"Mona Hassan","email":"mona.hassan@nilecrest.example","department":"Finance","designation":"Senior Accountant","status":"active","hire_date":"2021-03-01","exit_date":null,"synced_at":"2026-10-18T05:30:00.000Z"},{"id":"emp_2b3c4d5e","code":"NCT-0057","first_name":"Karim","last_name":"Adel","name":"Karim Adel","email":"karim.adel@nilecrest.example","department":"Sales","designation":"Sales Executive","status":"active","hire_date":"2023-07-15","exit_date":null,"synced_at":"2026-10-18T05:30:00.000Z"}],"next_cursor":"eyJhIjoiZW1wXzJiM2M0ZDVlIn0","has_more":true}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/employees/{id}":{"get":{"operationId":"getEmployee","summary":"Get an employee","description":"**Scope:** `employees:read`.","tags":["Employees"],"security":[{"ApiKey":["employees:read"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Employee id.","schema":{"type":"string"},"example":"emp_1a2b3c4d"}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Employee"},"example":{"id":"emp_1a2b3c4d","code":"NCT-0042","first_name":"Mona","last_name":"Hassan","name":"Mona Hassan","email":"mona.hassan@nilecrest.example","department":"Finance","designation":"Senior Accountant","status":"active","hire_date":"2021-03-01","exit_date":null,"synced_at":"2026-10-18T05:30:00.000Z"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/pay-runs":{"get":{"operationId":"listPayRuns","summary":"List pay runs","description":"Newest period first, with totals.\n\n**Scope:** `payruns:read`.","tags":["Pay runs"],"security":[{"ApiKey":["payruns:read"]}],"parameters":[{"name":"status","in":"query","description":"Only runs in this status.","schema":{"type":"string","enum":["draft","calculated","approved","locked","paid"]}},{"name":"period_from","in":"query","description":"First period, YYYY-MM.","schema":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$"},"example":"2026-01"},{"name":"period_to","in":"query","description":"Last period, YYYY-MM.","schema":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$"},"example":"2026-12"},{"name":"limit","in":"query","description":"Page size (1 to 100, default 50).","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","description":"Opaque cursor: the `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"type":"object","required":["data","next_cursor","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PayRun"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to get the next page; null on the last page."},"has_more":{"type":"boolean","description":"true when there is a next page."}}},"example":{"data":[{"id":"run_8f3a","label":"October 2026","period_month":"2026-10","period_start":"2026-10-01","period_end":"2026-10-31","pay_date":"2026-10-28","payroll_type":"main","status":"locked","revision":1,"currency":"EGP","totals":{"headcount":48,"errored_employees":0,"warnings":2,"gross":98450000,"si_employee":7210500,"si_employer":12150800,"uhi_employee":984500,"tax":9115300,"martyrs_fund":49225,"deductions":1250000,"net":79840475,"employer_cost":110600800,"unrecovered":0},"calculated_at":"2026-10-24T09:12:40.000Z","approved_at":"2026-10-25T11:03:05.000Z","locked_at":"2026-10-26T08:00:00.000Z","paid_at":null,"payslips_released":true},{"id":"run_77c1","label":"September 2026","period_month":"2026-09","period_start":"2026-09-01","period_end":"2026-09-30","pay_date":"2026-09-28","payroll_type":"main","status":"paid","revision":1,"currency":"EGP","totals":{"headcount":48,"errored_employees":0,"warnings":2,"gross":98450000,"si_employee":7210500,"si_employer":12150800,"uhi_employee":984500,"tax":9115300,"martyrs_fund":49225,"deductions":1250000,"net":79840475,"employer_cost":110600800,"unrecovered":0},"calculated_at":"2026-09-24T09:00:00.000Z","approved_at":"2026-09-25T10:00:00.000Z","locked_at":"2026-09-26T08:00:00.000Z","paid_at":"2026-09-28T10:00:00.000Z","payslips_released":true}],"next_cursor":null,"has_more":false}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/pay-runs/{id}":{"get":{"operationId":"getPayRun","summary":"Get a pay run with its totals","description":"**Scope:** `payruns:read`.","tags":["Pay runs"],"security":[{"ApiKey":["payruns:read"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Pay run id.","schema":{"type":"string"},"example":"run_8f3a"}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayRun"},"example":{"id":"run_8f3a","label":"October 2026","period_month":"2026-10","period_start":"2026-10-01","period_end":"2026-10-31","pay_date":"2026-10-28","payroll_type":"main","status":"locked","revision":1,"currency":"EGP","totals":{"headcount":48,"errored_employees":0,"warnings":2,"gross":98450000,"si_employee":7210500,"si_employer":12150800,"uhi_employee":984500,"tax":9115300,"martyrs_fund":49225,"deductions":1250000,"net":79840475,"employer_cost":110600800,"unrecovered":0},"calculated_at":"2026-10-24T09:12:40.000Z","approved_at":"2026-10-25T11:03:05.000Z","locked_at":"2026-10-26T08:00:00.000Z","paid_at":null,"payslips_released":true}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/pay-runs/{id}/results":{"get":{"operationId":"listPayRunResults","summary":"Per-employee results of a pay run","description":"Gross to net per employee with the result lines (earnings, statutory deductions, other deductions, employer contributions). Runs that are approved, locked or paid only (409 `run_not_final` otherwise); `run.final` is false while the run is only approved. Ordered by employee id. The first page of each read is written to the audit log.\n\n**Scope:** `payruns:read`.","tags":["Pay runs"],"security":[{"ApiKey":["payruns:read"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Pay run id.","schema":{"type":"string"},"example":"run_8f3a"},{"name":"lines","in":"query","description":"Set to `0` to leave out the result lines (faster).","schema":{"type":"string","enum":["0","1"],"default":"1"}},{"name":"limit","in":"query","description":"Page size (1 to 50, default 25).","schema":{"type":"integer","minimum":1,"maximum":50,"default":25}},{"name":"cursor","in":"query","description":"Opaque cursor: the `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResultsPage"},"example":{"run":{"id":"run_8f3a","period_month":"2026-10","status":"locked","revision":1,"final":true},"currency":"EGP","data":[{"employee_id":"emp_1a2b3c4d","employee_code":"NCT-0042","name":"Mona Hassan","department":"Finance","gross":2500000,"si_employee":183150,"si_employer":312188,"uhi_employee":25000,"tax":280000,"martyrs_fund":1250,"deductions":0,"net":2010600,"held":false,"employer_cost":2812188,"tax_treatment":"01","errors":0,"warnings":0,"calculated_at":"2026-10-24T09:12:40.000Z","lines":[{"code":"BASIC","group":"earning","label_en":"Basic salary","label_ar":"الأجر الأساسي","amount":2000000,"requested":2000000,"taxable":true,"insurable":true},{"code":"HOUSING","group":"earning","label_en":"Housing allowance","label_ar":"بدل سكن","amount":500000,"requested":500000,"taxable":true,"insurable":true},{"code":"SI_EMP","group":"statutory","label_en":"Social insurance (employee)","label_ar":"التأمينات الاجتماعية (العامل)","amount":183150,"requested":183150,"taxable":false,"insurable":false},{"code":"UHI_EMP","group":"statutory","label_en":"Universal Health Insurance","label_ar":"التأمين الصحي الشامل","amount":25000,"requested":25000,"taxable":false,"insurable":false},{"code":"TAX","group":"statutory","label_en":"Income tax","label_ar":"ضريبة كسب العمل","amount":280000,"requested":280000,"taxable":false,"insurable":false},{"code":"MARTYRS","group":"statutory","label_en":"Martyrs' fund","label_ar":"صندوق الشهداء","amount":1250,"requested":1250,"taxable":false,"insurable":false}]}],"next_cursor":"eyJhIjoiZW1wXzFhMmIzYzRkIn0","has_more":true}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/pay-runs/{id}/payslips":{"get":{"operationId":"listPayslips","summary":"List the payslips of a pay run","description":"Released runs only (409 `payslips_not_released` otherwise). Held salaries have no payslip here.\n\n**Scope:** `payslips:read`.","tags":["Payslips"],"security":[{"ApiKey":["payslips:read"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Pay run id.","schema":{"type":"string"},"example":"run_8f3a"},{"name":"limit","in":"query","description":"Page size (1 to 100, default 50).","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","description":"Opaque cursor: the `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayslipPage"},"example":{"currency":"EGP","data":[{"id":"run_8f3a.emp_1a2b3c4d","run_id":"run_8f3a","employee_id":"emp_1a2b3c4d","employee_code":"NCT-0042","name":"Mona Hassan","net":2010600,"pdf_path":"/api/v1/payslips/run_8f3a.emp_1a2b3c4d/pdf"},{"id":"run_8f3a.emp_2b3c4d5e","run_id":"run_8f3a","employee_id":"emp_2b3c4d5e","employee_code":"NCT-0057","name":"Karim Adel","net":1436900,"pdf_path":"/api/v1/payslips/run_8f3a.emp_2b3c4d5e/pdf"}],"next_cursor":null,"has_more":false}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/payslips/{id}/pdf":{"get":{"operationId":"getPayslipPdf","summary":"Download a payslip as PDF","description":"The id is `<run id>.<employee id>`, as listed by `GET /pay-runs/{id}/payslips`. Served only once the run's payslips are released. Limited to 30 PDFs a minute per key on top of the key's own limit. Each download is written to the audit log.\n\n**Scope:** `payslips:read`.","tags":["Payslips"],"security":[{"ApiKey":["payslips:read"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Payslip id: `<run id>.<employee id>`.","schema":{"type":"string"},"example":"run_8f3a.emp_1a2b3c4d"},{"name":"lang","in":"query","description":"Language of the document; default the organization's payslip language.","schema":{"type":"string","enum":["en","ar","both"]}}],"responses":{"200":{"description":"The PDF (Content-Disposition: inline with the file name).","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/reports":{"get":{"operationId":"listReports","summary":"List the reports and their parameters","description":"**Scope:** `reports:read`.","tags":["Reports"],"security":[{"ApiKey":["reports:read"]}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportList"},"example":{"data":[{"id":"register","parameters":["run_id"],"formats":["json","csv"]},{"id":"social-insurance","parameters":["run_id"],"formats":["json","csv"]},{"id":"tax","parameters":["run_id"],"formats":["json","csv"]},{"id":"year-end","parameters":["year"],"formats":["json","csv"]},{"id":"loans","parameters":[],"formats":["json","csv"]},{"id":"medical","parameters":["run_id"],"formats":["json","csv"]},{"id":"department-cost","parameters":["run_id"],"formats":["json","csv"]},{"id":"leavers","parameters":["from","to"],"formats":["json","csv"]},{"id":"leave-pay","parameters":["run_id"],"formats":["json","csv"]},{"id":"si-not-insured","parameters":["run_id"],"formats":["json","csv"]},{"id":"si-cost","parameters":["from","to","profile"],"formats":["json","csv"]},{"id":"service-charge","parameters":["from","to"],"formats":["json","csv"]}],"currency":"EGP"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/reports/{id}":{"get":{"operationId":"getReport","summary":"Run a report (JSON or CSV)","description":"Report ids: `register`, `social-insurance`, `tax`, `year-end`, `loans`, `medical`, `department-cost`, `leavers`, `leave-pay`, `si-not-insured`, `si-cost`, `service-charge`. `GET /reports` lists the parameters each one takes. Run-based reports need an approved, locked or paid run. In JSON, money columns (type `money`) are integer piastres; in CSV they are EGP with two decimals. Every export is written to the audit log; national IDs are never included.\n\n**Scope:** `reports:read`.","tags":["Reports"],"security":[{"ApiKey":["reports:read"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Report id.","schema":{"type":"string"},"example":"register"},{"name":"run_id","in":"query","description":"Pay run id (run-based reports).","schema":{"type":"string"},"example":"run_8f3a"},{"name":"year","in":"query","description":"Year, YYYY (year-end).","schema":{"type":"string","pattern":"^\\d{4}$"}},{"name":"department","in":"query","description":"Only this department.","schema":{"type":"string"}},{"name":"from","in":"query","description":"First date or period (leavers, si-cost, service-charge).","schema":{"type":"string"}},{"name":"to","in":"query","description":"Last date or period (leavers, si-cost, service-charge).","schema":{"type":"string"}},{"name":"profile","in":"query","description":"si-cost: only this SI profile id.","schema":{"type":"string"}},{"name":"format","in":"query","description":"Default `json`.","schema":{"type":"string","enum":["json","csv"],"default":"json"}},{"name":"lang","in":"query","description":"CSV headings.","schema":{"type":"string","enum":["en","ar","both"],"default":"en"}}],"responses":{"200":{"description":"The report","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportEnvelope"},"example":{"currency":"EGP","report":{"id":"register","title":{"en":"Payroll register","ar":"كشف الرواتب"},"note":null,"run":{"id":"run_8f3a","periodMonth":"2026-10","status":"locked","label":"October 2026","payrollType":"main","revision":1},"year":null,"department":null,"columns":[{"key":"code","en":"Code","ar":"الكود","type":"text"},{"key":"name","en":"Employee","ar":"الموظف","type":"text"},{"key":"department","en":"Department","ar":"القسم","type":"text"},{"key":"gross","en":"Gross pay","ar":"إجمالي الأجر","type":"money"},{"key":"net","en":"Net pay","ar":"صافي الأجر","type":"money"}],"rows":[{"code":"NCT-0042","name":"Mona Hassan","department":"Finance","gross":2500000,"net":2010600},{"code":"NCT-0057","name":"Karim Adel","department":"Sales","gross":1800000,"net":1436900}],"totals":{"code":"","name":"Total","department":"","gross":4300000,"net":3447500},"breakdown":null}}},"text/csv":{"schema":{"type":"string"},"example":"Code,Employee,Department,Gross pay,Net pay\r\nNCT-0042,Mona Hassan,Finance,25000.00,20106.00\r\nNCT-0057,Karim Adel,Sales,18000.00,14369.00\r\n"}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/si-profiles":{"get":{"operationId":"listSiProfiles","summary":"List SI profiles, the default and the rules","description":"Each profile with its rate in force today (`current_rate`, raw: null = follows the statutory rules), the same merged with today's statutory rules (`current_merged`) and the next scheduled change; the organization default and the assignment rule lists over time. One page (`has_more` is always false).\n\n**Scope:** `si_profiles:read`.","tags":["Social insurance"],"security":[{"ApiKey":["si_profiles:read"]}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiProfileList"},"example":{"today":"2026-10-18","earliest_effective_from":"2026-11-01","defaults":{"current":{"id":"sid_2c3d","effective_from":"2026-11-01","profile_id":"sip_1a2b","created_at":"2026-10-18T09:02:00.000Z"},"next":null,"history":[{"id":"sid_2c3d","effective_from":"2026-11-01","profile_id":"sip_1a2b","created_at":"2026-10-18T09:02:00.000Z"}]},"rules":{"current":null,"next":{"id":"lst_4e5f","effective_from":"2026-11-01","note":"Branch staff on the branch profile","created_at":"2026-10-18T09:05:00.000Z","rules":[{"id":"rul_01","name":"Alexandria staff","profile_id":"sip_3c4d","conditions":[{"field":"location","values":["Alexandria"]}]}]},"history":[{"id":"lst_4e5f","effective_from":"2026-11-01","note":"Branch staff on the branch profile","created_at":"2026-10-18T09:05:00.000Z","rules":[{"id":"rul_01","name":"Alexandria staff","profile_id":"sip_3c4d","conditions":[{"field":"location","values":["Alexandria"]}]}]}]},"statutory":null,"employees_on_default":31,"currency":"EGP","data":[{"id":"sip_1a2b","name":"Head office (Cairo)","description":"Head-office staff insured at the Nasr City office","is_active":true,"employer_insurance_number":"","insurance_office":"Nasr City","current_rate":{"id":"rate_9c8d","effective_from":"2026-11-01","min_insurable":null,"max_insurable":null,"branches":{"pension":{"employee_rate":null,"employer_rate":null},"sickness":{"employee_rate":null,"employer_rate":null},"work_injury":{"employee_rate":null,"employer_rate":null},"unemployment":{"employee_rate":null,"employer_rate":null}},"treasury_share":0,"employer_pays_employee_share":false,"insurable_base":"components","note":"Follows the statutory limits","created_at":"2026-10-18T09:00:00.000Z"},"next_rate":null,"current_merged":null,"is_default":true,"employees_current":31,"versions_using":31}],"next_cursor":null,"has_more":false}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}},"post":{"operationId":"createSiProfile","summary":"Create an SI profile with its first rate","description":"The rate starts on `rate.effective_from` (default: the first of the current month, or the first open date). Rejected (409 `si_date_locked`) on or before the end of a locked or paid run; 422 `si_profile_merge_invalid` when the values are not valid with the statutory rules of that stretch.\n\n**Scope:** `si_profiles:write`.","tags":["Social insurance"],"security":[{"ApiKey":["si_profiles:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiProfileInput"},"example":{"name":"Alexandria branch","description":"Branch staff insured at the Smouha office","insurance_office":"Smouha","rate":{"effective_from":"2026-11-01","min_insurable":null,"max_insurable":null,"branches":{"pension":{"employee_rate":null,"employer_rate":null},"sickness":{"employee_rate":null,"employer_rate":null},"work_injury":{"employee_rate":null,"employer_rate":null},"unemployment":{"employee_rate":null,"employer_rate":null}},"treasury_share":0,"employer_pays_employee_share":false,"insurable_base":"components","note":""}}}}},"responses":{"201":{"description":"Created","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SiProfile"}}},"example":{"data":{"id":"sip_3c4d","name":"Alexandria branch","description":"Branch staff insured at the Smouha office","is_active":true,"employer_insurance_number":"","insurance_office":"Smouha","current_rate":{"id":"rate_9c8d","effective_from":"2026-11-01","min_insurable":null,"max_insurable":null,"branches":{"pension":{"employee_rate":null,"employer_rate":null},"sickness":{"employee_rate":null,"employer_rate":null},"work_injury":{"employee_rate":null,"employer_rate":null},"unemployment":{"employee_rate":null,"employer_rate":null}},"treasury_share":0,"employer_pays_employee_share":false,"insurable_base":"components","note":"Follows the statutory limits","created_at":"2026-10-18T09:00:00.000Z"},"next_rate":null,"current_merged":null,"is_default":false,"employees_current":0,"versions_using":0}}}}},"401":{"$ref":"#/components/responses/E401"},"402":{"$ref":"#/components/responses/E402"},"403":{"$ref":"#/components/responses/E403"},"409":{"$ref":"#/components/responses/E409"},"422":{"$ref":"#/components/responses/E422"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/si-profiles/{id}":{"get":{"operationId":"getSiProfile","summary":"Get an SI profile with its rate history","description":"**Scope:** `si_profiles:read`.","tags":["Social insurance"],"security":[{"ApiKey":["si_profiles:read"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"SI profile id.","schema":{"type":"string"},"example":"sip_1a2b"}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiProfileDetail"},"example":{"profile":{"id":"sip_1a2b","name":"Head office (Cairo)","description":"Head-office staff insured at the Nasr City office","is_active":true,"employer_insurance_number":"","insurance_office":"Nasr City","current_rate":{"id":"rate_9c8d","effective_from":"2026-11-01","min_insurable":null,"max_insurable":null,"branches":{"pension":{"employee_rate":null,"employer_rate":null},"sickness":{"employee_rate":null,"employer_rate":null},"work_injury":{"employee_rate":null,"employer_rate":null},"unemployment":{"employee_rate":null,"employer_rate":null}},"treasury_share":0,"employer_pays_employee_share":false,"insurable_base":"components","note":"Follows the statutory limits","created_at":"2026-10-18T09:00:00.000Z"},"next_rate":null,"current_merged":null,"is_default":true,"employees_current":31,"versions_using":31},"rates":[{"id":"rate_9c8d","effective_from":"2026-11-01","min_insurable":null,"max_insurable":null,"branches":{"pension":{"employee_rate":null,"employer_rate":null},"sickness":{"employee_rate":null,"employer_rate":null},"work_injury":{"employee_rate":null,"employer_rate":null},"unemployment":{"employee_rate":null,"employer_rate":null}},"treasury_share":0,"employer_pays_employee_share":false,"insurable_base":"components","note":"Follows the statutory limits","created_at":"2026-10-18T09:00:00.000Z","status":"current","merged":null,"deletable":false}],"current_rate":{"id":"rate_9c8d","effective_from":"2026-11-01","min_insurable":null,"max_insurable":null,"branches":{"pension":{"employee_rate":null,"employer_rate":null},"sickness":{"employee_rate":null,"employer_rate":null},"work_injury":{"employee_rate":null,"employer_rate":null},"unemployment":{"employee_rate":null,"employer_rate":null}},"treasury_share":0,"employer_pays_employee_share":false,"insurable_base":"components","note":"Follows the statutory limits","created_at":"2026-10-18T09:00:00.000Z"},"next_rate":null,"statutory":null,"preset":null,"earliest_effective_from":"2026-11-01","currency":"EGP"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/si-profiles/{id}/rates":{"post":{"operationId":"addSiRate","summary":"Schedule a change of limits or rates","description":"Rows are never edited: a change is a new row from a date. Earlier periods keep their rate; open pay runs from that date are recalculated.\n\n**Scope:** `si_profiles:write`.","tags":["Social insurance"],"security":[{"ApiKey":["si_profiles:write"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"SI profile id.","schema":{"type":"string"},"example":"sip_1a2b"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiRateInput"},"example":{"effective_from":"2027-01-01","min_insurable":270000,"max_insurable":1665000,"insurable_base":"components","note":"New statutory limits for 2027"}}}},"responses":{"201":{"description":"Created","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SiRate"}}},"example":{"data":{"id":"rate_a1b2","effective_from":"2027-01-01","min_insurable":270000,"max_insurable":1665000,"branches":{"pension":{"employee_rate":null,"employer_rate":null},"sickness":{"employee_rate":null,"employer_rate":null},"work_injury":{"employee_rate":null,"employer_rate":null},"unemployment":{"employee_rate":null,"employer_rate":null}},"treasury_share":0,"employer_pays_employee_share":false,"insurable_base":"components","note":"New statutory limits for 2027","created_at":"2026-10-18T09:00:00.000Z"}}}}},"401":{"$ref":"#/components/responses/E401"},"402":{"$ref":"#/components/responses/E402"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"422":{"$ref":"#/components/responses/E422"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/si-profiles/{id}/rates/{rate_id}":{"delete":{"operationId":"cancelSiRate","summary":"Cancel a scheduled rate change","description":"Only a change that starts after every locked or paid period, never the first rate.\n\n**Scope:** `si_profiles:write`.","tags":["Social insurance"],"security":[{"ApiKey":["si_profiles:write"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"SI profile id.","schema":{"type":"string"},"example":"sip_1a2b"},{"name":"rate_id","in":"path","required":true,"description":"Rate id.","schema":{"type":"string"},"example":"rate_a1b2"}],"responses":{"204":{"description":"Cancelled","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}}},"401":{"$ref":"#/components/responses/E401"},"402":{"$ref":"#/components/responses/E402"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"422":{"$ref":"#/components/responses/E422"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/si-defaults":{"post":{"operationId":"setSiDefault","summary":"Set the organization default profile from a date","description":"`profile_id` null = no default from that date (the statutory rules apply to employees without a profile).\n\n**Scope:** `si_profiles:write`.","tags":["Social insurance"],"security":[{"ApiKey":["si_profiles:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiDefaultInput"},"example":{"profile_id":"sip_1a2b","effective_from":"2026-11-01"}}}},"responses":{"201":{"description":"Created","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SiDefault"}}},"example":{"data":{"id":"sid_2c3d","effective_from":"2026-11-01","profile_id":"sip_1a2b","created_at":"2026-10-18T09:02:00.000Z"}}}}},"401":{"$ref":"#/components/responses/E401"},"402":{"$ref":"#/components/responses/E402"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"422":{"$ref":"#/components/responses/E422"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/si-defaults/{id}":{"delete":{"operationId":"cancelSiDefault","summary":"Cancel a scheduled default change","description":"**Scope:** `si_profiles:write`.","tags":["Social insurance"],"security":[{"ApiKey":["si_profiles:write"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Default change id.","schema":{"type":"string"},"example":"sid_2c3d"}],"responses":{"204":{"description":"Cancelled","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}}},"401":{"$ref":"#/components/responses/E401"},"402":{"$ref":"#/components/responses/E402"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/si-rules":{"get":{"operationId":"listSiRules","summary":"List the assignment rule lists over time","description":"**Scope:** `si_profiles:read`.","tags":["Social insurance"],"security":[{"ApiKey":["si_profiles:read"]}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"type":"object","required":["data","next_cursor","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SiRuleList"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to get the next page; null on the last page."},"has_more":{"type":"boolean","description":"true when there is a next page."}}},"example":{"data":[{"id":"lst_4e5f","effective_from":"2026-11-01","note":"Branch staff on the branch profile","created_at":"2026-10-18T09:05:00.000Z","rules":[{"id":"rul_01","name":"Alexandria staff","profile_id":"sip_3c4d","conditions":[{"field":"location","values":["Alexandria"]}]}]}],"next_cursor":null,"has_more":false}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}},"post":{"operationId":"setSiRules","summary":"Set a new full rule list from a date","description":"Ordered: the first rule whose conditions all hold applies to employees whose salary package names no profile of its own. An empty list = no rules from that date.\n\n**Scope:** `si_profiles:write`.","tags":["Social insurance"],"security":[{"ApiKey":["si_profiles:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiRulesInput"},"example":{"effective_from":"2026-11-01","note":"Branch staff on the branch profile","rules":[{"name":"Alexandria staff","profile_id":"sip_3c4d","conditions":[{"field":"location","values":["Alexandria"]}]}]}}}},"responses":{"201":{"description":"Created","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SiRuleList"}}},"example":{"data":{"id":"lst_4e5f","effective_from":"2026-11-01","note":"Branch staff on the branch profile","created_at":"2026-10-18T09:05:00.000Z","rules":[{"id":"rul_01","name":"Alexandria staff","profile_id":"sip_3c4d","conditions":[{"field":"location","values":["Alexandria"]}]}]}}}}},"401":{"$ref":"#/components/responses/E401"},"402":{"$ref":"#/components/responses/E402"},"403":{"$ref":"#/components/responses/E403"},"409":{"$ref":"#/components/responses/E409"},"422":{"$ref":"#/components/responses/E422"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/si-rules/{id}":{"delete":{"operationId":"cancelSiRules","summary":"Cancel a scheduled rule list","description":"**Scope:** `si_profiles:write`.","tags":["Social insurance"],"security":[{"ApiKey":["si_profiles:write"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Rule list id.","schema":{"type":"string"},"example":"lst_4e5f"}],"responses":{"204":{"description":"Cancelled","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}}},"401":{"$ref":"#/components/responses/E401"},"402":{"$ref":"#/components/responses/E402"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/si-assignments":{"post":{"operationId":"bulkAssignSiProfile","summary":"Put a profile on many employees from a date","description":"One new salary version per employee that changes nothing but the profile. Every employee is checked first; any problem = nothing written (422 `bulk_failed`, `details.results` per employee). `dry_run: true` checks only.\n\n**Scope:** `si_profiles:write`.","tags":["Social insurance"],"security":[{"ApiKey":["si_profiles:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiAssignInput"},"example":{"employee_ids":["emp_1a2b3c4d","emp_2b3c4d5e"],"si_profile_id":"sip_3c4d","effective_from":"2026-11-01","note":"Moved to the Alexandria branch","dry_run":true}}}},"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiAssignResult"},"example":{"applied":0,"would_apply":2,"failed":0,"unchanged":0,"dry_run":true,"results":[{"employee_id":"emp_1a2b3c4d","name":"Mona Hassan","status":"ok","error":null,"from":"sip_1a2b"},{"employee_id":"emp_2b3c4d5e","name":"Karim Adel","status":"ok","error":null,"from":null}]}}}},"401":{"$ref":"#/components/responses/E401"},"402":{"$ref":"#/components/responses/E402"},"403":{"$ref":"#/components/responses/E403"},"409":{"$ref":"#/components/responses/E409"},"422":{"$ref":"#/components/responses/E422"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/employees/{id}/si-profile":{"get":{"operationId":"getEmployeeSiProfile","summary":"The SI terms an employee is paid with on a date","description":"Which profile applies and why (`resolution.source`: explicit, rule, default or statutory), the rate row in force and the values merged with the statutory rules of that date.\n\n**Scope:** `si_profiles:read`.","tags":["Social insurance"],"security":[{"ApiKey":["si_profiles:read"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Employee id.","schema":{"type":"string"},"example":"emp_1a2b3c4d"},{"name":"as_of","in":"query","description":"Date (YYYY-MM-DD); default the organization's today.","schema":{"type":"string","format":"date"},"example":"2026-10-18"}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiResolved"},"example":{"employee_id":"emp_1a2b3c4d","as_of":"2026-10-18","salary_version_id":"sv_6f7a","insured":true,"resolution":{"source":"default","profile_id":"sip_1a2b","profile_name":"Head office (Cairo)"},"in_force":true,"rate":{"id":"rate_9c8d","effective_from":"2026-11-01","min_insurable":null,"max_insurable":null,"branches":{"pension":{"employee_rate":null,"employer_rate":null},"sickness":{"employee_rate":null,"employer_rate":null},"work_injury":{"employee_rate":null,"employer_rate":null},"unemployment":{"employee_rate":null,"employer_rate":null}},"treasury_share":0,"employer_pays_employee_share":false,"insurable_base":"components","note":"Follows the statutory limits","created_at":"2026-10-18T09:00:00.000Z"},"rule_set":"EG-2026","effective":{"min_insurable":270000,"max_insurable":1665000,"treasury_share":0,"employer_pays_employee_share":false,"insurable_base":"components"},"currency":"EGP"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/commission-profiles":{"get":{"operationId":"listCommissionProfiles","summary":"List commission profiles and tiers","description":"The commission profiles configured for the organization with their tiers (slabs).\n\n**Scope:** `commissions:read`.","tags":["Commissions"],"security":[{"ApiKey":["commissions:read"]}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommissionProfileList"},"example":{"data":[{"id":"cpr_51aa","name":"Field sales 2026","description":"Tiered on monthly collected sales","calc_type":"tiered_slabs","fixed_rate_pct":null,"monthly_target":50000000,"active":true,"slabs":[{"tier_order":1,"from_amount":0,"to_amount":50000000,"rate_pct":2},{"tier_order":2,"from_amount":50000000,"to_amount":null,"rate_pct":3.5}]}]}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/commissions":{"get":{"operationId":"listCommissions","summary":"List posted commissions","description":"Commissions posted through the API or the CRM integration, newest first. `total` counts every match of the filters.\n\n**Scope:** `commissions:read`.","tags":["Commissions"],"security":[{"ApiKey":["commissions:read"]}],"parameters":[{"name":"payrun_id","in":"query","description":"Only this pay run.","schema":{"type":"string"},"example":"run_9b2e"},{"name":"employee_id","in":"query","description":"Only this employee.","schema":{"type":"string"},"example":"emp_2b3c4d5e"},{"name":"limit","in":"query","description":"Page size (1 to 100, default 50).","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","description":"Opaque cursor: the `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommissionList"},"example":{"data":[{"id":"1f0c2a9e-3b7d-4c51-9e2a-6d8b0f4a7c12","run_id":"run_9b2e","run_label":"November 2026","period_month":"2026-11","employee_id":"emp_2b3c4d5e","employee_name":"Karim Adel","employee_code":"NCT-0057","amount":750000,"currency":"EGP","label":"Sales Commission (API)","note":"Ref: CRM-DEAL-10482 | Q4 distributor deal","created_at":"2026-10-20T12:00:00.000Z"}],"currency":"EGP","total":1,"next_cursor":null,"has_more":false}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}},"post":{"operationId":"postCommissions","summary":"Post sales commissions","description":"Post one commission (the item fields at the top level) or up to 200 (`commissions`) into an open pay run, as earnings on the payslip. Employees are matched by `employee_id`, `employee_code` or `employee_email`. Without `payrun_id`, the draft or calculated run whose period covers `date` (default today) is used; 409 `no_open_payrun` when there is none, `run_approved` / `run_locked` when the run is past that. All or nothing: one unknown employee (422 `employee_not_found`) posts nothing. Not idempotent: see \"Idempotency and retries\".\n\n**Scope:** `commissions:write`.","tags":["Commissions"],"security":[{"ApiKey":["commissions:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostCommissionsInput"},"examples":{"batch":{"summary":"Several commissions","value":{"date":"2026-10-20","commissions":[{"employee_code":"NCT-0057","amount":750000,"reference":"CRM-DEAL-10482","note":"Q4 distributor deal"},{"employee_email":"mona.hassan@nilecrest.example","amount":120000,"reference":"INV-2026-0931","taxable":true}]}},"single":{"summary":"One commission","value":{"employee_id":"emp_2b3c4d5e","amount":500000,"date":"2026-10-20","reference":"CRM-DEAL-10490"}}}}}},"responses":{"201":{"description":"Created","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostCommissionsResult"},"example":{"data":[{"id":"1f0c2a9e-3b7d-4c51-9e2a-6d8b0f4a7c12","employee_id":"emp_2b3c4d5e","employee_name":"Karim Adel","employee_code":"NCT-0057","amount":750000,"currency":"EGP","label":"Sales Commission (API)","reference":"CRM-DEAL-10482","note":"Ref: CRM-DEAL-10482 | Q4 distributor deal","created_at":"2026-10-20T12:00:00.000Z"},{"id":"7a3e5b1c-9d2f-4e60-8a1b-2c4d6e8f0a13","employee_id":"emp_1a2b3c4d","employee_name":"Mona Hassan","employee_code":"NCT-0042","amount":120000,"currency":"EGP","label":"Sales Commission (API)","reference":"INV-2026-0931","note":"Ref: INV-2026-0931","created_at":"2026-10-20T12:00:00.000Z"}],"run":{"id":"run_9b2e","label":"November 2026","period_month":"2026-11","status":"draft"},"summary":{"posted_count":2,"total_amount":870000,"currency":"EGP"}}}}},"401":{"$ref":"#/components/responses/E401"},"402":{"$ref":"#/components/responses/E402"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"422":{"$ref":"#/components/responses/E422"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/pay-runs/{id}/commissions":{"post":{"operationId":"postPayRunCommissions","summary":"Post commissions into a given pay run","description":"Like `POST /commissions`, into this draft or calculated pay run (`payrun_id` and `date` in the body are ignored for the choice of run).\n\n**Scope:** `commissions:write`.","tags":["Commissions"],"security":[{"ApiKey":["commissions:write"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Pay run id.","schema":{"type":"string"},"example":"run_9b2e"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostCommissionsInput"},"example":{"date":"2026-10-20","commissions":[{"employee_code":"NCT-0057","amount":750000,"reference":"CRM-DEAL-10482","note":"Q4 distributor deal"},{"employee_email":"mona.hassan@nilecrest.example","amount":120000,"reference":"INV-2026-0931","taxable":true}]}}}},"responses":{"201":{"description":"Created","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostCommissionsResult"},"example":{"data":[{"id":"1f0c2a9e-3b7d-4c51-9e2a-6d8b0f4a7c12","employee_id":"emp_2b3c4d5e","employee_name":"Karim Adel","employee_code":"NCT-0057","amount":750000,"currency":"EGP","label":"Sales Commission (API)","reference":"CRM-DEAL-10482","note":"Ref: CRM-DEAL-10482 | Q4 distributor deal","created_at":"2026-10-20T12:00:00.000Z"},{"id":"7a3e5b1c-9d2f-4e60-8a1b-2c4d6e8f0a13","employee_id":"emp_1a2b3c4d","employee_name":"Mona Hassan","employee_code":"NCT-0042","amount":120000,"currency":"EGP","label":"Sales Commission (API)","reference":"INV-2026-0931","note":"Ref: INV-2026-0931","created_at":"2026-10-20T12:00:00.000Z"}],"run":{"id":"run_9b2e","label":"November 2026","period_month":"2026-11","status":"draft"},"summary":{"posted_count":2,"total_amount":870000,"currency":"EGP"}}}}},"401":{"$ref":"#/components/responses/E401"},"402":{"$ref":"#/components/responses/E402"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"409":{"$ref":"#/components/responses/E409"},"422":{"$ref":"#/components/responses/E422"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/webhooks":{"get":{"operationId":"listWebhooks","summary":"List webhook endpoints","description":"Also lists the event types you can subscribe to.\n\n**Scope:** `webhooks:manage`.","tags":["Webhook endpoints"],"security":[{"ApiKey":["webhooks:manage"]}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookList"},"example":{"data":[{"id":"whe_3d9f","url":"https://erp.nilecrest.example/hooks/wanas","description":"ERP payroll journal","events":["payrun.locked","payslip.released"],"status":"active","disabled_reason":null,"consecutive_failures":0,"secret_last4":"a91f","last_success_at":"2026-10-26T08:00:03.000Z","last_failure_at":null,"created_at":"2026-10-12T10:00:00.000Z"}],"events":["payrun.calculated","payrun.approved","payrun.locked","payrun.paid","payslip.released","employee.synced","ot.decided","si_profile.created","si_profile.rates_changed","si_profile.default_changed","si_profile.rules_changed","si_profile.assigned"]}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}},"post":{"operationId":"createWebhook","summary":"Create a webhook endpoint","description":"`https` on port 443 and a public host name; up to 10 endpoints. The signing secret is in this response **once** and never again: store it now.\n\n**Scope:** `webhooks:manage`.","tags":["Webhook endpoints"],"security":[{"ApiKey":["webhooks:manage"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookInput"},"example":{"url":"https://erp.nilecrest.example/hooks/wanas","events":["payrun.locked","payslip.released"],"description":"ERP payroll journal"}}}},"responses":{"201":{"description":"Created","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookWithSecret"},"example":{"id":"whe_3d9f","url":"https://erp.nilecrest.example/hooks/wanas","description":"ERP payroll journal","events":["payrun.locked","payslip.released"],"status":"active","disabled_reason":null,"consecutive_failures":0,"secret_last4":"a91f","last_success_at":null,"last_failure_at":null,"created_at":"2026-10-12T10:00:00.000Z","secret":"whsec_EXAMPLE_ONLY_shown_once_store_it_now"}}}},"401":{"$ref":"#/components/responses/E401"},"402":{"$ref":"#/components/responses/E402"},"403":{"$ref":"#/components/responses/E403"},"409":{"$ref":"#/components/responses/E409"},"422":{"$ref":"#/components/responses/E422"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/webhooks/{id}":{"patch":{"operationId":"updateWebhook","summary":"Change an endpoint","description":"Change the URL, events, description or status (`active` / `disabled`). Switching an endpoint off works even when the organization is read-only.\n\n**Scope:** `webhooks:manage`.","tags":["Webhook endpoints"],"security":[{"ApiKey":["webhooks:manage"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook endpoint id.","schema":{"type":"string"},"example":"whe_3d9f"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPatch"},"example":{"events":["payrun.approved","payrun.locked","payslip.released"],"status":"active"}}}},"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"},"example":{"id":"whe_3d9f","url":"https://erp.nilecrest.example/hooks/wanas","description":"ERP payroll journal","events":["payrun.approved","payrun.locked","payslip.released"],"status":"active","disabled_reason":null,"consecutive_failures":0,"secret_last4":"a91f","last_success_at":"2026-10-26T08:00:03.000Z","last_failure_at":null,"created_at":"2026-10-12T10:00:00.000Z"}}}},"401":{"$ref":"#/components/responses/E401"},"402":{"$ref":"#/components/responses/E402"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"422":{"$ref":"#/components/responses/E422"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}},"delete":{"operationId":"deleteWebhook","summary":"Delete an endpoint and its delivery log","description":"**Scope:** `webhooks:manage`.","tags":["Webhook endpoints"],"security":[{"ApiKey":["webhooks:manage"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook endpoint id.","schema":{"type":"string"},"example":"whe_3d9f"}],"responses":{"204":{"description":"Deleted","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/webhooks/{id}/rotate-secret":{"post":{"operationId":"rotateWebhookSecret","summary":"Rotate the signing secret","description":"The new secret is in this response once; the old one stops working at once.\n\n**Scope:** `webhooks:manage`.","tags":["Webhook endpoints"],"security":[{"ApiKey":["webhooks:manage"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook endpoint id.","schema":{"type":"string"},"example":"whe_3d9f"}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookWithSecret"},"example":{"id":"whe_3d9f","url":"https://erp.nilecrest.example/hooks/wanas","description":"ERP payroll journal","events":["payrun.locked","payslip.released"],"status":"active","disabled_reason":null,"consecutive_failures":0,"secret_last4":"a91f","last_success_at":null,"last_failure_at":null,"created_at":"2026-10-12T10:00:00.000Z","secret":"whsec_EXAMPLE_ONLY_shown_once_store_it_now"}}}},"401":{"$ref":"#/components/responses/E401"},"402":{"$ref":"#/components/responses/E402"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/webhooks/{id}/test":{"post":{"operationId":"testWebhook","summary":"Send a test event now","description":"Sends a `webhook.test` event to the endpoint and answers with the delivery. Limited to a few a minute.\n\n**Scope:** `webhooks:manage`.","tags":["Webhook endpoints"],"security":[{"ApiKey":["webhooks:manage"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook endpoint id.","schema":{"type":"string"},"example":"whe_3d9f"}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Delivery"},{"type":"object","required":["status"],"properties":{"status":{"const":"unknown"}}}]},"example":{"id":"dlv_0c11","endpoint_id":"whe_3d9f","event_id":"evt_test_51d2","event_type":"webhook.test","status":"delivered","attempts":1,"next_attempt_at":null,"last_attempt_at":"2026-10-26T08:00:02.000Z","last_status_code":200,"last_error":null,"delivered_at":"2026-10-26T08:00:03.000Z","is_test":true,"replay_of":null,"created_at":"2026-10-26T08:00:01.000Z"}}}},"401":{"$ref":"#/components/responses/E401"},"402":{"$ref":"#/components/responses/E402"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"422":{"$ref":"#/components/responses/E422"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/webhooks/{id}/deliveries":{"get":{"operationId":"listWebhookDeliveries","summary":"Delivery log of an endpoint","description":"Newest first; kept 30 days.\n\n**Scope:** `webhooks:manage`.","tags":["Webhook endpoints"],"security":[{"ApiKey":["webhooks:manage"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook endpoint id.","schema":{"type":"string"},"example":"whe_3d9f"},{"name":"limit","in":"query","description":"Page size (1 to 100, default 50).","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","description":"Opaque cursor: the `next_cursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"type":"object","required":["data","next_cursor","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Delivery"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to get the next page; null on the last page."},"has_more":{"type":"boolean","description":"true when there is a next page."}}},"example":{"data":[{"id":"dlv_0b7e","endpoint_id":"whe_3d9f","event_id":"evt_9c2f41d07a1b5e83","event_type":"payrun.locked","status":"delivered","attempts":1,"next_attempt_at":null,"last_attempt_at":"2026-10-26T08:00:02.000Z","last_status_code":200,"last_error":null,"delivered_at":"2026-10-26T08:00:03.000Z","is_test":false,"replay_of":null,"created_at":"2026-10-26T08:00:01.000Z"}],"next_cursor":null,"has_more":false}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}},"/webhook-deliveries/{id}":{"get":{"operationId":"getWebhookDelivery","summary":"Get a delivery with its attempts","description":"**Scope:** `webhooks:manage`.","tags":["Webhook endpoints"],"security":[{"ApiKey":["webhooks:manage"]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Delivery id.","schema":{"type":"string"},"example":"dlv_0b7e"}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryDetail"},"example":{"id":"dlv_0b7e","endpoint_id":"whe_3d9f","event_id":"evt_9c2f41d07a1b5e83","event_type":"payrun.locked","status":"delivered","attempts":1,"next_attempt_at":null,"last_attempt_at":"2026-10-26T08:00:02.000Z","last_status_code":200,"last_error":null,"delivered_at":"2026-10-26T08:00:03.000Z","is_test":false,"replay_of":null,"created_at":"2026-10-26T08:00:01.000Z","attempts_log":[{"n":1,"at":"2026-10-26T08:00:02.000Z","status_code":200,"error":null,"duration_ms":182}]}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"429":{"$ref":"#/components/responses/E429"},"500":{"$ref":"#/components/responses/E500"}}}}},"webhooks":{"payrun.calculated":{"post":{"operationId":"event_payrun_calculated","summary":"payrun.calculated","security":[],"description":"A pay run was calculated (or recalculated). The body carries ids and statuses only; fetch the details with the REST API. Verify `Wanas-Signature` before you trust the body (snippets in the introduction, \"Verify the signature\").","parameters":[{"name":"Wanas-Signature","in":"header","required":true,"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>`","schema":{"type":"string"},"example":"t=1792915201,v1=5d41402abc4b2a76b9719d911017c592ae2f1e0c4c7f1b2f5b2e8c3d9a1f6e70"},{"name":"Wanas-Event","in":"header","required":true,"description":"The event type.","schema":{"type":"string"},"example":"payrun.calculated"},{"name":"Wanas-Event-Id","in":"header","required":true,"description":"The event id; the same on every retry.","schema":{"type":"string"},"example":"evt_9c2f41d07a1b5e83"},{"name":"Wanas-Delivery-Id","in":"header","required":true,"description":"This delivery (one per endpoint and event).","schema":{"type":"string"},"example":"dlv_0b7e"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/EventEnvelope"},{"type":"object","properties":{"type":{"const":"payrun.calculated"},"data":{"$ref":"#/components/schemas/Event_payrun_calculated"}}}]},"example":{"id":"evt_9c2f41d07a1b5e83","type":"payrun.calculated","api_version":"2026-10-12","created_at":"2026-10-26T08:00:01.000Z","org_id":"org_7d2c41ab","data":{"run_id":"run_8f3a","period_month":"2026-10","payroll_type":"main","status":"calculated","revision":1,"scope":"full"}}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx status within 10 seconds. Anything else is retried (9 attempts over about 22.7 hours)."}},"x-codeSamples":[{"lang":"JavaScript","label":"Verify (Node.js)","source":"import crypto from 'node:crypto';\nimport express from 'express';\n\n// rawBody: the request body exactly as received (a Buffer or string), never re-serialised JSON.\nexport function verifyWanasSignature(secret, rawBody, header, toleranceSec = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n  const expected = crypto.createHmac('sha256', secret).update(t + '.').update(rawBody).digest();\n  const given = Buffer.from(parts.v1 ?? '', 'hex');\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n\nconst app = express();\napp.post('/hooks/wanas', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!verifyWanasSignature(process.env.WANAS_WEBHOOK_SECRET, req.body, req.get('Wanas-Signature') ?? '')) return res.sendStatus(400);\n  const event = JSON.parse(req.body.toString('utf8'));\n  // event.id is the same on every retry: skip ids you have already processed.\n  res.sendStatus(200);\n});"},{"lang":"Python","label":"Verify (Python)","source":"import hashlib\nimport hmac\nimport os\nimport time\n\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n\ndef verify_wanas_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:\n    \"\"\"raw_body: the request body exactly as received (bytes), never re-serialised JSON.\"\"\"\n    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n    try:\n        t = int(parts[\"t\"])\n    except (KeyError, ValueError):\n        return False\n    if abs(time.time() - t) > tolerance:\n        return False\n    expected = hmac.new(secret.encode(), str(t).encode() + b\".\" + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n\n\n@app.post(\"/hooks/wanas\")\ndef wanas_webhook():\n    if not verify_wanas_signature(os.environ[\"WANAS_WEBHOOK_SECRET\"], request.get_data(), request.headers.get(\"Wanas-Signature\", \"\")):\n        return \"\", 400\n    event = request.get_json()  # event[\"id\"] is the same on every retry: skip ids you have already processed\n    return \"\", 200"}]}},"payrun.approved":{"post":{"operationId":"event_payrun_approved","summary":"payrun.approved","security":[],"description":"A pay run was approved. The body carries ids and statuses only; fetch the details with the REST API. Verify `Wanas-Signature` before you trust the body (snippets in the introduction, \"Verify the signature\").","parameters":[{"name":"Wanas-Signature","in":"header","required":true,"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>`","schema":{"type":"string"},"example":"t=1792915201,v1=5d41402abc4b2a76b9719d911017c592ae2f1e0c4c7f1b2f5b2e8c3d9a1f6e70"},{"name":"Wanas-Event","in":"header","required":true,"description":"The event type.","schema":{"type":"string"},"example":"payrun.approved"},{"name":"Wanas-Event-Id","in":"header","required":true,"description":"The event id; the same on every retry.","schema":{"type":"string"},"example":"evt_9c2f41d07a1b5e83"},{"name":"Wanas-Delivery-Id","in":"header","required":true,"description":"This delivery (one per endpoint and event).","schema":{"type":"string"},"example":"dlv_0b7e"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/EventEnvelope"},{"type":"object","properties":{"type":{"const":"payrun.approved"},"data":{"$ref":"#/components/schemas/Event_payrun_approved"}}}]},"example":{"id":"evt_9c2f41d07a1b5e83","type":"payrun.approved","api_version":"2026-10-12","created_at":"2026-10-26T08:00:01.000Z","org_id":"org_7d2c41ab","data":{"run_id":"run_8f3a","period_month":"2026-10","payroll_type":"main","status":"approved","revision":1}}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx status within 10 seconds. Anything else is retried (9 attempts over about 22.7 hours)."}},"x-codeSamples":[{"lang":"JavaScript","label":"Verify (Node.js)","source":"import crypto from 'node:crypto';\nimport express from 'express';\n\n// rawBody: the request body exactly as received (a Buffer or string), never re-serialised JSON.\nexport function verifyWanasSignature(secret, rawBody, header, toleranceSec = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n  const expected = crypto.createHmac('sha256', secret).update(t + '.').update(rawBody).digest();\n  const given = Buffer.from(parts.v1 ?? '', 'hex');\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n\nconst app = express();\napp.post('/hooks/wanas', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!verifyWanasSignature(process.env.WANAS_WEBHOOK_SECRET, req.body, req.get('Wanas-Signature') ?? '')) return res.sendStatus(400);\n  const event = JSON.parse(req.body.toString('utf8'));\n  // event.id is the same on every retry: skip ids you have already processed.\n  res.sendStatus(200);\n});"},{"lang":"Python","label":"Verify (Python)","source":"import hashlib\nimport hmac\nimport os\nimport time\n\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n\ndef verify_wanas_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:\n    \"\"\"raw_body: the request body exactly as received (bytes), never re-serialised JSON.\"\"\"\n    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n    try:\n        t = int(parts[\"t\"])\n    except (KeyError, ValueError):\n        return False\n    if abs(time.time() - t) > tolerance:\n        return False\n    expected = hmac.new(secret.encode(), str(t).encode() + b\".\" + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n\n\n@app.post(\"/hooks/wanas\")\ndef wanas_webhook():\n    if not verify_wanas_signature(os.environ[\"WANAS_WEBHOOK_SECRET\"], request.get_data(), request.headers.get(\"Wanas-Signature\", \"\")):\n        return \"\", 400\n    event = request.get_json()  # event[\"id\"] is the same on every retry: skip ids you have already processed\n    return \"\", 200"}]}},"payrun.locked":{"post":{"operationId":"event_payrun_locked","summary":"payrun.locked","security":[],"description":"A pay run was locked (final; payslips exist). The body carries ids and statuses only; fetch the details with the REST API. Verify `Wanas-Signature` before you trust the body (snippets in the introduction, \"Verify the signature\").","parameters":[{"name":"Wanas-Signature","in":"header","required":true,"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>`","schema":{"type":"string"},"example":"t=1792915201,v1=5d41402abc4b2a76b9719d911017c592ae2f1e0c4c7f1b2f5b2e8c3d9a1f6e70"},{"name":"Wanas-Event","in":"header","required":true,"description":"The event type.","schema":{"type":"string"},"example":"payrun.locked"},{"name":"Wanas-Event-Id","in":"header","required":true,"description":"The event id; the same on every retry.","schema":{"type":"string"},"example":"evt_9c2f41d07a1b5e83"},{"name":"Wanas-Delivery-Id","in":"header","required":true,"description":"This delivery (one per endpoint and event).","schema":{"type":"string"},"example":"dlv_0b7e"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/EventEnvelope"},{"type":"object","properties":{"type":{"const":"payrun.locked"},"data":{"$ref":"#/components/schemas/Event_payrun_locked"}}}]},"example":{"id":"evt_9c2f41d07a1b5e83","type":"payrun.locked","api_version":"2026-10-12","created_at":"2026-10-26T08:00:01.000Z","org_id":"org_7d2c41ab","data":{"run_id":"run_8f3a","period_month":"2026-10","payroll_type":"main","status":"locked","revision":1}}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx status within 10 seconds. Anything else is retried (9 attempts over about 22.7 hours)."}},"x-codeSamples":[{"lang":"JavaScript","label":"Verify (Node.js)","source":"import crypto from 'node:crypto';\nimport express from 'express';\n\n// rawBody: the request body exactly as received (a Buffer or string), never re-serialised JSON.\nexport function verifyWanasSignature(secret, rawBody, header, toleranceSec = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n  const expected = crypto.createHmac('sha256', secret).update(t + '.').update(rawBody).digest();\n  const given = Buffer.from(parts.v1 ?? '', 'hex');\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n\nconst app = express();\napp.post('/hooks/wanas', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!verifyWanasSignature(process.env.WANAS_WEBHOOK_SECRET, req.body, req.get('Wanas-Signature') ?? '')) return res.sendStatus(400);\n  const event = JSON.parse(req.body.toString('utf8'));\n  // event.id is the same on every retry: skip ids you have already processed.\n  res.sendStatus(200);\n});"},{"lang":"Python","label":"Verify (Python)","source":"import hashlib\nimport hmac\nimport os\nimport time\n\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n\ndef verify_wanas_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:\n    \"\"\"raw_body: the request body exactly as received (bytes), never re-serialised JSON.\"\"\"\n    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n    try:\n        t = int(parts[\"t\"])\n    except (KeyError, ValueError):\n        return False\n    if abs(time.time() - t) > tolerance:\n        return False\n    expected = hmac.new(secret.encode(), str(t).encode() + b\".\" + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n\n\n@app.post(\"/hooks/wanas\")\ndef wanas_webhook():\n    if not verify_wanas_signature(os.environ[\"WANAS_WEBHOOK_SECRET\"], request.get_data(), request.headers.get(\"Wanas-Signature\", \"\")):\n        return \"\", 400\n    event = request.get_json()  # event[\"id\"] is the same on every retry: skip ids you have already processed\n    return \"\", 200"}]}},"payrun.paid":{"post":{"operationId":"event_payrun_paid","summary":"payrun.paid","security":[],"description":"A pay run was marked as paid. The body carries ids and statuses only; fetch the details with the REST API. Verify `Wanas-Signature` before you trust the body (snippets in the introduction, \"Verify the signature\").","parameters":[{"name":"Wanas-Signature","in":"header","required":true,"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>`","schema":{"type":"string"},"example":"t=1792915201,v1=5d41402abc4b2a76b9719d911017c592ae2f1e0c4c7f1b2f5b2e8c3d9a1f6e70"},{"name":"Wanas-Event","in":"header","required":true,"description":"The event type.","schema":{"type":"string"},"example":"payrun.paid"},{"name":"Wanas-Event-Id","in":"header","required":true,"description":"The event id; the same on every retry.","schema":{"type":"string"},"example":"evt_9c2f41d07a1b5e83"},{"name":"Wanas-Delivery-Id","in":"header","required":true,"description":"This delivery (one per endpoint and event).","schema":{"type":"string"},"example":"dlv_0b7e"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/EventEnvelope"},{"type":"object","properties":{"type":{"const":"payrun.paid"},"data":{"$ref":"#/components/schemas/Event_payrun_paid"}}}]},"example":{"id":"evt_9c2f41d07a1b5e83","type":"payrun.paid","api_version":"2026-10-12","created_at":"2026-10-26T08:00:01.000Z","org_id":"org_7d2c41ab","data":{"run_id":"run_8f3a","period_month":"2026-10","payroll_type":"main","status":"paid","revision":1}}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx status within 10 seconds. Anything else is retried (9 attempts over about 22.7 hours)."}},"x-codeSamples":[{"lang":"JavaScript","label":"Verify (Node.js)","source":"import crypto from 'node:crypto';\nimport express from 'express';\n\n// rawBody: the request body exactly as received (a Buffer or string), never re-serialised JSON.\nexport function verifyWanasSignature(secret, rawBody, header, toleranceSec = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n  const expected = crypto.createHmac('sha256', secret).update(t + '.').update(rawBody).digest();\n  const given = Buffer.from(parts.v1 ?? '', 'hex');\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n\nconst app = express();\napp.post('/hooks/wanas', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!verifyWanasSignature(process.env.WANAS_WEBHOOK_SECRET, req.body, req.get('Wanas-Signature') ?? '')) return res.sendStatus(400);\n  const event = JSON.parse(req.body.toString('utf8'));\n  // event.id is the same on every retry: skip ids you have already processed.\n  res.sendStatus(200);\n});"},{"lang":"Python","label":"Verify (Python)","source":"import hashlib\nimport hmac\nimport os\nimport time\n\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n\ndef verify_wanas_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:\n    \"\"\"raw_body: the request body exactly as received (bytes), never re-serialised JSON.\"\"\"\n    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n    try:\n        t = int(parts[\"t\"])\n    except (KeyError, ValueError):\n        return False\n    if abs(time.time() - t) > tolerance:\n        return False\n    expected = hmac.new(secret.encode(), str(t).encode() + b\".\" + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n\n\n@app.post(\"/hooks/wanas\")\ndef wanas_webhook():\n    if not verify_wanas_signature(os.environ[\"WANAS_WEBHOOK_SECRET\"], request.get_data(), request.headers.get(\"Wanas-Signature\", \"\")):\n        return \"\", 400\n    event = request.get_json()  # event[\"id\"] is the same on every retry: skip ids you have already processed\n    return \"\", 200"}]}},"payslip.released":{"post":{"operationId":"event_payslip_released","summary":"payslip.released","security":[],"description":"The payslips of a run became visible to employees (once per revision of the run). The body carries ids and statuses only; fetch the details with the REST API. Verify `Wanas-Signature` before you trust the body (snippets in the introduction, \"Verify the signature\").","parameters":[{"name":"Wanas-Signature","in":"header","required":true,"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>`","schema":{"type":"string"},"example":"t=1792915201,v1=5d41402abc4b2a76b9719d911017c592ae2f1e0c4c7f1b2f5b2e8c3d9a1f6e70"},{"name":"Wanas-Event","in":"header","required":true,"description":"The event type.","schema":{"type":"string"},"example":"payslip.released"},{"name":"Wanas-Event-Id","in":"header","required":true,"description":"The event id; the same on every retry.","schema":{"type":"string"},"example":"evt_9c2f41d07a1b5e83"},{"name":"Wanas-Delivery-Id","in":"header","required":true,"description":"This delivery (one per endpoint and event).","schema":{"type":"string"},"example":"dlv_0b7e"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/EventEnvelope"},{"type":"object","properties":{"type":{"const":"payslip.released"},"data":{"$ref":"#/components/schemas/Event_payslip_released"}}}]},"example":{"id":"evt_9c2f41d07a1b5e83","type":"payslip.released","api_version":"2026-10-12","created_at":"2026-10-26T08:00:01.000Z","org_id":"org_7d2c41ab","data":{"run_id":"run_8f3a","period_month":"2026-10","payroll_type":"main","revision":1,"released_at":"2026-10-28T00:00:00.000Z"}}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx status within 10 seconds. Anything else is retried (9 attempts over about 22.7 hours)."}},"x-codeSamples":[{"lang":"JavaScript","label":"Verify (Node.js)","source":"import crypto from 'node:crypto';\nimport express from 'express';\n\n// rawBody: the request body exactly as received (a Buffer or string), never re-serialised JSON.\nexport function verifyWanasSignature(secret, rawBody, header, toleranceSec = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n  const expected = crypto.createHmac('sha256', secret).update(t + '.').update(rawBody).digest();\n  const given = Buffer.from(parts.v1 ?? '', 'hex');\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n\nconst app = express();\napp.post('/hooks/wanas', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!verifyWanasSignature(process.env.WANAS_WEBHOOK_SECRET, req.body, req.get('Wanas-Signature') ?? '')) return res.sendStatus(400);\n  const event = JSON.parse(req.body.toString('utf8'));\n  // event.id is the same on every retry: skip ids you have already processed.\n  res.sendStatus(200);\n});"},{"lang":"Python","label":"Verify (Python)","source":"import hashlib\nimport hmac\nimport os\nimport time\n\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n\ndef verify_wanas_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:\n    \"\"\"raw_body: the request body exactly as received (bytes), never re-serialised JSON.\"\"\"\n    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n    try:\n        t = int(parts[\"t\"])\n    except (KeyError, ValueError):\n        return False\n    if abs(time.time() - t) > tolerance:\n        return False\n    expected = hmac.new(secret.encode(), str(t).encode() + b\".\" + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n\n\n@app.post(\"/hooks/wanas\")\ndef wanas_webhook():\n    if not verify_wanas_signature(os.environ[\"WANAS_WEBHOOK_SECRET\"], request.get_data(), request.headers.get(\"Wanas-Signature\", \"\")):\n        return \"\", 400\n    event = request.get_json()  # event[\"id\"] is the same on every retry: skip ids you have already processed\n    return \"\", 200"}]}},"employee.synced":{"post":{"operationId":"event_employee_synced","summary":"employee.synced","security":[],"description":"The employee list was synchronised from Zoho People. The body carries ids and statuses only; fetch the details with the REST API. Verify `Wanas-Signature` before you trust the body (snippets in the introduction, \"Verify the signature\").","parameters":[{"name":"Wanas-Signature","in":"header","required":true,"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>`","schema":{"type":"string"},"example":"t=1792915201,v1=5d41402abc4b2a76b9719d911017c592ae2f1e0c4c7f1b2f5b2e8c3d9a1f6e70"},{"name":"Wanas-Event","in":"header","required":true,"description":"The event type.","schema":{"type":"string"},"example":"employee.synced"},{"name":"Wanas-Event-Id","in":"header","required":true,"description":"The event id; the same on every retry.","schema":{"type":"string"},"example":"evt_9c2f41d07a1b5e83"},{"name":"Wanas-Delivery-Id","in":"header","required":true,"description":"This delivery (one per endpoint and event).","schema":{"type":"string"},"example":"dlv_0b7e"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/EventEnvelope"},{"type":"object","properties":{"type":{"const":"employee.synced"},"data":{"$ref":"#/components/schemas/Event_employee_synced"}}}]},"example":{"id":"evt_9c2f41d07a1b5e83","type":"employee.synced","api_version":"2026-10-12","created_at":"2026-10-26T08:00:01.000Z","org_id":"org_7d2c41ab","data":{"added":2,"updated":5,"exited":1,"total":120,"mode":"delta"}}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx status within 10 seconds. Anything else is retried (9 attempts over about 22.7 hours)."}},"x-codeSamples":[{"lang":"JavaScript","label":"Verify (Node.js)","source":"import crypto from 'node:crypto';\nimport express from 'express';\n\n// rawBody: the request body exactly as received (a Buffer or string), never re-serialised JSON.\nexport function verifyWanasSignature(secret, rawBody, header, toleranceSec = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n  const expected = crypto.createHmac('sha256', secret).update(t + '.').update(rawBody).digest();\n  const given = Buffer.from(parts.v1 ?? '', 'hex');\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n\nconst app = express();\napp.post('/hooks/wanas', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!verifyWanasSignature(process.env.WANAS_WEBHOOK_SECRET, req.body, req.get('Wanas-Signature') ?? '')) return res.sendStatus(400);\n  const event = JSON.parse(req.body.toString('utf8'));\n  // event.id is the same on every retry: skip ids you have already processed.\n  res.sendStatus(200);\n});"},{"lang":"Python","label":"Verify (Python)","source":"import hashlib\nimport hmac\nimport os\nimport time\n\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n\ndef verify_wanas_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:\n    \"\"\"raw_body: the request body exactly as received (bytes), never re-serialised JSON.\"\"\"\n    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n    try:\n        t = int(parts[\"t\"])\n    except (KeyError, ValueError):\n        return False\n    if abs(time.time() - t) > tolerance:\n        return False\n    expected = hmac.new(secret.encode(), str(t).encode() + b\".\" + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n\n\n@app.post(\"/hooks/wanas\")\ndef wanas_webhook():\n    if not verify_wanas_signature(os.environ[\"WANAS_WEBHOOK_SECRET\"], request.get_data(), request.headers.get(\"Wanas-Signature\", \"\")):\n        return \"\", 400\n    event = request.get_json()  # event[\"id\"] is the same on every retry: skip ids you have already processed\n    return \"\", 200"}]}},"ot.decided":{"post":{"operationId":"event_ot_decided","summary":"ot.decided","security":[],"description":"Overtime was approved or rejected. The body carries ids and statuses only; fetch the details with the REST API. Verify `Wanas-Signature` before you trust the body (snippets in the introduction, \"Verify the signature\").","parameters":[{"name":"Wanas-Signature","in":"header","required":true,"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>`","schema":{"type":"string"},"example":"t=1792915201,v1=5d41402abc4b2a76b9719d911017c592ae2f1e0c4c7f1b2f5b2e8c3d9a1f6e70"},{"name":"Wanas-Event","in":"header","required":true,"description":"The event type.","schema":{"type":"string"},"example":"ot.decided"},{"name":"Wanas-Event-Id","in":"header","required":true,"description":"The event id; the same on every retry.","schema":{"type":"string"},"example":"evt_9c2f41d07a1b5e83"},{"name":"Wanas-Delivery-Id","in":"header","required":true,"description":"This delivery (one per endpoint and event).","schema":{"type":"string"},"example":"dlv_0b7e"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/EventEnvelope"},{"type":"object","properties":{"type":{"const":"ot.decided"},"data":{"$ref":"#/components/schemas/Event_ot_decided"}}}]},"example":{"id":"evt_9c2f41d07a1b5e83","type":"ot.decided","api_version":"2026-10-12","created_at":"2026-10-26T08:00:01.000Z","org_id":"org_7d2c41ab","data":{"decision":"approved","items":[{"employee_id":"emp_41c2","date":"2026-10-03","status":"approved"}]}}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx status within 10 seconds. Anything else is retried (9 attempts over about 22.7 hours)."}},"x-codeSamples":[{"lang":"JavaScript","label":"Verify (Node.js)","source":"import crypto from 'node:crypto';\nimport express from 'express';\n\n// rawBody: the request body exactly as received (a Buffer or string), never re-serialised JSON.\nexport function verifyWanasSignature(secret, rawBody, header, toleranceSec = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n  const expected = crypto.createHmac('sha256', secret).update(t + '.').update(rawBody).digest();\n  const given = Buffer.from(parts.v1 ?? '', 'hex');\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n\nconst app = express();\napp.post('/hooks/wanas', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!verifyWanasSignature(process.env.WANAS_WEBHOOK_SECRET, req.body, req.get('Wanas-Signature') ?? '')) return res.sendStatus(400);\n  const event = JSON.parse(req.body.toString('utf8'));\n  // event.id is the same on every retry: skip ids you have already processed.\n  res.sendStatus(200);\n});"},{"lang":"Python","label":"Verify (Python)","source":"import hashlib\nimport hmac\nimport os\nimport time\n\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n\ndef verify_wanas_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:\n    \"\"\"raw_body: the request body exactly as received (bytes), never re-serialised JSON.\"\"\"\n    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n    try:\n        t = int(parts[\"t\"])\n    except (KeyError, ValueError):\n        return False\n    if abs(time.time() - t) > tolerance:\n        return False\n    expected = hmac.new(secret.encode(), str(t).encode() + b\".\" + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n\n\n@app.post(\"/hooks/wanas\")\ndef wanas_webhook():\n    if not verify_wanas_signature(os.environ[\"WANAS_WEBHOOK_SECRET\"], request.get_data(), request.headers.get(\"Wanas-Signature\", \"\")):\n        return \"\", 400\n    event = request.get_json()  # event[\"id\"] is the same on every retry: skip ids you have already processed\n    return \"\", 200"}]}},"si_profile.created":{"post":{"operationId":"event_si_profile_created","summary":"si_profile.created","security":[],"description":"A social insurance profile was created (with its first rate). The body carries ids and statuses only; fetch the details with the REST API. Verify `Wanas-Signature` before you trust the body (snippets in the introduction, \"Verify the signature\").","parameters":[{"name":"Wanas-Signature","in":"header","required":true,"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>`","schema":{"type":"string"},"example":"t=1792915201,v1=5d41402abc4b2a76b9719d911017c592ae2f1e0c4c7f1b2f5b2e8c3d9a1f6e70"},{"name":"Wanas-Event","in":"header","required":true,"description":"The event type.","schema":{"type":"string"},"example":"si_profile.created"},{"name":"Wanas-Event-Id","in":"header","required":true,"description":"The event id; the same on every retry.","schema":{"type":"string"},"example":"evt_9c2f41d07a1b5e83"},{"name":"Wanas-Delivery-Id","in":"header","required":true,"description":"This delivery (one per endpoint and event).","schema":{"type":"string"},"example":"dlv_0b7e"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/EventEnvelope"},{"type":"object","properties":{"type":{"const":"si_profile.created"},"data":{"$ref":"#/components/schemas/Event_si_profile_created"}}}]},"example":{"id":"evt_9c2f41d07a1b5e83","type":"si_profile.created","api_version":"2026-10-12","created_at":"2026-10-26T08:00:01.000Z","org_id":"org_7d2c41ab","data":{"profile_id":"sip_1a2b","effective_from":"2026-11-01"}}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx status within 10 seconds. Anything else is retried (9 attempts over about 22.7 hours)."}},"x-codeSamples":[{"lang":"JavaScript","label":"Verify (Node.js)","source":"import crypto from 'node:crypto';\nimport express from 'express';\n\n// rawBody: the request body exactly as received (a Buffer or string), never re-serialised JSON.\nexport function verifyWanasSignature(secret, rawBody, header, toleranceSec = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n  const expected = crypto.createHmac('sha256', secret).update(t + '.').update(rawBody).digest();\n  const given = Buffer.from(parts.v1 ?? '', 'hex');\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n\nconst app = express();\napp.post('/hooks/wanas', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!verifyWanasSignature(process.env.WANAS_WEBHOOK_SECRET, req.body, req.get('Wanas-Signature') ?? '')) return res.sendStatus(400);\n  const event = JSON.parse(req.body.toString('utf8'));\n  // event.id is the same on every retry: skip ids you have already processed.\n  res.sendStatus(200);\n});"},{"lang":"Python","label":"Verify (Python)","source":"import hashlib\nimport hmac\nimport os\nimport time\n\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n\ndef verify_wanas_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:\n    \"\"\"raw_body: the request body exactly as received (bytes), never re-serialised JSON.\"\"\"\n    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n    try:\n        t = int(parts[\"t\"])\n    except (KeyError, ValueError):\n        return False\n    if abs(time.time() - t) > tolerance:\n        return False\n    expected = hmac.new(secret.encode(), str(t).encode() + b\".\" + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n\n\n@app.post(\"/hooks/wanas\")\ndef wanas_webhook():\n    if not verify_wanas_signature(os.environ[\"WANAS_WEBHOOK_SECRET\"], request.get_data(), request.headers.get(\"Wanas-Signature\", \"\")):\n        return \"\", 400\n    event = request.get_json()  # event[\"id\"] is the same on every retry: skip ids you have already processed\n    return \"\", 200"}]}},"si_profile.rates_changed":{"post":{"operationId":"event_si_profile_rates_changed","summary":"si_profile.rates_changed","security":[],"description":"A social insurance profile's limits or rates change from a date (a rate change was added or cancelled). Open pay runs from that date are recalculated. The body carries ids and statuses only; fetch the details with the REST API. Verify `Wanas-Signature` before you trust the body (snippets in the introduction, \"Verify the signature\").","parameters":[{"name":"Wanas-Signature","in":"header","required":true,"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>`","schema":{"type":"string"},"example":"t=1792915201,v1=5d41402abc4b2a76b9719d911017c592ae2f1e0c4c7f1b2f5b2e8c3d9a1f6e70"},{"name":"Wanas-Event","in":"header","required":true,"description":"The event type.","schema":{"type":"string"},"example":"si_profile.rates_changed"},{"name":"Wanas-Event-Id","in":"header","required":true,"description":"The event id; the same on every retry.","schema":{"type":"string"},"example":"evt_9c2f41d07a1b5e83"},{"name":"Wanas-Delivery-Id","in":"header","required":true,"description":"This delivery (one per endpoint and event).","schema":{"type":"string"},"example":"dlv_0b7e"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/EventEnvelope"},{"type":"object","properties":{"type":{"const":"si_profile.rates_changed"},"data":{"$ref":"#/components/schemas/Event_si_profile_rates_changed"}}}]},"example":{"id":"evt_9c2f41d07a1b5e83","type":"si_profile.rates_changed","api_version":"2026-10-12","created_at":"2026-10-26T08:00:01.000Z","org_id":"org_7d2c41ab","data":{"profile_id":"sip_1a2b","change":"added","rate_id":"rate_9c8d","effective_from":"2027-01-01"}}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx status within 10 seconds. Anything else is retried (9 attempts over about 22.7 hours)."}},"x-codeSamples":[{"lang":"JavaScript","label":"Verify (Node.js)","source":"import crypto from 'node:crypto';\nimport express from 'express';\n\n// rawBody: the request body exactly as received (a Buffer or string), never re-serialised JSON.\nexport function verifyWanasSignature(secret, rawBody, header, toleranceSec = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n  const expected = crypto.createHmac('sha256', secret).update(t + '.').update(rawBody).digest();\n  const given = Buffer.from(parts.v1 ?? '', 'hex');\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n\nconst app = express();\napp.post('/hooks/wanas', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!verifyWanasSignature(process.env.WANAS_WEBHOOK_SECRET, req.body, req.get('Wanas-Signature') ?? '')) return res.sendStatus(400);\n  const event = JSON.parse(req.body.toString('utf8'));\n  // event.id is the same on every retry: skip ids you have already processed.\n  res.sendStatus(200);\n});"},{"lang":"Python","label":"Verify (Python)","source":"import hashlib\nimport hmac\nimport os\nimport time\n\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n\ndef verify_wanas_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:\n    \"\"\"raw_body: the request body exactly as received (bytes), never re-serialised JSON.\"\"\"\n    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n    try:\n        t = int(parts[\"t\"])\n    except (KeyError, ValueError):\n        return False\n    if abs(time.time() - t) > tolerance:\n        return False\n    expected = hmac.new(secret.encode(), str(t).encode() + b\".\" + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n\n\n@app.post(\"/hooks/wanas\")\ndef wanas_webhook():\n    if not verify_wanas_signature(os.environ[\"WANAS_WEBHOOK_SECRET\"], request.get_data(), request.headers.get(\"Wanas-Signature\", \"\")):\n        return \"\", 400\n    event = request.get_json()  # event[\"id\"] is the same on every retry: skip ids you have already processed\n    return \"\", 200"}]}},"si_profile.default_changed":{"post":{"operationId":"event_si_profile_default_changed","summary":"si_profile.default_changed","security":[],"description":"The organisation default SI profile changes from a date (set or cancelled). profile_id null = no default. The body carries ids and statuses only; fetch the details with the REST API. Verify `Wanas-Signature` before you trust the body (snippets in the introduction, \"Verify the signature\").","parameters":[{"name":"Wanas-Signature","in":"header","required":true,"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>`","schema":{"type":"string"},"example":"t=1792915201,v1=5d41402abc4b2a76b9719d911017c592ae2f1e0c4c7f1b2f5b2e8c3d9a1f6e70"},{"name":"Wanas-Event","in":"header","required":true,"description":"The event type.","schema":{"type":"string"},"example":"si_profile.default_changed"},{"name":"Wanas-Event-Id","in":"header","required":true,"description":"The event id; the same on every retry.","schema":{"type":"string"},"example":"evt_9c2f41d07a1b5e83"},{"name":"Wanas-Delivery-Id","in":"header","required":true,"description":"This delivery (one per endpoint and event).","schema":{"type":"string"},"example":"dlv_0b7e"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/EventEnvelope"},{"type":"object","properties":{"type":{"const":"si_profile.default_changed"},"data":{"$ref":"#/components/schemas/Event_si_profile_default_changed"}}}]},"example":{"id":"evt_9c2f41d07a1b5e83","type":"si_profile.default_changed","api_version":"2026-10-12","created_at":"2026-10-26T08:00:01.000Z","org_id":"org_7d2c41ab","data":{"change":"set","profile_id":"sip_1a2b","effective_from":"2026-11-01"}}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx status within 10 seconds. Anything else is retried (9 attempts over about 22.7 hours)."}},"x-codeSamples":[{"lang":"JavaScript","label":"Verify (Node.js)","source":"import crypto from 'node:crypto';\nimport express from 'express';\n\n// rawBody: the request body exactly as received (a Buffer or string), never re-serialised JSON.\nexport function verifyWanasSignature(secret, rawBody, header, toleranceSec = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n  const expected = crypto.createHmac('sha256', secret).update(t + '.').update(rawBody).digest();\n  const given = Buffer.from(parts.v1 ?? '', 'hex');\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n\nconst app = express();\napp.post('/hooks/wanas', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!verifyWanasSignature(process.env.WANAS_WEBHOOK_SECRET, req.body, req.get('Wanas-Signature') ?? '')) return res.sendStatus(400);\n  const event = JSON.parse(req.body.toString('utf8'));\n  // event.id is the same on every retry: skip ids you have already processed.\n  res.sendStatus(200);\n});"},{"lang":"Python","label":"Verify (Python)","source":"import hashlib\nimport hmac\nimport os\nimport time\n\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n\ndef verify_wanas_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:\n    \"\"\"raw_body: the request body exactly as received (bytes), never re-serialised JSON.\"\"\"\n    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n    try:\n        t = int(parts[\"t\"])\n    except (KeyError, ValueError):\n        return False\n    if abs(time.time() - t) > tolerance:\n        return False\n    expected = hmac.new(secret.encode(), str(t).encode() + b\".\" + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n\n\n@app.post(\"/hooks/wanas\")\ndef wanas_webhook():\n    if not verify_wanas_signature(os.environ[\"WANAS_WEBHOOK_SECRET\"], request.get_data(), request.headers.get(\"Wanas-Signature\", \"\")):\n        return \"\", 400\n    event = request.get_json()  # event[\"id\"] is the same on every retry: skip ids you have already processed\n    return \"\", 200"}]}},"si_profile.rules_changed":{"post":{"operationId":"event_si_profile_rules_changed","summary":"si_profile.rules_changed","security":[],"description":"The SI profile assignment rules change from a date (a new list, or a scheduled one cancelled). The body carries ids and statuses only; fetch the details with the REST API. Verify `Wanas-Signature` before you trust the body (snippets in the introduction, \"Verify the signature\").","parameters":[{"name":"Wanas-Signature","in":"header","required":true,"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>`","schema":{"type":"string"},"example":"t=1792915201,v1=5d41402abc4b2a76b9719d911017c592ae2f1e0c4c7f1b2f5b2e8c3d9a1f6e70"},{"name":"Wanas-Event","in":"header","required":true,"description":"The event type.","schema":{"type":"string"},"example":"si_profile.rules_changed"},{"name":"Wanas-Event-Id","in":"header","required":true,"description":"The event id; the same on every retry.","schema":{"type":"string"},"example":"evt_9c2f41d07a1b5e83"},{"name":"Wanas-Delivery-Id","in":"header","required":true,"description":"This delivery (one per endpoint and event).","schema":{"type":"string"},"example":"dlv_0b7e"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/EventEnvelope"},{"type":"object","properties":{"type":{"const":"si_profile.rules_changed"},"data":{"$ref":"#/components/schemas/Event_si_profile_rules_changed"}}}]},"example":{"id":"evt_9c2f41d07a1b5e83","type":"si_profile.rules_changed","api_version":"2026-10-12","created_at":"2026-10-26T08:00:01.000Z","org_id":"org_7d2c41ab","data":{"change":"set","list_id":"lst_4e5f","effective_from":"2026-11-01","rules":3}}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx status within 10 seconds. Anything else is retried (9 attempts over about 22.7 hours)."}},"x-codeSamples":[{"lang":"JavaScript","label":"Verify (Node.js)","source":"import crypto from 'node:crypto';\nimport express from 'express';\n\n// rawBody: the request body exactly as received (a Buffer or string), never re-serialised JSON.\nexport function verifyWanasSignature(secret, rawBody, header, toleranceSec = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n  const expected = crypto.createHmac('sha256', secret).update(t + '.').update(rawBody).digest();\n  const given = Buffer.from(parts.v1 ?? '', 'hex');\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n\nconst app = express();\napp.post('/hooks/wanas', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!verifyWanasSignature(process.env.WANAS_WEBHOOK_SECRET, req.body, req.get('Wanas-Signature') ?? '')) return res.sendStatus(400);\n  const event = JSON.parse(req.body.toString('utf8'));\n  // event.id is the same on every retry: skip ids you have already processed.\n  res.sendStatus(200);\n});"},{"lang":"Python","label":"Verify (Python)","source":"import hashlib\nimport hmac\nimport os\nimport time\n\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n\ndef verify_wanas_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:\n    \"\"\"raw_body: the request body exactly as received (bytes), never re-serialised JSON.\"\"\"\n    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n    try:\n        t = int(parts[\"t\"])\n    except (KeyError, ValueError):\n        return False\n    if abs(time.time() - t) > tolerance:\n        return False\n    expected = hmac.new(secret.encode(), str(t).encode() + b\".\" + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n\n\n@app.post(\"/hooks/wanas\")\ndef wanas_webhook():\n    if not verify_wanas_signature(os.environ[\"WANAS_WEBHOOK_SECRET\"], request.get_data(), request.headers.get(\"Wanas-Signature\", \"\")):\n        return \"\", 400\n    event = request.get_json()  # event[\"id\"] is the same on every retry: skip ids you have already processed\n    return \"\", 200"}]}},"si_profile.assigned":{"post":{"operationId":"event_si_profile_assigned","summary":"si_profile.assigned","security":[],"description":"A profile was put on several employee salary packages at once (new salary versions from a date). The body carries ids and statuses only; fetch the details with the REST API. Verify `Wanas-Signature` before you trust the body (snippets in the introduction, \"Verify the signature\").","parameters":[{"name":"Wanas-Signature","in":"header","required":true,"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>`","schema":{"type":"string"},"example":"t=1792915201,v1=5d41402abc4b2a76b9719d911017c592ae2f1e0c4c7f1b2f5b2e8c3d9a1f6e70"},{"name":"Wanas-Event","in":"header","required":true,"description":"The event type.","schema":{"type":"string"},"example":"si_profile.assigned"},{"name":"Wanas-Event-Id","in":"header","required":true,"description":"The event id; the same on every retry.","schema":{"type":"string"},"example":"evt_9c2f41d07a1b5e83"},{"name":"Wanas-Delivery-Id","in":"header","required":true,"description":"This delivery (one per endpoint and event).","schema":{"type":"string"},"example":"dlv_0b7e"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/EventEnvelope"},{"type":"object","properties":{"type":{"const":"si_profile.assigned"},"data":{"$ref":"#/components/schemas/Event_si_profile_assigned"}}}]},"example":{"id":"evt_9c2f41d07a1b5e83","type":"si_profile.assigned","api_version":"2026-10-12","created_at":"2026-10-26T08:00:01.000Z","org_id":"org_7d2c41ab","data":{"profile_id":"sip_1a2b","effective_from":"2026-11-01","employees":12}}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx status within 10 seconds. Anything else is retried (9 attempts over about 22.7 hours)."}},"x-codeSamples":[{"lang":"JavaScript","label":"Verify (Node.js)","source":"import crypto from 'node:crypto';\nimport express from 'express';\n\n// rawBody: the request body exactly as received (a Buffer or string), never re-serialised JSON.\nexport function verifyWanasSignature(secret, rawBody, header, toleranceSec = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;\n  const expected = crypto.createHmac('sha256', secret).update(t + '.').update(rawBody).digest();\n  const given = Buffer.from(parts.v1 ?? '', 'hex');\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n\nconst app = express();\napp.post('/hooks/wanas', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!verifyWanasSignature(process.env.WANAS_WEBHOOK_SECRET, req.body, req.get('Wanas-Signature') ?? '')) return res.sendStatus(400);\n  const event = JSON.parse(req.body.toString('utf8'));\n  // event.id is the same on every retry: skip ids you have already processed.\n  res.sendStatus(200);\n});"},{"lang":"Python","label":"Verify (Python)","source":"import hashlib\nimport hmac\nimport os\nimport time\n\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n\ndef verify_wanas_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:\n    \"\"\"raw_body: the request body exactly as received (bytes), never re-serialised JSON.\"\"\"\n    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n    try:\n        t = int(parts[\"t\"])\n    except (KeyError, ValueError):\n        return False\n    if abs(time.time() - t) > tolerance:\n        return False\n    expected = hmac.new(secret.encode(), str(t).encode() + b\".\" + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n\n\n@app.post(\"/hooks/wanas\")\ndef wanas_webhook():\n    if not verify_wanas_signature(os.environ[\"WANAS_WEBHOOK_SECRET\"], request.get_data(), request.headers.get(\"Wanas-Signature\", \"\")):\n        return \"\", 400\n    event = request.get_json()  # event[\"id\"] is the same on every retry: skip ids you have already processed\n    return \"\", 200"}]}}},"components":{"securitySchemes":{"ApiKey":{"type":"http","scheme":"bearer","bearerFormat":"wpk_live_<key id>_<secret>","description":"An API key created by an organization admin in Settings → Integrations → API and webhooks, sent as `Authorization: Bearer wpk_live_…`. Scopes: `employees:read`, `payruns:read`, `payslips:read`, `reports:read`, `webhooks:manage`, `si_profiles:read`, `si_profiles:write`, `commissions:read`, `commissions:write`."}},"headers":{"RequestId":{"description":"Id of this request (req_…). Quote it to support.","schema":{"type":"string"},"example":"req_4f1c9b2a7d3e8f60a1b2"},"RateLimitLimit":{"description":"Requests a minute this key may make.","schema":{"type":"integer"},"example":120},"RateLimitRemaining":{"description":"Requests left in the bucket now.","schema":{"type":"integer"},"example":117},"RetryAfter":{"description":"Seconds to wait before trying again.","schema":{"type":"integer"},"example":12}},"responses":{"E400":{"description":"Invalid query parameter (`invalid_limit`, `invalid_cursor`, `invalid_status`, `invalid_period`, `invalid_date`, `invalid_format`, `invalid_lang`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_limit","message":"limit must be an integer from 1 to 200","request_id":"req_4f1c9b2a7d3e8f60a1b2"}}}}},"E401":{"description":"Missing, malformed, revoked or expired key (`invalid_api_key`, `api_key_revoked`, `api_key_expired`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"WWW-Authenticate":{"description":"Bearer realm=\"wanas-payroll\"","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_api_key","message":"Missing or malformed API key. Send \"Authorization: Bearer wpk_live_...\"","request_id":"req_4f1c9b2a7d3e8f60a1b2"}}}}},"E402":{"description":"Subscription required: the organization is read-only (`subscription_required`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"subscription_required","message":"An active subscription is required","request_id":"req_4f1c9b2a7d3e8f60a1b2"}}}}},"E403":{"description":"`insufficient_scope` (details.required_scope), `org_suspended` or `feature_disabled`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"insufficient_scope","message":"This API key lacks the \"payruns:read\" scope","details":{"required_scope":"payruns:read"},"request_id":"req_4f1c9b2a7d3e8f60a1b2"}}}}},"E404":{"description":"Not found (also for another organization's ids).","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"Pay run not found","request_id":"req_4f1c9b2a7d3e8f60a1b2"}}}}},"E409":{"description":"Not in the right state: `run_not_final`, `payslips_not_released`, `no_open_payrun`, `run_locked`, `run_approved`, `endpoint_limit`, `si_date_locked`, `si_rate_date_taken`, `si_default_unchanged`, `si_rules_unchanged`, `si_rate_first`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"run_not_final","message":"Results are available once the pay run is approved, locked or paid","details":{"status":"calculated","available_from":"approved"},"request_id":"req_4f1c9b2a7d3e8f60a1b2"}}}}},"E422":{"description":"Invalid body: `invalid_input` (details: field errors), `invalid_url`, `invalid_events`, `employee_not_found`, `invalid_si_profile`, `si_profile_merge_invalid`, `bulk_failed`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_input","message":"Invalid input","details":{"formErrors":[],"fieldErrors":{"url":["Required"]}},"request_id":"req_4f1c9b2a7d3e8f60a1b2"}}}}},"E429":{"description":"`rate_limited`: wait `Retry-After` seconds.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Too many requests; slow down","details":{"retry_after":12},"request_id":"req_4f1c9b2a7d3e8f60a1b2"}}}}},"E500":{"description":"`internal`: retry later with backoff.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"internal","message":"Internal error","request_id":"req_4f1c9b2a7d3e8f60a1b2"}}}}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"type":"string","description":"Stable machine-readable code: branch on this."},"message":{"type":"string","description":"Human-readable; may change."},"details":{"description":"Extra data for some codes (field errors, required_scope, retry_after ...)."},"request_id":{"type":"string","description":"Quote it to support."}}}}},"Me":{"type":"object","required":["organization","key","currency","amount_unit"],"properties":{"organization":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"key":{"type":"object","required":["id","name","scopes"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string","enum":["employees:read","payruns:read","payslips:read","reports:read","webhooks:manage","si_profiles:read","si_profiles:write","commissions:read","commissions:write"]}}}},"currency":{"type":"string","description":"Always EGP.","const":"EGP"},"amount_unit":{"type":"string","description":"Money fields are integer piastres.","const":"piastre"}}},"Employee":{"type":"object","required":["id","status"],"properties":{"id":{"type":"string","description":"Wanas Payroll employee id."},"code":{"type":["string","null"],"description":"Employee number (Zoho People)."},"first_name":{"type":["string","null"]},"last_name":{"type":["string","null"]},"name":{"type":["string","null"],"description":"First and last name."},"email":{"type":["string","null"],"description":"Work e-mail.","format":"email"},"department":{"type":["string","null"]},"designation":{"type":["string","null"]},"status":{"type":"string","enum":["active","exited"]},"hire_date":{"type":["string","null"],"description":"Date of joining.","format":"date"},"exit_date":{"type":["string","null"],"description":"Date of exit; null while employed.","format":"date"},"synced_at":{"type":"string","description":"Last synchronised from Zoho People.","format":"date-time"}}},"Totals":{"type":"object","properties":{"headcount":{"type":"integer","description":"Employees in the run."},"errored_employees":{"type":"integer","description":"Employees whose calculation failed."},"warnings":{"type":"integer"},"gross":{"type":"integer","description":"Gross pay. Integer piastres (1/100 EGP)."},"si_employee":{"type":"integer","description":"Social insurance, employee share. Integer piastres (1/100 EGP)."},"si_employer":{"type":"integer","description":"Social insurance, employer share. Integer piastres (1/100 EGP)."},"uhi_employee":{"type":"integer","description":"Universal Health Insurance, employee share. Integer piastres (1/100 EGP)."},"tax":{"type":"integer","description":"Income tax. Integer piastres (1/100 EGP)."},"martyrs_fund":{"type":"integer","description":"Martyrs' fund. Integer piastres (1/100 EGP)."},"deductions":{"type":"integer","description":"Other deductions. Integer piastres (1/100 EGP)."},"net":{"type":"integer","description":"Net pay. Integer piastres (1/100 EGP)."},"employer_cost":{"type":"integer","description":"Total employer cost. Integer piastres (1/100 EGP)."},"unrecovered":{"type":"integer","description":"Deductions that could not be taken in full. Integer piastres (1/100 EGP)."}}},"PayRun":{"type":"object","required":["id","period_month","status","currency","totals"],"properties":{"id":{"type":"string"},"label":{"type":"string","description":"Name of the run."},"period_month":{"type":"string","description":"YYYY-MM"},"period_start":{"type":"string","description":"Date, YYYY-MM-DD.","format":"date"},"period_end":{"type":"string","description":"Date, YYYY-MM-DD.","format":"date"},"pay_date":{"type":"string","description":"Date, YYYY-MM-DD.","format":"date"},"payroll_type":{"type":"string","enum":["main","additional"],"description":"`main`: the monthly run; `additional`: an extra run in the same month (bonus, correction)."},"status":{"type":"string","enum":["draft","calculated","approved","locked","paid"]},"revision":{"type":"integer","description":"Increases each time a locked run is reopened."},"currency":{"type":"string","description":"Always EGP.","const":"EGP"},"totals":{"$ref":"#/components/schemas/Totals"},"calculated_at":{"type":["string","null"],"description":"Timestamp, ISO 8601 UTC; null until it happens.","format":"date-time"},"approved_at":{"type":["string","null"],"description":"Timestamp, ISO 8601 UTC; null until it happens.","format":"date-time"},"locked_at":{"type":["string","null"],"description":"Timestamp, ISO 8601 UTC; null until it happens.","format":"date-time"},"paid_at":{"type":["string","null"],"description":"Timestamp, ISO 8601 UTC; null until it happens.","format":"date-time"},"payslips_released":{"type":"boolean","description":"Whether employees can see their payslips (and the API can serve them)."}}},"ResultLine":{"type":"object","properties":{"code":{"type":"string","description":"Component code."},"group":{"type":"string","enum":["earning","benefit","reimbursement","attendance","statutory","deduction","employer"]},"label_en":{"type":"string"},"label_ar":{"type":"string"},"amount":{"type":"integer","description":"Amount taken or paid. Integer piastres (1/100 EGP)."},"requested":{"type":"integer","description":"Amount asked for (deductions can be capped). Integer piastres (1/100 EGP)."},"taxable":{"type":"boolean"},"insurable":{"type":"boolean"},"eta_code":{"type":["string","null"],"description":"ETA code; only when the ETA feature is on for your organization."}}},"ResultRow":{"type":"object","properties":{"employee_id":{"type":"string"},"employee_code":{"type":["string","null"]},"name":{"type":"string"},"department":{"type":["string","null"]},"gross":{"type":"integer","description":"Gross. Integer piastres (1/100 EGP)."},"si_employee":{"type":"integer","description":"SI, employee share. Integer piastres (1/100 EGP)."},"si_employer":{"type":"integer","description":"SI, employer share. Integer piastres (1/100 EGP)."},"uhi_employee":{"type":"integer","description":"UHI, employee share. Integer piastres (1/100 EGP)."},"tax":{"type":"integer","description":"Income tax. Integer piastres (1/100 EGP)."},"martyrs_fund":{"type":"integer","description":"Martyrs' fund. Integer piastres (1/100 EGP)."},"deductions":{"type":"integer","description":"Other deductions. Integer piastres (1/100 EGP)."},"net":{"type":"integer","description":"Net. Integer piastres (1/100 EGP)."},"held":{"type":"boolean","description":"Salary held: not paid with this run."},"employer_cost":{"type":"integer","description":"Employer cost. Integer piastres (1/100 EGP)."},"tax_treatment":{"type":"string","description":"ETA tax treatment code (01 to 05)."},"errors":{"type":"integer"},"warnings":{"type":"integer"},"eta_days":{"type":"integer","description":"Only when the ETA feature is on."},"calculated_at":{"type":"string","description":"Timestamp, ISO 8601 UTC.","format":"date-time"},"lines":{"type":"array","items":{"$ref":"#/components/schemas/ResultLine"},"description":"Left out with ?lines=0."}}},"ResultsPage":{"type":"object","required":["run","currency","data","next_cursor","has_more"],"properties":{"run":{"type":"object","properties":{"id":{"type":"string"},"period_month":{"type":"string"},"status":{"type":"string","enum":["draft","calculated","approved","locked","paid"]},"revision":{"type":"integer"},"final":{"type":"boolean","description":"false while the run is only approved: figures can still change if it is reopened."}}},"currency":{"type":"string","description":"Always EGP.","const":"EGP"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ResultRow"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to get the next page; null on the last page."},"has_more":{"type":"boolean","description":"true when there is a next page."}}},"PayslipPage":{"type":"object","required":["currency","data","next_cursor","has_more"],"properties":{"currency":{"type":"string","description":"Always EGP.","const":"EGP"},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Use with /payslips/{id}/pdf."},"run_id":{"type":"string"},"employee_id":{"type":"string"},"employee_code":{"type":["string","null"]},"name":{"type":"string"},"net":{"type":"integer","description":"Net pay. Integer piastres (1/100 EGP)."},"pdf_path":{"type":"string","description":"Path of the PDF."}}}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to get the next page; null on the last page."},"has_more":{"type":"boolean","description":"true when there is a next page."}}},"ReportList":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","enum":["register","social-insurance","tax","year-end","loans","medical","department-cost","leavers","leave-pay","si-not-insured","si-cost","service-charge"]},"parameters":{"type":"array","items":{"type":"string"}},"formats":{"type":"array","items":{"type":"string","enum":["json","csv"]}}}}},"currency":{"type":"string","description":"Always EGP.","const":"EGP"}}},"ReportColumn":{"type":"object","properties":{"key":{"type":"string","description":"Key of the value in each row."},"en":{"type":"string","description":"English heading."},"ar":{"type":"string","description":"Arabic heading."},"type":{"type":"string","enum":["text","money","int","date"]}}},"ReportEnvelope":{"type":"object","required":["currency","report"],"properties":{"currency":{"type":"string","description":"Always EGP.","const":"EGP"},"report":{"type":"object","description":"Note: the report object keeps the app's own camelCase keys.","properties":{"id":{"type":"string"},"title":{"type":"object","properties":{"en":{"type":"string"},"ar":{"type":"string"}}},"note":{"type":["object","null"],"properties":{"en":{"type":"string"},"ar":{"type":"string"}}},"run":{"type":["object","null"],"properties":{"id":{"type":"string"},"periodMonth":{"type":"string"},"status":{"type":"string","enum":["draft","calculated","approved","locked","paid"]},"label":{"type":"string"},"payrollType":{"type":"string","enum":["main","additional"]},"revision":{"type":"integer"},"scope":{"type":"string"}}},"year":{"type":["string","null"]},"department":{"type":["string","null"]},"columns":{"type":"array","items":{"$ref":"#/components/schemas/ReportColumn"}},"rows":{"type":"array","items":{"type":"object","description":"Keyed by column key; money in piastres."}},"totals":{"type":["object","null"],"description":"Keyed by column key."},"breakdown":{"type":["object","null"],"description":"A second small table (title, columns, rows), e.g. the tax report per ETA code."},"range":{"type":"object","description":"si-cost: the months and the profile it is limited to.","properties":{"from":{"type":"string"},"to":{"type":"string"},"profile":{"type":["string","null"]},"profileName":{"type":["string","null"]}}}}}}},"SiRate":{"type":"object","properties":{"id":{"type":"string"},"effective_from":{"type":"string","description":"Date, YYYY-MM-DD.","format":"date"},"min_insurable":{"type":["integer","null"],"description":"Piastres a month; null = follows the statutory rules."},"max_insurable":{"type":["integer","null"],"description":"Piastres a month; null = follows the statutory rules."},"branches":{"type":"object","description":"Law 148/2019 branches. Omitted = all follow the statutory rules.","properties":{"pension":{"type":"object","properties":{"employee_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employee rate. A fraction (0.09 = 9%). null = follows the statutory rules."},"employer_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employer rate. A fraction (0.09 = 9%). null = follows the statutory rules."}}},"sickness":{"type":"object","properties":{"employee_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employee rate. A fraction (0.09 = 9%). null = follows the statutory rules."},"employer_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employer rate. A fraction (0.09 = 9%). null = follows the statutory rules."}}},"work_injury":{"type":"object","properties":{"employee_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employee rate. A fraction (0.09 = 9%). null = follows the statutory rules."},"employer_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employer rate. A fraction (0.09 = 9%). null = follows the statutory rules."}}},"unemployment":{"type":"object","properties":{"employee_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employee rate. A fraction (0.09 = 9%). null = follows the statutory rules."},"employer_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employer rate. A fraction (0.09 = 9%). null = follows the statutory rules."}}}}},"treasury_share":{"type":"number","minimum":0,"maximum":1,"description":"Share (0..1) of the employer contribution the Public Treasury pays."},"employer_pays_employee_share":{"type":"boolean"},"insurable_base":{"type":"string","enum":["components","basic_only","declared"],"description":"`components`: the insurable pay components; `basic_only`: basic salary only; `declared`: the declared insurable wage."},"note":{"type":"string"},"created_at":{"type":"string","description":"Timestamp, ISO 8601 UTC.","format":"date-time"}}},"SiRateInput":{"type":"object","required":["min_insurable","max_insurable"],"properties":{"effective_from":{"type":"string","description":"YYYY-MM-DD; default the first of the current month (or the first open date).","format":"date"},"min_insurable":{"type":["integer","null"],"description":"Piastres a month; null = statutory."},"max_insurable":{"type":["integer","null"],"description":"Piastres a month; null = statutory."},"branches":{"type":"object","description":"Law 148/2019 branches. Omitted = all follow the statutory rules.","properties":{"pension":{"type":"object","properties":{"employee_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employee rate. A fraction (0.09 = 9%). null = follows the statutory rules."},"employer_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employer rate. A fraction (0.09 = 9%). null = follows the statutory rules."}}},"sickness":{"type":"object","properties":{"employee_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employee rate. A fraction (0.09 = 9%). null = follows the statutory rules."},"employer_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employer rate. A fraction (0.09 = 9%). null = follows the statutory rules."}}},"work_injury":{"type":"object","properties":{"employee_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employee rate. A fraction (0.09 = 9%). null = follows the statutory rules."},"employer_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employer rate. A fraction (0.09 = 9%). null = follows the statutory rules."}}},"unemployment":{"type":"object","properties":{"employee_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employee rate. A fraction (0.09 = 9%). null = follows the statutory rules."},"employer_rate":{"type":["number","null"],"minimum":0,"maximum":0.9999,"description":"Employer rate. A fraction (0.09 = 9%). null = follows the statutory rules."}}}}},"treasury_share":{"type":"number","minimum":0,"maximum":1,"default":0},"employer_pays_employee_share":{"type":"boolean","default":false},"insurable_base":{"type":"string","enum":["components","basic_only","declared"],"description":"`components`: the insurable pay components; `basic_only`: basic salary only; `declared`: the declared insurable wage.","default":"components"},"note":{"type":"string","maxLength":300}}},"SiProfile":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"is_active":{"type":"boolean"},"employer_insurance_number":{"type":"string","description":"Empty = the organization's."},"insurance_office":{"type":"string","description":"Empty = the organization's."},"current_rate":{"oneOf":[{"$ref":"#/components/schemas/SiRate"},{"type":"null"}]},"next_rate":{"oneOf":[{"$ref":"#/components/schemas/SiRate"},{"type":"null"}]},"current_merged":{"type":["object","null"],"description":"The current rate merged with today's statutory rules (what payroll uses today)."},"is_default":{"type":"boolean","description":"The organization default today."},"employees_current":{"type":"integer","description":"Employees on this profile today."},"versions_using":{"type":"integer","description":"Salary versions naming it."},"first_effective_from":{"type":["string","null"],"description":"Date, YYYY-MM-DD.","format":"date"},"was_default":{"type":"boolean"},"in_rules":{"type":"boolean","description":"Named by an assignment rule list: cannot be deleted."},"preset":{"type":["object","null"],"description":"The platform preset it was started from."}}},"SiProfileDetail":{"type":"object","description":"profile, rates (status current|scheduled|past, merged, deletable), current_rate, next_rate, statutory, preset, earliest_effective_from.","properties":{"profile":{"$ref":"#/components/schemas/SiProfile"},"rates":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/SiRate"},{"type":"object","properties":{"status":{"type":"string","enum":["current","scheduled","past"]},"merged":{"type":["object","null"]},"deletable":{"type":"boolean"}}}]}},"current_rate":{"oneOf":[{"$ref":"#/components/schemas/SiRate"},{"type":"null"}]},"next_rate":{"oneOf":[{"$ref":"#/components/schemas/SiRate"},{"type":"null"}]},"statutory":{"type":["object","null"]},"preset":{"type":["object","null"]},"earliest_effective_from":{"type":["string","null"],"description":"Date, YYYY-MM-DD.","format":"date"},"currency":{"type":"string","description":"Always EGP.","const":"EGP"}}},"SiProfileInput":{"type":"object","required":["name","rate"],"properties":{"name":{"type":"string","maxLength":80},"description":{"type":"string","maxLength":500},"is_active":{"type":"boolean","default":true},"employer_insurance_number":{"type":"string"},"insurance_office":{"type":"string"},"preset_id":{"type":["string","null"]},"rate":{"$ref":"#/components/schemas/SiRateInput"}}},"SiDefault":{"type":"object","properties":{"id":{"type":"string"},"effective_from":{"type":"string","description":"Date, YYYY-MM-DD.","format":"date"},"profile_id":{"type":["string","null"],"description":"null = no default."},"created_at":{"type":"string","description":"Timestamp, ISO 8601 UTC.","format":"date-time"}}},"SiDefaultInput":{"type":"object","required":["profile_id"],"properties":{"profile_id":{"type":["string","null"],"description":"null = no default from that date."},"effective_from":{"type":"string","description":"Date, YYYY-MM-DD.","format":"date"}}},"SiProfileList":{"type":"object","properties":{"today":{"type":"string","description":"The organization's today.","format":"date"},"earliest_effective_from":{"type":["string","null"],"description":"First date a change may start on (null: no locked or paid run yet).","format":"date"},"defaults":{"type":"object","properties":{"current":{"oneOf":[{"$ref":"#/components/schemas/SiDefault"},{"type":"null"}]},"next":{"oneOf":[{"$ref":"#/components/schemas/SiDefault"},{"type":"null"}]},"history":{"type":"array","items":{"$ref":"#/components/schemas/SiDefault"}}}},"rules":{"type":"object","properties":{"current":{"oneOf":[{"$ref":"#/components/schemas/SiRuleList"},{"type":"null"}]},"next":{"oneOf":[{"$ref":"#/components/schemas/SiRuleList"},{"type":"null"}]},"history":{"type":"array","items":{"$ref":"#/components/schemas/SiRuleList"}}}},"statutory":{"type":["object","null"],"description":"Today's statutory SI values."},"employees_on_default":{"type":"integer","description":"Employees on no profile of their own."},"currency":{"type":"string","description":"Always EGP.","const":"EGP"},"data":{"type":"array","items":{"$ref":"#/components/schemas/SiProfile"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to get the next page; null on the last page."},"has_more":{"type":"boolean","description":"true when there is a next page."}}},"SiRuleCondition":{"type":"object","required":["field","values"],"properties":{"field":{"type":"string","enum":["department","designation","location","employee_type","business_unit","division","expat","work_governorate","cost_center"]},"values":{"type":"array","items":{"type":"string"}}}},"SiRuleList":{"type":"object","properties":{"id":{"type":"string"},"effective_from":{"type":"string","description":"Date, YYYY-MM-DD.","format":"date"},"note":{"type":"string"},"created_at":{"type":"string","description":"Timestamp, ISO 8601 UTC.","format":"date-time"},"rules":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"profile_id":{"type":"string"},"conditions":{"type":"array","items":{"$ref":"#/components/schemas/SiRuleCondition"}}}}}}},"SiRulesInput":{"type":"object","required":["rules"],"properties":{"effective_from":{"type":"string","description":"Date, YYYY-MM-DD.","format":"date"},"note":{"type":"string"},"rules":{"type":"array","maxItems":50,"items":{"type":"object","required":["name","profile_id","conditions"],"properties":{"name":{"type":"string"},"profile_id":{"type":"string"},"conditions":{"type":"array","items":{"$ref":"#/components/schemas/SiRuleCondition"}}}}}}},"SiAssignInput":{"type":"object","required":["employee_ids","si_profile_id","effective_from"],"properties":{"employee_ids":{"type":"array","items":{"type":"string"},"maxItems":200},"si_profile_id":{"type":["string","null"],"description":"null = back to the rules / default."},"effective_from":{"type":"string","description":"Date, YYYY-MM-DD.","format":"date"},"note":{"type":"string"},"dry_run":{"type":"boolean","description":"Check only; write nothing."}}},"SiAssignResult":{"type":"object","properties":{"applied":{"type":"integer"},"would_apply":{"type":"integer"},"failed":{"type":"integer"},"unchanged":{"type":"integer"},"dry_run":{"type":"boolean"},"results":{"type":"array","items":{"type":"object","properties":{"employee_id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","enum":["ok","unchanged","error"]},"error":{"type":["string","null"]},"from":{"type":["string","null"],"description":"The profile before."}}}}}},"SiResolved":{"type":"object","properties":{"employee_id":{"type":"string"},"as_of":{"type":"string","description":"Date, YYYY-MM-DD.","format":"date"},"salary_version_id":{"type":["string","null"]},"insured":{"type":["boolean","null"]},"resolution":{"type":"object","description":"source: explicit | rule | default | statutory; profile_id, profile_name; for a rule also rule_id, rule_name, rule_number.","properties":{"source":{"type":"string","enum":["explicit","rule","default","statutory"]},"profile_id":{"type":["string","null"]},"profile_name":{"type":["string","null"]}}},"in_force":{"type":["boolean","null"]},"rate":{"oneOf":[{"$ref":"#/components/schemas/SiRate"},{"type":"null"}]},"rule_set":{"type":["string","null"],"description":"Statutory rule set code."},"effective":{"type":["object","null"],"description":"Merged limits (piastres), rates, branches, treasury_share, employer_pays_employee_share, insurable_base, sources."},"currency":{"type":"string","description":"Always EGP.","const":"EGP"}}},"Webhook":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string","format":"uri"},"description":{"type":"string"},"events":{"type":"array","items":{"type":"string","enum":["payrun.calculated","payrun.approved","payrun.locked","payrun.paid","payslip.released","employee.synced","ot.decided","si_profile.created","si_profile.rates_changed","si_profile.default_changed","si_profile.rules_changed","si_profile.assigned"]}},"status":{"type":"string","enum":["active","disabled"]},"disabled_reason":{"type":["string","null"],"description":"Set when switched off (by an admin, or automatically after 20 failures in a row)."},"consecutive_failures":{"type":"integer"},"secret_last4":{"type":"string","description":"Last 4 characters of the signing secret."},"last_success_at":{"type":["string","null"],"description":"Timestamp, ISO 8601 UTC; null until it happens.","format":"date-time"},"last_failure_at":{"type":["string","null"],"description":"Timestamp, ISO 8601 UTC; null until it happens.","format":"date-time"},"created_at":{"type":"string","description":"Timestamp, ISO 8601 UTC.","format":"date-time"}}},"WebhookList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}},"events":{"type":"array","items":{"type":"string","enum":["payrun.calculated","payrun.approved","payrun.locked","payrun.paid","payslip.released","employee.synced","ot.decided","si_profile.created","si_profile.rates_changed","si_profile.default_changed","si_profile.rules_changed","si_profile.assigned"]},"description":"Every event type you can subscribe to."}}},"WebhookWithSecret":{"allOf":[{"$ref":"#/components/schemas/Webhook"},{"type":"object","required":["secret"],"properties":{"secret":{"type":"string","description":"Signing secret, whsec_… Shown once."}}}]},"WebhookInput":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","description":"https URL on a public host name, port 443.","format":"uri","maxLength":2048},"events":{"type":"array","minItems":1,"items":{"type":"string","enum":["payrun.calculated","payrun.approved","payrun.locked","payrun.paid","payslip.released","employee.synced","ot.decided","si_profile.created","si_profile.rates_changed","si_profile.default_changed","si_profile.rules_changed","si_profile.assigned"]}},"description":{"type":"string","maxLength":200}}},"WebhookPatch":{"type":"object","properties":{"url":{"type":"string","format":"uri","maxLength":2048},"events":{"type":"array","minItems":1,"items":{"type":"string","enum":["payrun.calculated","payrun.approved","payrun.locked","payrun.paid","payslip.released","employee.synced","ot.decided","si_profile.created","si_profile.rates_changed","si_profile.default_changed","si_profile.rules_changed","si_profile.assigned"]}},"description":{"type":"string","maxLength":200},"status":{"type":"string","enum":["active","disabled"]}}},"Delivery":{"type":"object","properties":{"id":{"type":"string"},"endpoint_id":{"type":"string"},"event_id":{"type":"string"},"event_type":{"type":"string"},"status":{"type":"string","enum":["pending","delivered","failed","cancelled"]},"attempts":{"type":"integer"},"next_attempt_at":{"type":["string","null"],"description":"Next retry; null when none is planned.","format":"date-time"},"last_attempt_at":{"type":["string","null"],"description":"Timestamp, ISO 8601 UTC; null until it happens.","format":"date-time"},"last_status_code":{"type":["integer","null"]},"last_error":{"type":["string","null"]},"delivered_at":{"type":["string","null"],"description":"Timestamp, ISO 8601 UTC; null until it happens.","format":"date-time"},"is_test":{"type":"boolean"},"replay_of":{"type":["string","null"],"description":"The delivery this one replays."},"created_at":{"type":"string","description":"Timestamp, ISO 8601 UTC.","format":"date-time"}}},"DeliveryDetail":{"allOf":[{"$ref":"#/components/schemas/Delivery"},{"type":"object","properties":{"attempts_log":{"type":"array","items":{"type":"object","properties":{"n":{"type":"integer","description":"Attempt number."},"at":{"type":"string","description":"Timestamp, ISO 8601 UTC.","format":"date-time"},"status_code":{"type":["integer","null"]},"error":{"type":["string","null"]},"duration_ms":{"type":["integer","null"]}}}}}}]},"EventEnvelope":{"type":"object","required":["id","type","api_version","created_at","org_id","data"],"properties":{"id":{"type":"string","description":"evt_…; the same for every retry of this event."},"type":{"type":"string","enum":["payrun.calculated","payrun.approved","payrun.locked","payrun.paid","payslip.released","employee.synced","ot.decided","si_profile.created","si_profile.rates_changed","si_profile.default_changed","si_profile.rules_changed","si_profile.assigned","webhook.test"]},"api_version":{"const":"2026-10-12"},"created_at":{"type":"string","description":"Timestamp, ISO 8601 UTC.","format":"date-time"},"org_id":{"type":"string"},"data":{"type":"object"}}},"CommissionSlab":{"type":"object","properties":{"tier_order":{"type":"integer"},"from_amount":{"type":"integer","description":"From (threshold). Integer piastres (1/100 EGP)."},"to_amount":{"type":["integer","null"],"description":"To (threshold); null for the open-ended top tier."},"rate_pct":{"type":"number","description":"Commission percentage (5 = 5%)."}}},"CommissionProfile":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"calc_type":{"type":"string","enum":["fixed_percent","tiered_slabs","flat_slabs","target_slabs"]},"fixed_rate_pct":{"type":["number","null"],"description":"For fixed_percent."},"monthly_target":{"type":"integer","description":"Monthly sales target. Integer piastres (1/100 EGP)."},"active":{"type":"boolean"},"slabs":{"type":"array","items":{"$ref":"#/components/schemas/CommissionSlab"}}}},"CommissionProfileList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CommissionProfile"}}}},"CommissionItemInput":{"type":"object","required":["amount"],"anyOf":[{"required":["employee_id"]},{"required":["employee_code"]},{"required":["employee_email"]}],"properties":{"employee_id":{"type":"string","description":"Wanas Payroll employee id (one of employee_id, employee_code or employee_email is required)."},"employee_code":{"type":"string","description":"Employee number."},"employee_email":{"type":"string","description":"Work e-mail.","format":"email"},"amount":{"type":"integer","minimum":1,"description":"Commission in integer piastres (500000 = EGP 5,000.00)."},"date":{"type":"string","description":"Transaction or deal date; default today. Chooses the pay run when none is given.","format":"date"},"payrun_id":{"type":"string","description":"Pay run to post into."},"label":{"type":"string","maxLength":100,"description":"Label on the payslip (default \"Sales Commission (API)\")."},"reference":{"type":"string","maxLength":100,"description":"Your CRM deal id or invoice number."},"note":{"type":"string","maxLength":255},"taxable":{"type":"boolean","default":true}}},"PostCommissionsBatch":{"type":"object","required":["commissions"],"properties":{"payrun_id":{"type":"string","description":"Pay run to post into (optional)."},"date":{"type":"string","description":"Transaction date; chooses the pay run when payrun_id is not given.","format":"date"},"commissions":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/components/schemas/CommissionItemInput"}}}},"PostCommissionsInput":{"description":"A batch (`commissions`) or a single commission (the item fields at the top level).","oneOf":[{"$ref":"#/components/schemas/PostCommissionsBatch"},{"$ref":"#/components/schemas/CommissionItemInput"}]},"CommissionItemResult":{"type":"object","properties":{"id":{"type":"string","description":"Adjustment id."},"employee_id":{"type":"string"},"employee_name":{"type":"string"},"employee_code":{"type":["string","null"]},"amount":{"type":"integer","description":"Amount. Integer piastres (1/100 EGP)."},"currency":{"type":"string","description":"Always EGP.","const":"EGP"},"label":{"type":"string"},"reference":{"type":["string","null"]},"note":{"type":["string","null"],"description":"Reference and note as shown on the adjustment."},"created_at":{"type":"string","description":"Timestamp, ISO 8601 UTC.","format":"date-time"}}},"PostCommissionsResult":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CommissionItemResult"}},"run":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"period_month":{"type":"string"},"status":{"type":"string","enum":["draft","calculated","approved","locked","paid"]}}},"summary":{"type":"object","properties":{"posted_count":{"type":"integer"},"total_amount":{"type":"integer","description":"Total. Integer piastres (1/100 EGP)."},"currency":{"type":"string","description":"Always EGP.","const":"EGP"}}}}},"CommissionListItem":{"type":"object","properties":{"id":{"type":"string"},"run_id":{"type":"string"},"run_label":{"type":"string"},"period_month":{"type":"string"},"employee_id":{"type":"string"},"employee_name":{"type":"string"},"employee_code":{"type":["string","null"]},"amount":{"type":"integer","description":"Amount. Integer piastres (1/100 EGP)."},"currency":{"type":"string","description":"Always EGP.","const":"EGP"},"label":{"type":"string"},"note":{"type":["string","null"]},"created_at":{"type":"string","description":"Timestamp, ISO 8601 UTC.","format":"date-time"}}},"CommissionList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CommissionListItem"}},"currency":{"type":"string","description":"Always EGP.","const":"EGP"},"total":{"type":"integer","description":"Matches of the filters, all pages."},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to get the next page; null on the last page."},"has_more":{"type":"boolean","description":"true when there is a next page."}}},"Event_payrun_calculated":{"type":"object","properties":{"run_id":{"type":"string"},"period_month":{"type":"string"},"payroll_type":{"type":"string"},"status":{"type":"string"},"revision":{"type":"integer"},"scope":{"type":"string"}},"description":"A pay run was calculated (or recalculated).","example":{"run_id":"run_8f3a","period_month":"2026-10","payroll_type":"main","status":"calculated","revision":1,"scope":"full"}},"Event_payrun_approved":{"type":"object","properties":{"run_id":{"type":"string"},"period_month":{"type":"string"},"payroll_type":{"type":"string"},"status":{"type":"string"},"revision":{"type":"integer"}},"description":"A pay run was approved.","example":{"run_id":"run_8f3a","period_month":"2026-10","payroll_type":"main","status":"approved","revision":1}},"Event_payrun_locked":{"type":"object","properties":{"run_id":{"type":"string"},"period_month":{"type":"string"},"payroll_type":{"type":"string"},"status":{"type":"string"},"revision":{"type":"integer"}},"description":"A pay run was locked (final; payslips exist).","example":{"run_id":"run_8f3a","period_month":"2026-10","payroll_type":"main","status":"locked","revision":1}},"Event_payrun_paid":{"type":"object","properties":{"run_id":{"type":"string"},"period_month":{"type":"string"},"payroll_type":{"type":"string"},"status":{"type":"string"},"revision":{"type":"integer"}},"description":"A pay run was marked as paid.","example":{"run_id":"run_8f3a","period_month":"2026-10","payroll_type":"main","status":"paid","revision":1}},"Event_payslip_released":{"type":"object","properties":{"run_id":{"type":"string"},"period_month":{"type":"string"},"payroll_type":{"type":"string"},"revision":{"type":"integer"},"released_at":{"type":"string"}},"description":"The payslips of a run became visible to employees (once per revision of the run).","example":{"run_id":"run_8f3a","period_month":"2026-10","payroll_type":"main","revision":1,"released_at":"2026-10-28T00:00:00.000Z"}},"Event_employee_synced":{"type":"object","properties":{"added":{"type":"integer"},"updated":{"type":"integer"},"exited":{"type":"integer"},"total":{"type":"integer"},"mode":{"type":"string"}},"description":"The employee list was synchronised from Zoho People.","example":{"added":2,"updated":5,"exited":1,"total":120,"mode":"delta"}},"Event_ot_decided":{"type":"object","properties":{"decision":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"employee_id":{"type":"string"},"date":{"type":"string"},"status":{"type":"string"}}}}},"description":"Overtime was approved or rejected.","example":{"decision":"approved","items":[{"employee_id":"emp_41c2","date":"2026-10-03","status":"approved"}]}},"Event_si_profile_created":{"type":"object","properties":{"profile_id":{"type":"string"},"effective_from":{"type":"string"}},"description":"A social insurance profile was created (with its first rate).","example":{"profile_id":"sip_1a2b","effective_from":"2026-11-01"}},"Event_si_profile_rates_changed":{"type":"object","properties":{"profile_id":{"type":"string"},"change":{"type":"string"},"rate_id":{"type":"string"},"effective_from":{"type":"string"}},"description":"A social insurance profile's limits or rates change from a date (a rate change was added or cancelled). Open pay runs from that date are recalculated.","example":{"profile_id":"sip_1a2b","change":"added","rate_id":"rate_9c8d","effective_from":"2027-01-01"}},"Event_si_profile_default_changed":{"type":"object","properties":{"change":{"type":"string"},"profile_id":{"type":"string"},"effective_from":{"type":"string"}},"description":"The organisation default SI profile changes from a date (set or cancelled). profile_id null = no default.","example":{"change":"set","profile_id":"sip_1a2b","effective_from":"2026-11-01"}},"Event_si_profile_rules_changed":{"type":"object","properties":{"change":{"type":"string"},"list_id":{"type":"string"},"effective_from":{"type":"string"},"rules":{"type":"integer"}},"description":"The SI profile assignment rules change from a date (a new list, or a scheduled one cancelled).","example":{"change":"set","list_id":"lst_4e5f","effective_from":"2026-11-01","rules":3}},"Event_si_profile_assigned":{"type":"object","properties":{"profile_id":{"type":"string"},"effective_from":{"type":"string"},"employees":{"type":"integer"}},"description":"A profile was put on several employee salary packages at once (new salary versions from a date).","example":{"profile_id":"sip_1a2b","effective_from":"2026-11-01","employees":12}}}},"x-webhook-delivery":{"signature_header":"Wanas-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>","headers":["Wanas-Signature","Wanas-Event","Wanas-Event-Id","Wanas-Delivery-Id"],"timeout_seconds":10,"attempts":9,"retry_delays_seconds":[30,120,600,1800,3600,10800,21600,43200],"auto_disable_after_consecutive_failures":20},"x-mcp-server":{"url":"https://payroll.wanasapps.com/mcp","server_card":"https://payroll.wanasapps.com/.well-known/mcp.json","install_page":"https://payroll.wanasapps.com/mcp"},"x-scope-descriptions":{"employees:read":"List and read employees (no national ID, bank account or salary).","payruns:read":"List and read pay runs, their totals and per-employee results.","payslips:read":"List payslips of released pay runs and download them as PDF.","reports:read":"Run the payroll reports as JSON or CSV.","webhooks:manage":"Create, change, test and delete webhook endpoints; read the delivery log.","si_profiles:read":"Read social insurance profiles, rates, defaults, assignment rules and what applies to an employee.","si_profiles:write":"Create SI profiles, schedule rate, default and rule changes, assign profiles to employees.","commissions:read":"Read commission profiles and posted commissions.","commissions:write":"Post sales commissions into open (draft or calculated) pay runs."}}