ahlan hamad

API reference

Version 1.0.0-draft.1 · generated fromopenapi.yaml(JSON). Base URL:https://actions.ahlanhamad.com.

The Ahlan Hamad Partner API lets approved partners (accounting, point-of-sale, banking) integrate with a company's HR and payroll data in Ahlan Hamad.

Status: DRAFT. This contract is published ahead of the implementation so partners can build in parallel. Every operation carries an x-release marker:

ReleaseContents
v1.0Company, employees, payroll runs (read)
v1.1Attendance ingest + read-back
v1.2Webhook endpoints + events

Shapes may still change before each release ships; after it ships, v1 only changes additively (new fields, new optional parameters, new enum values, new event types). Clients must ignore unknown fields and tolerate unknown enum values.

Data protection. v1 never returns salaries, allowances per employee, bank details (IBAN), civil ID / passport numbers, dates of birth, social insurance numbers, or documents. Payroll data is available only as run-level totals and journal lines.

Conventions

Authentication

Authorization: Bearer <key>. Keys are created in Ahlan Hamad under Integrations → API keys by a company user with the integrations.manage permission (CEO or HR admin by default). A key belongs to exactly one company, carries a fixed set of scopes, and is shown once at creation.

Sandbox, self-serve. Anyone with an Ahlan Hamad login can press Create sandbox company (on /developers or Integrations → API keys) to get a private sandbox company pre-filled with fake employees and one finalized payroll run, then issue ah_test_ keys for it. No contact with Ahlan Hamad is needed to start building.

Scopes: employees:read, payroll:read, attendance:read, attendance:write, webhooks:manage. GET /v1/company needs none.

Company

The company this API key belongs to

GET/v1/companyships in v1.0any valid key

Available to every valid key; no scope required.

Responses

StatusDescriptionBody
200

The company.

Company
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem
429

Too many requests for this key.

Problem

Employees

List employees

GET/v1/employeesships in v1.0scope: employees:read

Returns partner-safe employee records, oldest first. Archived records are never returned. To follow changes, subscribe to the employee.* webhooks (v1.2) instead of polling; there is no updated_since filter in v1.

Parameters

NameInTypeDescription
statusquerystring

active — currently employed (every status except left). left — employment has ended. Omit for both.

One of: active, left
entity_idquerystring

Only employees of this legal entity (see Company.entities).

limitqueryinteger

Page size.

Range: 1–200Default: 50
cursorquerystring

Opaque next_cursor from the previous page. Omit for the first page.

Responses

StatusDescriptionBody
200

A page of employees.

Page + object
400

Malformed request (bad query parameter, invalid JSON, unknown cursor).

Problem
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem
429

Too many requests for this key.

Problem

Get one employee

GET/v1/employees/{employee_id}ships in v1.0scope: employees:read

Parameters

NameInTypeDescription
employee_idrequiredpathstring

Responses

StatusDescriptionBody
200

The employee.

Employee
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem
404

No such resource in this company (or not visible to partners, e.g. a draft payroll run).

Problem
429

Too many requests for this key.

Problem

Payroll

List closed payroll runs

GET/v1/payroll-runsships in v1.0scope: payroll:read

Returns only runs that are finalized or paid, newest month first. Draft runs are never exposed. A company that pays in several countries has one run per country (and legal entity) per month.

Parameters

NameInTypeDescription
fromqueryMonth

First payroll month to include (inclusive).

toqueryMonth

Last payroll month to include (inclusive).

statusquerystringOne of: finalized, paid
limitqueryinteger

Page size.

Range: 1–200Default: 50
cursorquerystring

Opaque next_cursor from the previous page. Omit for the first page.

Responses

StatusDescriptionBody
200

A page of payroll runs (summary form).

Page + object
400

Malformed request (bad query parameter, invalid JSON, unknown cursor).

Problem
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem
429

Too many requests for this key.

Problem

Get a closed payroll run with its components and journal

GET/v1/payroll-runs/{payroll_run_id}ships in v1.0scope: payroll:read

Run totals, a breakdown by pay component (each with the number of employees it applies to), and the double-entry journal Ahlan Hamad posted for the run. No per-employee amounts are returned.

Parameters

NameInTypeDescription
payroll_run_idrequiredpathstring

Responses

StatusDescriptionBody
200

The payroll run.

PayrollRun
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem
404

No such resource in this company (or not visible to partners, e.g. a draft payroll run).

Problem
429

Too many requests for this key.

Problem

Attendance

Push clock events (clock in/out, breaks)

POST/v1/attendance/eventsships in v1.1scope: attendance:write

Submit up to 500 events per request. Each event is processed independently and the response lists one result per event, in the same order as the request.

Idempotent on external_id (per company and source): resending an event you already sent returns duplicate and changes nothing, so it is always safe to retry a request that timed out.

Employee matching. employee_ref identifies the person. Events whose employee cannot be matched are stored, not dropped, with result unmatched_employee: the company's HR team links the person once in Ahlan Hamad and every stored event for them is then processed automatically. You do not need to resend.

Branches. branch_ref is your identifier for the location. An unknown branch never rejects an event — it is stored as given and HR can map it later.

Clock events are paired into shifts in the company's timezone; a shift that crosses midnight belongs to the day it started. Break time is subtracted from worked time.

Request body

FieldTypeDescription
eventsrequiredarray of AttendanceEventInputUp to 500 items

Example:

{
  "events": [
    {
      "external_id": "qompos-sess-8812-in",
      "employee_ref": {
        "type": "external_id",
        "value": "cashier-0042"
      },
      "type": "clock_in",
      "at": "2026-09-29T08:02:11+04:00",
      "branch_ref": "dubai-marina-01",
      "source": "qompos"
    },
    {
      "external_id": "qompos-sess-8812-out",
      "employee_ref": {
        "type": "external_id",
        "value": "cashier-0042"
      },
      "type": "clock_out",
      "at": "2026-09-29T16:31:40+04:00",
      "branch_ref": "dubai-marina-01",
      "source": "qompos"
    }
  ]
}

Responses

StatusDescriptionBody
207

Per-event results, in request order.

object
400

Malformed request (bad query parameter, invalid JSON, unknown cursor).

Problem
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem
413

More than 500 events, or body larger than 1 MB.

Problem
422

The body is well-formed JSON but fails validation.

Problem
429

Too many requests for this key.

Problem

Read back daily attendance

GET/v1/attendanceships in v1.1scope: attendance:read

One record per employee per day, as Ahlan Hamad holds it after pairing clock events. Includes days recorded by other sources (the employee app, manual HR entries) — check sources.

Parameters

NameInTypeDescription
fromrequiredquerystring (date)
torequiredquerystring (date)

Inclusive. At most 62 days after from.

employee_idquerystring
limitqueryinteger

Page size.

Range: 1–200Default: 50
cursorquerystring

Opaque next_cursor from the previous page. Omit for the first page.

Responses

StatusDescriptionBody
200

A page of attendance days.

Page + object
400

Malformed request (bad query parameter, invalid JSON, unknown cursor).

Problem
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem
429

Too many requests for this key.

Problem

Webhooks

List webhook endpoints registered by this key

GET/v1/webhooksships in v1.2scope: webhooks:manage

Responses

StatusDescriptionBody
200

Endpoints owned by the calling key.

object
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem

Register a webhook endpoint

POST/v1/webhooksships in v1.2scope: webhooks:manage

The response includes the endpoint's signing secret once. Store it; it cannot be retrieved again (rotate by deleting and re-creating). The URL must be https:// on a public host.

Request body

WebhookEndpointInput

Responses

StatusDescriptionBody
201

Created.

WebhookEndpoint + object
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem
422

The body is well-formed JSON but fails validation.

Problem

Get a webhook endpoint

GET/v1/webhooks/{webhook_id}ships in v1.2scope: webhooks:manage

Parameters

NameInTypeDescription
webhook_idrequiredpathstring

Responses

StatusDescriptionBody
200

The endpoint.

WebhookEndpoint
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem
404

No such resource in this company (or not visible to partners, e.g. a draft payroll run).

Problem

Change URL, events, or re-enable an endpoint

PATCH/v1/webhooks/{webhook_id}ships in v1.2scope: webhooks:manage

Parameters

NameInTypeDescription
webhook_idrequiredpathstring

Request body

FieldTypeDescription
urlstring (uri)
eventsarray of WebhookEventType
activeboolean

Responses

StatusDescriptionBody
200

Updated.

WebhookEndpoint
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem
404

No such resource in this company (or not visible to partners, e.g. a draft payroll run).

Problem
422

The body is well-formed JSON but fails validation.

Problem

Delete a webhook endpoint

DELETE/v1/webhooks/{webhook_id}ships in v1.2scope: webhooks:manage

Parameters

NameInTypeDescription
webhook_idrequiredpathstring

Responses

StatusDescriptionBody
204

Deleted. Pending deliveries are cancelled.

—
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem
404

No such resource in this company (or not visible to partners, e.g. a draft payroll run).

Problem

Send a `ping` event to the endpoint now

POST/v1/webhooks/{webhook_id}/testships in v1.2scope: webhooks:manage

Parameters

NameInTypeDescription
webhook_idrequiredpathstring

Responses

StatusDescriptionBody
202

Queued for immediate delivery.

—
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem
404

No such resource in this company (or not visible to partners, e.g. a draft payroll run).

Problem

Recent delivery attempts for an endpoint (self-debugging)

GET/v1/webhooks/{webhook_id}/deliveriesships in v1.2scope: webhooks:manage

The last 100 deliveries to this endpoint, newest first: what we sent, when, what your server answered, and whether we will retry. The same log is visible to the company in Ahlan Hamad under Integrations → API keys. Deliveries older than 30 days are removed.

Parameters

NameInTypeDescription
webhook_idrequiredpathstring
statusquerystringOne of: succeeded, failed, pending

Responses

StatusDescriptionBody
200

Up to 100 deliveries.

object
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem
404

No such resource in this company (or not visible to partners, e.g. a draft payroll run).

Problem

Send one past event again now

POST/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliverships in v1.2scope: webhooks:manage

Re-sends the original event (same event id, same payload, fresh signature and timestamp) as a new delivery attempt. Works for any delivery in the log, including successful ones, and on an endpoint that was auto-disabled — re-enable it first with PATCH.

Parameters

NameInTypeDescription
webhook_idrequiredpathstring
delivery_idrequiredpathstring

Responses

StatusDescriptionBody
202

Queued. The new attempt appears in the deliveries log.

WebhookDelivery
401

Missing, malformed, revoked or wrong-environment API key.

Problem
403

The key is valid but lacks the required scope (insufficient_scope), or the Partner API is not enabled for this company (api_not_enabled).

Problem
404

No such resource in this company (or not visible to partners, e.g. a draft payroll run).

Problem
409

The endpoint is disabled.

Problem

Webhook events

Every delivery is a WebhookEventenvelope whose data depends on the event type. Verify theX-AH-Signature header on every delivery — see theQuickstart.

EventWhenRelease
employee.createdAn employee was addedv1.2
employee.updatedA partner-visible field of an employee changed

Sent when any field in the Employee schema changes. Delivered at least once and possibly late (a nightly reconciliation catches changes made through bulk tools); always re-read the embedded object rather than diffing.

v1.2
employee.leftAn employee's employment endedv1.2
payroll_run.finalizedA payroll run was closed and its journal postedv1.2
payroll_run.paidA payroll run was marked paid and its payment journal postedv1.2
payroll_run.correctedA closed payroll run's amounts changed

A correction was applied to a run that was already finalized or paid. Totals and journal lines may have changed — re-fetch the run with GET /v1/payroll-runs/{id}.

v1.2
payroll_run.deletedA finalized payroll run was deleted and its journal reversedv1.2
attendance.rejectedA stored clock event could not become attendance

Sent for events that were accepted but later could not be paired into a valid shift (e.g. a clock-out with no clock-in, or a shift over 16 hours), and for events still unmatched to an employee after 72 hours.

v1.2
pingTest event sent by the "send test event" actionv1.2

Schemas

Problem

RFC 9457 (formerly 7807) problem details.

FieldTypeDescription
typerequiredstring (uri)
titlerequiredstring
statusrequiredinteger
detailstring
coderequiredstring

Stable machine-readable error code. Branch on this, not on title.

One of: invalid_request, unauthenticated, insufficient_scope, api_not_enabled, not_found, conflict, payload_too_large, validation_failed, rate_limited, internal_error
request_idrequiredstring
errorsarray of object

Field-level problems (for validation_failed).

Page

FieldTypeDescription
next_cursorrequiredstring | null

Pass as cursor to get the next page. null on the last page.

Month

stringPattern: ^[0-9]{4}-(0[1-9]|1[0-2])$

Money

Decimal string in the currency's minor-unit precision.

stringPattern: ^-?[0-9]+(\.[0-9]{1,3})?$

CountryCode

stringOne of: KW, SA, AE, QA, BH, OM

CurrencyCode

ISO 4217.

string

LocalizedText

FieldTypeDescription
enrequiredstring
arstring | null

Company

FieldTypeDescription
idrequiredstring
namerequiredLocalizedText
countryrequiredCountryCode
currencyrequiredCurrencyCode
timezonerequiredstring

IANA timezone, e.g. Asia/Kuwait.

sandboxrequiredboolean

True for sandbox companies (fake data, ah_test_ keys).

entitiesrequiredarray of Entity

Legal establishments. A company operating in several GCC countries has one per country; each has its own currency and payroll runs.

Employee

Partner-safe employee record. Salary, bank, ID-document, date-of-birth, insurance-number and exit-reason data are never included.

FieldTypeDescription
idrequiredstring
employee_coderequiredstring | null

The company's own staff number, if it uses one.

namerequiredLocalizedText
job_titlerequiredLocalizedText
departmentrequiredstring
statusrequiredstring

notice_period — resignation or termination recorded, last day not yet reached. left — employment has ended (the reason is not exposed).

One of: active, probation, on_leave, suspended, notice_period, left
employment_typerequiredstringOne of: full_time, part_time, contract
countryrequiredCountryCode
entity_idstring | null
work_locationnull | object

The branch / site the employee is assigned to, if any.

manager_idstring | null
emailstring (email) | null

Contact email on file (usually the work address).

phonestring | null

Contact phone on file, E.164 where available.

hire_daterequiredstring (date)
termination_datestring (date) | null
external_idsmap of string

Identifiers this person has in partner systems, keyed by partner app (e.g. {"qompos": "cashier-0042"}). Only the calling key's own app is included. Available from v1.1.

PayrollRunSummary

FieldTypeDescription
idrequiredstring
monthrequiredMonth
period_startrequiredstring (date)
period_endrequiredstring (date)
pay_datestring (date) | null
statusrequiredstringOne of: finalized, paid
countryrequiredCountryCode
entity_idstring | null
currencyrequiredCurrencyCode
employee_countrequiredinteger
totalsrequiredobject
last_corrected_atstring (date-time) | null

Set if a correction changed this run after it was closed.

PayrollRun

Includes every field of PayrollRunSummary.

FieldTypeDescription
componentsrequiredarray of PayrollComponent

Run totals broken down by pay component. Components with a zero total are omitted.

journalrequiredarray of JournalEntry

The journal entries Ahlan Hamad posted for this run: the accrual leg when the run was finalized, and the payment leg once it is marked paid. Each entry balances (sum of debits = sum of credits).

PayrollComponent

FieldTypeDescription
typerequiredstring

New types may be added; treat unknown values as "other". eosb_accrual and wps_bank_fees appear once end-of-service accrual and wage-protection fees are included in the payroll journal.

One of: basic_salary, housing_allowance, transport_allowance, social_allowance, other_allowances, overtime, additions, absence_deductions, leave_deductions, loan_deductions, other_deductions, employee_social_insurance, employer_social_insurance, employer_dews, income_tax, eosb_accrual, wps_bank_fees, net_pay
totalrequiredMoney
employee_countrequiredinteger

Number of employees with a non-zero amount for this component.

JournalEntry

FieldTypeDescription
idrequiredstring
legrequiredstringOne of: accrual, payment
referencerequiredstring
daterequiredstring (date)
currencyrequiredCurrencyCode
statusrequiredstringOne of: posted, reversed
descriptionLocalizedText
linesrequiredarray of JournalLine

JournalLine

FieldTypeDescription
account_coderequiredstring

Account code in the company's Ahlan Hamad chart of accounts (e.g. 5100).

account_namerequiredLocalizedText
account_typestringOne of: asset, liability, equity, revenue, expense
componentstring | null

The PayrollComponent.type this line comes from, when it maps to one.

debitrequiredMoney
creditrequiredMoney
memostring | null

EmployeeRef

FieldTypeDescription
typerequiredstring
  • external_id — your own identifier for the person (recommended). HR links it to the employee once; after that it always matches.
  • employee_id — Ahlan Hamad's Employee.id.
  • employee_code — the company's staff number.
  • email, phone — matched against the employee's contact details.
One of: external_id, employee_id, employee_code, email, phone
valuerequiredstring

AttendanceEventInput

FieldTypeDescription
external_idrequiredstring

Your unique ID for this event. Makes the call idempotent.

Max 128 characters
employee_refrequiredEmployeeRef
typerequiredstringOne of: clock_in, clock_out, break_start, break_end
atrequiredstring (date-time)

When it happened, RFC 3339 with offset. Must not be more than 5 minutes in the future or 35 days in the past.

branch_refstring

Your identifier for the location.

Max 128 characters
sourcerequiredstring

Your system's name, fixed per integration (e.g. qompos).

Pattern: ^[a-z0-9_-]{2,32}$
metaobject

Free-form, stored for support (max 2 KB). Never interpreted.

AttendanceEventResult

FieldTypeDescription
external_idrequiredstring
event_idstring
statusrequiredstring
  • accepted — stored and matched to an employee.
  • duplicate — already received; nothing changed.
  • unmatched_employee — stored; will be processed once HR links the person.
  • rejected — not stored; see reason.
One of: accepted, duplicate, unmatched_employee, rejected
reasonstring

Present when status is rejected.

One of: invalid_timestamp, timestamp_out_of_range, invalid_type, employee_left, period_locked
detailstring

AttendanceDay

FieldTypeDescription
employee_idrequiredstring
daterequiredstring (date)
statusrequiredstringOne of: present, late, half_day, absent, on_leave, holiday
first_instring (date-time) | null
last_outstring (date-time) | null
worked_minutesrequiredinteger
break_minutesrequiredinteger
sourcesrequiredarray of string

Where the day's records came from.

WebhookEventType

stringOne of: employee.created, employee.updated, employee.left, payroll_run.finalized, payroll_run.paid, payroll_run.corrected, payroll_run.deleted, attendance.rejected

WebhookEndpointInput

FieldTypeDescription
urlrequiredstring (uri)

https:// only; private and loopback addresses are refused.

eventsrequiredarray of WebhookEventType
descriptionstringMax 200 characters

WebhookEndpoint

FieldTypeDescription
idrequiredstring
urlrequiredstring (uri)
eventsrequiredarray of WebhookEventType
descriptionstring | null
activerequiredboolean

False after 7 days of failed deliveries, or when the API key that owns it is revoked. Re-enable with PATCH once fixed.

disabled_reasonstring | nullOne of: delivery_failures, key_revoked, manual
created_atrequiredstring (date-time)
last_deliverynull | object

WebhookDelivery

FieldTypeDescription
idrequiredstring
event_idrequiredstring
event_typerequiredstring
attemptrequiredinteger

1 for the first try; increments on each retry or redelivery.

statusrequiredstringOne of: succeeded, failed, pending
response_status_codeinteger | null

Your server's HTTP status, or null if the connection failed or timed out.

response_body_excerptstring | null

First 1 KB of your response body.

errorstring | null

Our side of a failure (timeout, DNS, TLS, connection refused).

duration_msinteger | null
created_atrequiredstring (date-time)
next_attempt_atstring (date-time) | null

When we will retry, if we will.

requestobject

Exactly what we sent.

WebhookEvent

Envelope for every webhook delivery.

Verify every delivery. Each request carries X-AH-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256> where the HMAC is computed with the endpoint's secret over <t>.<raw request body>. Reject if the signature does not match or t is more than 5 minutes from your clock.

Delivery. At least once, not necessarily in order. Deduplicate on id. Respond 2xx within 10 seconds; otherwise we retry after 1 min, 5 min, 30 min, 2 h, 12 h and 24 h. After 7 days of failures the endpoint is disabled and the company owner is emailed.

FieldTypeDescription
idrequiredstring

Unique event ID.

typerequiredstring
created_atrequiredstring (date-time)
company_idrequiredstring
livemoderequiredboolean

False for sandbox companies.

datarequiredobject