Skip to content

Harmoniq API

Connect your systems to Harmoniq to read and update employee records, units, leave, performance, training and other company data. The API uses JSON over HTTPS, with separate keys for each system and permissions for each resource.

The API is part of the Enterprise plan.

Getting started

  1. As a company owner or admin, open Access → API keys and choose New key.
  2. Name the key after the system that will use it. Grant only the read or write permissions it needs and, where possible, restrict access to the system’s IP addresses.
  3. Copy the key: it is shown once. Harmoniq keeps only a fingerprint of it.
  4. Call your company’s address with the key in the Authorization header.

Every call goes to your company’s own address:

https://compania-mea.harmoniq.ro/api/v1
curl https://compania-mea.harmoniq.ro/api/v1/employees?per_page=10 \
  -H "Authorization: Bearer hq_12_••••••••"

Authentication

Send the key in the Authorization header as a Bearer token. A key works only at its own company’s address. It can expire, be suspended and resumed, or revoked at any time; it can be limited to IP addresses or ranges. Changes are recorded under the name of the person who created the key. If they lose permission to manage API keys, the key retains read access but can no longer write data.

Authorization: Bearer hq_12_••••••••

Scopes

For each resource, a key can have read ({resource}:read) and write ({resource}:write) permissions. These apply to data across the whole company. Two additional scopes are available: sensitive, for reading and writing custom fields marked as sensitive, and metrics:write, for sending KPI values using business references.

Requests and responses

  • Requests and responses use JSON with UTF-8 encoding. Include Content-Type: application/json when sending a request body.
  • Use YYYY-MM-DD for dates and ISO 8601 for timestamps. Responses use UTC, for example 2026-05-01T08:00:00Z.
  • A single record is returned as {"data": {…}}; a list as {"data": […], "meta": {page, per_page, total, last_page}, "links": {next, prev}}.
  • Lists: page (from 1), per_page (25 by default, at most 100), sort (id, created_at, updated_at; a minus for descending), updated_since (records modified after a given timestamp, for synchronisation), plus each resource’s filters.
  • Create records with POST and update them with PATCH, sending only the changed fields. DELETE returns 204. Requests containing fields that cannot be written are rejected.
  • Write operations are atomic: all changes are saved together, or none are saved.
GET https://compania-mea.harmoniq.ro/api/v1/employees?status=active&updated_since=2026-05-01T00:00:00Z&page=2

Errors

Errors are returned as {"error": "<code>", "message": "<text>"}. Invalid data returns status 422 with details in "errors" for each affected field.

StatusCodeMeaning
401invalid_tokenThe key is missing, unknown or belongs to another company.
403token_inactiveThe key is suspended or expired.
403insufficient_scopeThe key lacks the scope required for this operation.
403ip_not_allowedThe key does not accept calls from this address.
403token_owner_inactiveThe key’s creator may no longer manage keys: it reads, but does not write.
404—No such resource or record in your company (or not in your plan).
405—The resource does not offer this operation.
409in_useThe record is still used elsewhere and cannot be deleted.
422—Invalid data; nothing was saved.
429—Too many requests; try again after Retry-After seconds.

Limits

The limit is 120 requests per minute per key and 300 per IP address. Exceeding it returns status 429. The Retry-After header specifies how long to wait before trying again.

Versions

The current version is 1 (/api/v1). New resources, fields and filters are added to this version. Incompatible changes will be introduced in a new version, announced in advance.

Personal data

  • The CNP (Romanian personal numeric code) cannot be read or written through the API.
  • Access to sensitive custom fields requires the sensitive scope.
  • Anonymous survey results are available only as totals. They remain unavailable while responses are sealed or too few to protect participants’ anonymity.
  • The API log retains the key, endpoint, response status and duration for 30 days, without the request or response content. Recent calls are available in Access → API keys.

Pushing KPI values

Systems that only send KPI values (sales, reviews, counts) can do it by business reference instead of ids: POST /api/v1/metrics/{key}/values with a period and the values for employees (by employee number), units or departments (by code). Needs the metrics:write scope; GET /api/v1/metrics lists the metrics that accept them.

POST https://compania-mea.harmoniq.ro/api/v1/metrics/sales.actual/values

{
    "period": "2026-09",
    "values": [
        {
            "subject": "CJ01",
            "value": 1234.5
        },
        {
            "subject": "FV0012",
            "value": 980
        }
    ]
}

Organisation

Geographic areas /geo-areas

The tree of countries, regions and zones the units belong to; it also sets area managers’ reach. Names are unique among siblings. Deleting one is refused while units or sub-areas use it.

Scopes: geo_areas:read, geo_areas:write

Endpoints

  • GET /api/v1/geo-areas List
  • GET /api/v1/geo-areas/{id} One record
  • POST /api/v1/geo-areas Create
  • PATCH /api/v1/geo-areas/{id} Change
  • DELETE /api/v1/geo-areas/{id} Delete

Fields

NameTypeDescription
idintegerread only Identifier.
namestringrequired Name, unique among the areas with the same parent.
level_labelstring | null What this level is called (country, region, zone).
parent_idinteger | null The area it sits in; null for a top-level area. Never itself or one of its own sub-areas.
sortinteger | null Order among its siblings.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

parent_idintegerOnly the areas directly under this one.
rootsbooleantrue: only the top-level areas.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/geo-areas

{
    "name": "Nord-Vest",
    "level_label": "regiune",
    "parent_id": 1,
    "sort": 0
}

Example response

{
    "data": {
        "id": 3,
        "name": "Nord-Vest",
        "level_label": "regiune",
        "parent_id": 1,
        "sort": 0,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Work units /work-units

The company’s units: stores, pharmacies, offices, hotels. Deleting one is refused while people, documents or results still refer to it.

Scopes: work_units:read, work_units:write

Endpoints

  • GET /api/v1/work-units List
  • GET /api/v1/work-units/{id} One record
  • POST /api/v1/work-units Create
  • PATCH /api/v1/work-units/{id} Change
  • DELETE /api/v1/work-units/{id} Delete

Fields

NameTypeDescription
idintegerread only Identifier.
namestringrequired Name, unique in the company.
codestring | null Your own code, unique in the company (used to match KPI values).
geo_area_idinteger | null The geographic area (region, country) it belongs to.
addressstring | null Street address.
citystring | nullread only City (from the official list for Romanian units).
countystring | nullread only County or region.
phonestring | null Phone.
emailstring | null Email.
opened_ondate | null Opening date.
closed_ondate | null Closing date.
is_activeboolean Whether it is in use.
custom_fieldsobject The company’s own fields, by field id (see /custom-fields).
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

codestringExact code (case-insensitive).
geo_area_idintegerOnly in this area.
is_activebooleantrue or false.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/work-units

{
    "name": "Farmacia Verde Centru",
    "code": "CJ01",
    "geo_area_id": 3,
    "address": "Str. Memorandumului 10",
    "opened_on": "2016-03-01",
    "is_active": true,
    "custom_fields": {
        "12": "XL"
    }
}

Example response

{
    "data": {
        "id": 12,
        "name": "Farmacia Verde Centru",
        "code": "CJ01",
        "geo_area_id": 3,
        "address": "Str. Memorandumului 10",
        "city": "Cluj-Napoca",
        "county": "Cluj",
        "phone": null,
        "email": null,
        "opened_on": "2016-03-01",
        "closed_on": "2026-05-01",
        "is_active": true,
        "custom_fields": {
            "12": "XL"
        },
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Departments /departments

Company-wide functions (Sales, Logistics…) that can be nested. Deleting one is refused while placements or sub-departments use it.

Scopes: departments:read, departments:write

Endpoints

  • GET /api/v1/departments List
  • GET /api/v1/departments/{id} One record
  • POST /api/v1/departments Create
  • PATCH /api/v1/departments/{id} Change
  • DELETE /api/v1/departments/{id} Delete

Fields

NameTypeDescription
idintegerread only Identifier.
namestringrequired Name, unique in the company.
codestring | null Your own code, unique in the company (used to match KPI values).
parent_idinteger | null The department it belongs to; never itself or one of its own sub-departments.
is_activeboolean Whether it is in use.
custom_fieldsobject The company’s own fields, by field id (see /custom-fields).
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

codestringExact code (case-insensitive).
parent_idintegerOnly the departments directly under this one.
is_activebooleantrue or false.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/departments

{
    "name": "Farmacie",
    "code": "FARM",
    "is_active": true,
    "custom_fields": {
        "12": "XL"
    }
}

Example response

{
    "data": {
        "id": 4,
        "name": "Farmacie",
        "code": "FARM",
        "parent_id": 1,
        "is_active": true,
        "custom_fields": {
            "12": "XL"
        },
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Positions /positions

Job positions and their level (a higher number is a more senior position). Deleting one is refused while someone holds it.

Scopes: positions:read, positions:write

Endpoints

  • GET /api/v1/positions List
  • GET /api/v1/positions/{id} One record
  • POST /api/v1/positions Create
  • PATCH /api/v1/positions/{id} Change
  • DELETE /api/v1/positions/{id} Delete

Fields

NameTypeDescription
idintegerread only Identifier.
namestringrequired Name, unique in the company.
codestring | null Your own code, unique in the company.
levelintegerrequired Seniority, from 0 to 100.
descriptiontext | null Description.
is_activeboolean Whether it is in use.
custom_fieldsobject The company’s own fields, by field id (see /custom-fields).
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

codestringExact code (case-insensitive).
is_activebooleantrue or false.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/positions

{
    "name": "Farmacist",
    "code": "FA",
    "level": 2,
    "is_active": true,
    "custom_fields": {
        "12": "XL"
    }
}

Example response

{
    "data": {
        "id": 7,
        "name": "Farmacist",
        "code": "FA",
        "level": 2,
        "description": null,
        "is_active": true,
        "custom_fields": {
            "12": "XL"
        },
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Teams /teams

Groups of people across units (a project, a working group). Members are added and removed one by one with the actions below.

Scopes: teams:read, teams:write

Endpoints

  • GET /api/v1/teams List
  • GET /api/v1/teams/{id} One record
  • POST /api/v1/teams Create
  • PATCH /api/v1/teams/{id} Change
  • DELETE /api/v1/teams/{id} Delete
  • POST /api/v1/teams/{id}/add-member Adds an employee to the team (nothing changes if they are already in it). Body: {"employee_id": 345}.
  • POST /api/v1/teams/{id}/remove-member Removes an employee from the team. Body: {"employee_id": 345}.

Fields

NameTypeDescription
idintegerread only Identifier.
namestringrequired Name, unique in the company.
descriptiontext | null Description.
is_activeboolean Whether it is in use.
member_idsarrayread only The employees in the team (read only; change them with add-member / remove-member).
custom_fieldsobject The company’s own fields, by field id (see /custom-fields).
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

is_activebooleantrue or false.
employee_idintegerOnly the teams this employee is in.

/api/v1/teams/{id}/add-member: employee_id (integer) — The employee to add (an id from /employees).

/api/v1/teams/{id}/remove-member: employee_id (integer) — The employee to remove (an id from /employees).

Example request

POST https://compania-mea.harmoniq.ro/api/v1/teams

{
    "name": "Proiect inventar",
    "is_active": true,
    "custom_fields": {
        "12": "XL"
    }
}

Example response

{
    "data": {
        "id": 2,
        "name": "Proiect inventar",
        "description": null,
        "is_active": true,
        "member_ids": [
            345,
            346
        ],
        "custom_fields": {
            "12": "XL"
        },
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Custom fields /custom-fields

The fields the company added to its own records, read only: what each id in a record’s custom_fields means, its type and choices. Sensitive employee fields are listed only for a key with the sensitive scope. Fields are defined in the app.

Scopes: custom_fields:read

Endpoints

  • GET /api/v1/custom-fields List
  • GET /api/v1/custom-fields/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier; the key in a record’s custom_fields.
entityemployee | work_unit | department | position | team | companyread only The kind of record it belongs to.
labelstringread only Its name, in the language of the request (Accept-Language).
typetext | textarea | number | date | boolean | select | multiselect | urlread only What it holds: text, textarea, number, date (Y-m-d), boolean, select, multiselect or url.
optionsarray | nullread only The choices of a select or multiselect: id and label; the value stored is the id.
is_requiredbooleanread only Whether a value is required.
visibilityreach | sensitiveread only reach (everyone who sees the employee) or sensitive.
is_activebooleanread only Whether it is in use.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

entityemployee | work_unit | department | position | team | companyOnly the fields of this kind of record.
is_activebooleantrue or false.

Example response

{
    "data": {
        "id": 12,
        "entity": "employee",
        "label": "Mărime uniformă",
        "type": "select",
        "options": [
            {
                "id": "m",
                "label": "M"
            }
        ],
        "is_required": false,
        "visibility": "reach",
        "is_active": true,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

People and time

Employees /employees

The company’s people. Creating one also creates their first placement; later moves go through /placements. The CNP (personal numeric code) is never read or written through the API. Logins are created in the app.

Scopes: employees:read, employees:write

Endpoints

  • GET /api/v1/employees List
  • GET /api/v1/employees/{id} One record
  • POST /api/v1/employees Create
  • PATCH /api/v1/employees/{id} Change
  • DELETE /api/v1/employees/{id} Delete

Fields

NameTypeDescription
idintegerread only Identifier.
employee_numberstring | null Your employee number, unique in the company (used to match KPI values).
first_namestringrequired First name.
last_namestringrequired Last name.
emailstring | null Work email.
phonestring | null Phone.
birth_datedate | null Date of birth.
hire_datedate | null Hire date (the first placement starts on it).
termination_datedate | null Leaving date.
statusactive | inactive | terminated active, inactive or terminated.
work_unit_idintegerrequired Unit of the current main placement; required on create, read-only after.
department_idinteger | null Department of the current main placement (create only).
position_idinteger | null Position of the current main placement (create only).
has_loginbooleanread only Whether the person can sign in.
custom_fieldsobject The company’s own fields, by field id; sensitive ones only with the sensitive scope.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

statusactive | inactive | terminatedOnly with this status.
work_unit_idintegerCurrently placed in this unit.
employee_numberstringExact employee number.
emailstringExact email.
qstringPart of the name.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/employees

{
    "employee_number": "FV0012",
    "first_name": "Ana",
    "last_name": "Popescu",
    "email": "ana.popescu@example.com",
    "hire_date": "2024-02-01",
    "status": "active",
    "work_unit_id": 12,
    "custom_fields": {
        "12": "XL"
    }
}

Example response

{
    "data": {
        "id": 345,
        "employee_number": "FV0012",
        "first_name": "Ana",
        "last_name": "Popescu",
        "email": "ana.popescu@example.com",
        "phone": null,
        "birth_date": "2026-05-01",
        "hire_date": "2024-02-01",
        "termination_date": "2026-05-01",
        "status": "active",
        "work_unit_id": 12,
        "department_id": 1,
        "position_id": 1,
        "has_login": true,
        "custom_fields": {
            "12": "XL"
        },
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Placements /placements

Where each person works: unit, department and position, from when to when. A new main placement takes over from the previous one, which stays as a current secondary placement until it is ended.

Scopes: placements:read, placements:write

Endpoints

  • GET /api/v1/placements List
  • GET /api/v1/placements/{id} One record
  • POST /api/v1/placements Create
  • DELETE /api/v1/placements/{id} Delete
  • POST /api/v1/placements/{id}/end Ends the placement on valid_to (not before it began).
  • POST /api/v1/placements/{id}/make-primary Makes it the person’s main placement; the previous main one becomes secondary.

Fields

NameTypeDescription
idintegerread only Identifier.
employee_idintegerrequired The person.
work_unit_idintegerrequired The unit.
department_idinteger | null The department.
position_idinteger | null The position.
is_primaryboolean Whether it is the person’s main placement.
valid_fromdaterequired First day.
valid_todate | null Last day (empty: still current).
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

employee_idintegerOnly this person’s.
work_unit_idintegerOnly in this unit.
currentbooleantrue: only placements in force today.

/api/v1/placements/{id}/end: valid_to (date) — The last day of the placement (Y-m-d).

Example request

POST https://compania-mea.harmoniq.ro/api/v1/placements

{
    "employee_id": 345,
    "work_unit_id": 12,
    "is_primary": true,
    "valid_from": "2026-03-01"
}

Example response

{
    "data": {
        "id": 901,
        "employee_id": 345,
        "work_unit_id": 12,
        "department_id": 1,
        "position_id": 1,
        "is_primary": true,
        "valid_from": "2026-03-01",
        "valid_to": "2026-05-01",
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Leave types /leave-types

The kinds of leave the company uses, as configured in the app. Read-only.

Scopes: leave_types:read

Endpoints

  • GET /api/v1/leave-types List
  • GET /api/v1/leave-types/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier.
codestring | nullread only Short code.
namestringread only Name, in the language of the request (Accept-Language), falling back to the company’s language.
uses_balancebooleanread only Whether days count against a yearly balance.
default_entitlementnumber | nullread only Days a year for a full year’s work.
is_paidbooleanread only Paid leave.
requires_attachmentbooleanread only A document must be attached (requests needing one are filed in the app).
requires_referencebooleanread only A reference number is required.
requires_replacementbooleanread only A replacement colleague is required.
is_activebooleanread only Whether it can be requested.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

is_activebooleantrue or false.

Example response

{
    "data": {
        "id": 1,
        "code": "CO",
        "name": "Concediu de odihnă",
        "uses_balance": true,
        "default_entitlement": 21,
        "is_paid": true,
        "requires_attachment": true,
        "requires_reference": true,
        "requires_replacement": true,
        "is_active": true,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Leave requests /leave-requests

Leave requests, through the same rules as the app: the working days are counted, the balance and overlapping requests are checked, and the company’s approval flow starts. Approving and rejecting follow the request’s flow: the key’s creator must be an approver of the current step.

Scopes: leave_requests:read, leave_requests:write

Endpoints

  • GET /api/v1/leave-requests List
  • GET /api/v1/leave-requests/{id} One record
  • POST /api/v1/leave-requests Create
  • POST /api/v1/leave-requests/{id}/approve Approves the current step (comment optional).
  • POST /api/v1/leave-requests/{id}/reject Rejects the request; comment (the reason) required.
  • POST /api/v1/leave-requests/{id}/cancel Cancels a pending or approved request.

Fields

NameTypeDescription
idintegerread only Identifier.
employee_idintegerrequired The person on leave.
leave_type_idintegerrequired The kind of leave.
starts_ondaterequired First day.
ends_ondaterequired Last day.
daysnumberread only Days counted (working days, holidays excluded).
statuspending | approved | rejected | cancelledread only pending, approved, rejected or cancelled.
replacement_employee_idinteger | null The colleague who covers.
referencestring | null Reference number (for example a medical certificate).
notetext | null Note.
created_atdatetimeread only Filed.
updated_atdatetimeread only Last changed.

Filters

employee_idintegerOnly this person’s.
statuspending | approved | rejected | cancelledOnly with this status.
fromdateTouching this day or later.
todateTouching this day or earlier.

/api/v1/leave-requests/{id}/approve: comment (text) — Optional comment for the person.

/api/v1/leave-requests/{id}/reject: comment (text) — The reason, shown to the person (required).

Example request

POST https://compania-mea.harmoniq.ro/api/v1/leave-requests

{
    "employee_id": 345,
    "leave_type_id": 1,
    "starts_on": "2026-07-06",
    "ends_on": "2026-07-10"
}

Example response

{
    "data": {
        "id": 77,
        "employee_id": 345,
        "leave_type_id": 1,
        "starts_on": "2026-07-06",
        "ends_on": "2026-07-10",
        "days": 5,
        "status": "pending",
        "replacement_employee_id": 1,
        "reference": null,
        "note": null,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Leave balances /leave-balances

Each person’s yearly balance per leave type that counts days. Listing a year opens it for every active person, pro rata and with carry-over, as the app does; used, pending and available are computed. The entitlement, adjustment and carry-over can be changed — never for the key creator’s own record.

Scopes: leave_balances:read, leave_balances:write

Endpoints

  • GET /api/v1/leave-balances List
  • GET /api/v1/leave-balances/{id} One record
  • PATCH /api/v1/leave-balances/{id} Change

Fields

NameTypeDescription
idintegerread only Identifier.
employee_idintegerread only The person.
leave_type_idintegerread only The leave type.
yearintegerread only The year.
entitlementnumber Days for the year (0–365).
adjustmentnumber Days added or taken (−365–365).
carried_overnumber Days carried over from last year.
carried_over_expires_ondate | null Until when the carried days can be used.
carried_usablenumberread only Carried days still usable today.
earliernumberread only Days still left from the years before the previous one, usable today.
earlier_expires_ondate | nullread only Until when the earliest of those days can be taken.
usednumberread only Days taken (approved).
pendingnumberread only Days asked, waiting for approval.
availablenumberread only Days left.
notestring | null Note.
created_atdatetimeread only Opened.
updated_atdatetimeread only Last changed.

Filters

yearintegerThe year (default: this year).
employee_idintegerOnly this person.
leave_type_idintegerOnly this leave type.

Example response

{
    "data": {
        "id": 55,
        "employee_id": 345,
        "leave_type_id": 1,
        "year": 2026,
        "entitlement": 21,
        "adjustment": 0,
        "carried_over": 3,
        "carried_over_expires_on": "2026-03-31",
        "carried_usable": 3,
        "earlier": 0,
        "earlier_expires_on": "2026-05-01",
        "used": 5,
        "pending": 2,
        "available": 17,
        "note": null,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Calendar events /calendar-events

The company calendar. Participants (or the unit) are notified of a new event, a new time or a new place. A new event may repeat: each occurrence becomes its own event in one series and is changed, cancelled or deleted on its own.

Scopes: calendar_events:read, calendar_events:write

Endpoints

  • GET /api/v1/calendar-events List
  • GET /api/v1/calendar-events/{id} One record
  • POST /api/v1/calendar-events Create
  • PATCH /api/v1/calendar-events/{id} Change
  • DELETE /api/v1/calendar-events/{id} Delete
  • POST /api/v1/calendar-events/{id}/cancel Cancels this occurrence; participants are told.

Fields

NameTypeDescription
idintegerread only Identifier.
event_type_idinteger | null Event type.
titlestringrequired Title.
descriptiontext | null Description (HTML; cleaned like any text written in the app).
locationstring | null Place.
starts_atdatetimerequired Start (ISO 8601).
ends_atdatetimerequired End (ISO 8601).
all_dayboolean All day.
visibilityeveryone | unit | participantsrequired everyone, unit (the unit’s people) or participants.
work_unit_idinteger | null The unit, for visibility unit.
participantsarray Employee ids invited.
rsvpboolean Asks who is coming.
recurrenceweekly | biweekly | monthly | yearly | null On create only: weekly, biweekly, monthly or yearly.
recurrence_untildate | null On create only: the last day of the series (at most two years ahead).
series_idinteger | nullread only The series it belongs to.
cancelled_atdatetime | nullread only When it was cancelled.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

fromdateEnding on this day or later.
todateStarting on this day or earlier.
work_unit_idintegerOnly this unit’s.
cancelledbooleantrue: only cancelled ones; false: only the rest.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/calendar-events

{
    "title": "Inventar trimestrial",
    "description": "<p>Toată echipa.</p>",
    "starts_at": "2026-07-06T15:00:00Z",
    "ends_at": "2026-07-06T17:00:00Z",
    "visibility": "everyone",
    "participants": [
        345,
        346
    ]
}

Example response

{
    "data": {
        "id": 410,
        "event_type_id": 1,
        "title": "Inventar trimestrial",
        "description": "<p>Toată echipa.</p>",
        "location": null,
        "starts_at": "2026-07-06T15:00:00Z",
        "ends_at": "2026-07-06T17:00:00Z",
        "all_day": true,
        "visibility": "everyone",
        "work_unit_id": 1,
        "participants": [
            345,
            346
        ],
        "rsvp": true,
        "recurrence": "weekly",
        "recurrence_until": "2026-05-01",
        "series_id": 1,
        "cancelled_at": "2026-05-01T08:00:00Z",
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Performance

KPI metrics /kpi-metrics

What the company measures: each metric’s key, unit, the subjects it is measured for (grain) and how often (period). Read only; metrics are defined in the app. Values can be sent through the API only to metrics whose source is “api”.

Scopes: kpi_metrics:read

Endpoints

  • GET /api/v1/kpi-metrics List
  • GET /api/v1/kpi-metrics/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier.
keystringread only The metric’s key, used in formulas and when sending values.
labelstringread only Name, in the company’s language.
descriptionstring | nullread only What it measures.
unitcurrency | percent | count | score | days | hours | ratioread only Unit: currency, percent, count, score, days, hours or ratio.
value_typedecimal | integer | percent | booleanread only Kind of value: decimal, integer, percent or boolean.
grainemployee | team | department | work_unit | geo_area | companyread only Whom it is measured for: employee, team, department, work_unit, geo_area or company.
periodday | week | month | quarter | yearread only How often: day, week, month, quarter or year.
aggregationsum | avg | last | min | max | countread only How values add up over a longer period.
source_typemanual | import | module | derived | api | objective | reviewread only Where its values come from (manual, import, api, a module…).
is_higher_betterbooleanread only Whether a higher value is better.
is_activebooleanread only Whether it is in use.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

keystringExact key (case-insensitive).
source_typemanual | import | module | derived | api | objective | reviewOnly metrics with this source.
is_activebooleantrue or false.

Example response

{
    "data": {
        "id": 7,
        "key": "sales.actual",
        "label": "Sales",
        "description": null,
        "unit": "currency",
        "value_type": "decimal",
        "grain": "employee",
        "period": "month",
        "aggregation": "sum",
        "source_type": "api",
        "is_higher_better": true,
        "is_active": true,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

KPI values /metric-values

The value of a metric for one subject in one period. Creating one writes it (or replaces the value already there for that subject and period); only metrics whose source is “api” accept values. A missing value (null) means “no data”, never zero. To send many values by business reference (employee number, unit code) use POST /metrics/{key}/values.

Scopes: metric_values:read, metric_values:write

Endpoints

  • GET /api/v1/metric-values List
  • GET /api/v1/metric-values/{id} One record
  • POST /api/v1/metric-values Create

Fields

NameTypeDescription
idintegerread only Identifier.
metricstringrequired The metric’s key.
subject_typeemployee | team | department | work_unit | geo_area | companyread only The kind of subject (the metric’s grain).
subject_idintegerrequired The subject’s id (an employee, unit, department, area or team id; 0 for the company).
periodstringrequired The period’s key: 2026-09 (month), 2026-W38 (week), 2026-Q3, 2026, 2026-09-14 (day).
period_startdateread only First day of the period.
period_enddateread only Last day of the period.
valuescalar | null The value, as a number or text (“1234.5”, “1.234,5”, “87%”, true/false); null for no data. Stored as a decimal string.
sourcemanual | import | module | derived | api | objective | reviewread only Where it came from.
recorded_atdatetimeread only When it was written.

Filters

metricstringOnly this metric’s values (its key).
periodstringOnly this period (needs metric).
subject_idintegerOnly this subject.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/metric-values

{
    "metric": "sales.api",
    "subject_id": 345,
    "period": "2026-09",
    "value": "1234.5"
}

Example response

{
    "data": {
        "id": 5120,
        "metric": "sales.api",
        "subject_type": "employee",
        "subject_id": 345,
        "period": "2026-09",
        "period_start": "2026-09-01",
        "period_end": "2026-09-30",
        "value": "1234.5",
        "source": "api",
        "recorded_at": "2026-05-01T08:00:00Z"
    }
}

Scorecards /kpi-scorecards

Scorecards and every version of them (draft, published, archived): the parts a score is made of, their weights, and the levels a score falls into. Read only; scorecards are built in the app, and a published version never changes.

Scopes: kpi_scorecards:read

Endpoints

  • GET /api/v1/kpi-scorecards List
  • GET /api/v1/kpi-scorecards/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier of this version.
familystringread only The scorecard across its versions.
versionintegerread only Version number.
namestringread only Name.
descriptionstring | nullread only Description.
grainemployee | team | department | work_unit | geo_area | companyread only Whom it scores.
periodday | week | month | quarter | yearread only How often it scores.
statusdraft | published | archivedread only draft, published or archived.
effective_fromdate | nullread only In force from.
effective_todate | nullread only In force until.
published_atdatetime | nullread only When this version was published.
componentsarrayread only Its parts: key, label, weight (0–1), target, display format, whether the employee sees it.
levelsarrayread only Its levels: name, the lowest score that reaches it, colour.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

statusdraft | published | archivedOnly versions with this status.
familystringOnly the versions of this scorecard.

Example response

{
    "data": {
        "id": 4,
        "family": "9b2e0c1e-0f0a-4b47-9f3a-2c1d7e8a1b20",
        "version": 2,
        "name": "Pharmacist performance",
        "description": null,
        "grain": "employee",
        "period": "month",
        "status": "published",
        "effective_from": "2026-05-01",
        "effective_to": "2026-05-01",
        "published_at": "2026-05-01T08:00:00Z",
        "components": [
            {
                "key": "sales",
                "label": "Sales achievement",
                "weight": "0.55"
            }
        ],
        "levels": [
            {
                "name": "Good",
                "min_score": "85",
                "color": "#2B6CB0"
            }
        ],
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

KPI results /kpi-results

Scores on the scorecards: the current result of each subject and period, the level it reaches, and each part’s value and points. A total or part without data is null, never zero. Read only; results are produced when a period is closed.

Scopes: kpi_results:read

Endpoints

  • GET /api/v1/kpi-results List
  • GET /api/v1/kpi-results/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier.
scorecard_idintegerread only The scorecard version that scored it.
scorecardstringread only Its name.
subject_typeemployee | team | department | work_unit | geo_area | companyread only The kind of subject.
subject_idintegerread only The subject’s id.
subjectstring | nullread only The subject’s name.
perioddateread only First day of the period.
totalstring | nullread only The score, as a decimal string; null when there is no data.
levelstring | nullread only The level reached.
level_colorstring | nullread only The level’s colour.
bonusstring | nullread only The bonus, when the scorecard computes one.
partsarrayread only Each part: key, label, value and the points it brings (decimal strings; null for no data).
created_atdatetimeread only When it was computed.

Filters

scorecard_idintegerOnly this scorecard version.
perioddateFirst day of the period (Y-m-d).
subject_typeemployee | team | department | work_unit | geo_area | companyOnly this kind of subject.
subject_idintegerOnly this subject.

Example response

{
    "data": {
        "id": 9811,
        "scorecard_id": 4,
        "scorecard": "Pharmacist performance",
        "subject_type": "employee",
        "subject_id": 345,
        "subject": "Ana Popescu",
        "period": "2026-09-01",
        "total": "92.40",
        "level": "Good",
        "level_color": "#2B6CB0",
        "bonus": null,
        "parts": [
            {
                "key": "sales",
                "label": "Sales achievement",
                "value": "104.2",
                "points": "57.31"
            }
        ],
        "created_at": "2026-05-01T08:00:00Z"
    }
}

Objectives /objectives

Objectives under a rule (for example “focus products”): their period, parameters and unit targets. The rule and period are fixed once created. The record action stores what people or units achieved; they are scored when the period is scored. An objective already scored cannot be deleted.

Scopes: objectives:read, objectives:write

Endpoints

  • GET /api/v1/objectives List
  • GET /api/v1/objectives/{id} One record
  • POST /api/v1/objectives Create
  • PATCH /api/v1/objectives/{id} Change
  • DELETE /api/v1/objectives/{id} Delete
  • POST /api/v1/objectives/{id}/record Record what subjects achieved: their measures replace what was recorded before; empty measures forget it (no data again).

Fields

NameTypeDescription
idintegerread only Identifier.
rule_idintegerrequired The rule that scores it (create only).
rulestringread only The rule’s name.
namestringrequired Name.
descriptiontext | null Description.
periodstringrequired The period’s key, in the rule’s period (create only), for example 2026-09.
period_startdateread only First day.
period_enddateread only Last day.
paramsobject Values for the rule’s parameters, by name (others are ignored); omitted: the rule’s defaults.
targetsarray Unit targets: a list of {work_unit_id, target}. When there are targets, the objective is only for those units.
external_refstring | null Your own reference (create only).
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

rule_idintegerOnly under this rule.
period_startdateFirst day of the period (Y-m-d).
external_refstringYour exact reference.

/api/v1/objectives/{id}/record: achievements (array) — A list of {subject_id, measures: {measure name: number}}; subjects at the rule’s grain, in the target units.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/objectives

{
    "rule_id": 2,
    "name": "Vitamin C 1000",
    "period": "2026-09",
    "params": {
        "bonus": "2"
    },
    "targets": [
        {
            "work_unit_id": 12,
            "target": "30"
        }
    ],
    "external_ref": "CAMP-2026-09"
}

Example response

{
    "data": {
        "id": 31,
        "rule_id": 2,
        "rule": "Focus products",
        "name": "Vitamin C 1000",
        "description": null,
        "period": "2026-09",
        "period_start": "2026-09-01",
        "period_end": "2026-09-30",
        "params": {
            "bonus": "2"
        },
        "targets": [
            {
                "work_unit_id": 12,
                "target": "30"
            }
        ],
        "external_ref": "CAMP-2026-09",
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Evaluations /reviews

Evaluations given on the grids: whom, by whom, for which period, the points and result, and each criterion’s answer. Read only; evaluations are given in the app. Drafts are the evaluator’s work in progress and are never listed.

Scopes: reviews:read

Endpoints

  • GET /api/v1/reviews List
  • GET /api/v1/reviews/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier.
form_idintegerread only The grid version.
formstringread only The grid’s name.
employee_idintegerread only The person evaluated.
evaluator_idinteger | nullread only The login of who evaluated.
period_startdateread only First day of the period.
period_enddateread only Last day.
statussubmitted | cancelledread only submitted or cancelled.
total_pointsstring | nullread only Points obtained.
max_pointsstring | nullread only Points possible.
resultstring | nullread only The result (points, or a percentage when the grid gives one).
commenttext | nullread only The evaluator’s comment, as plain text.
submitted_atdatetime | nullread only When it was submitted.
cancelled_atdatetime | nullread only When it was cancelled.
answersarrayread only Each criterion: key, the options chosen, a value, its points and a comment.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

statussubmitted | cancelledsubmitted or cancelled.
employee_idintegerOnly this person.
form_idintegerOnly this grid version.
period_startdateFirst day of the period (Y-m-d).

Example response

{
    "data": {
        "id": 812,
        "form_id": 3,
        "form": "Monthly evaluation",
        "employee_id": 345,
        "evaluator_id": 12,
        "period_start": "2026-09-01",
        "period_end": "2026-09-30",
        "status": "submitted",
        "total_points": "8.00",
        "max_points": "10.00",
        "result": "80",
        "comment": null,
        "submitted_at": "2026-05-01T08:00:00Z",
        "cancelled_at": "2026-05-01T08:00:00Z",
        "answers": [
            {
                "criterion_key": "attitude",
                "selected": [
                    "teamwork"
                ],
                "value": null,
                "points": "2.00",
                "comment": null
            }
        ],
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Training, audits and health & safety

Courses /courses

The course catalogue. Title and description are written in the company’s language (other languages are translated in the app); chapters and covers are written in the app. A course can be deleted only while nobody was ever assigned it — archive it otherwise.

Scopes: courses:read, courses:write

Endpoints

  • GET /api/v1/courses List
  • GET /api/v1/courses/{id} One record
  • POST /api/v1/courses Create
  • PATCH /api/v1/courses/{id} Change
  • DELETE /api/v1/courses/{id} Delete

Fields

NameTypeDescription
idintegerread only Identifier.
titlestringrequired Title.
descriptiontext | null Short description, plain text.
category_idinteger | null Category.
categorystring | nullread only Category name.
authorstring | null Author.
statusdraft | published | archivedrequired draft, published or archived; only published courses can be assigned.
estimated_minutesinteger | null Estimated duration in minutes.
validity_monthsinteger | null How many months the certificate stays valid; empty: no expiry.
issues_certificateboolean Whether finishing it issues a certificate.
recertifiesboolean Whether people are re-enrolled before the certificate expires.
pointsintegerrequired Points earned on finishing.
chapters_countintegerread only Number of chapters.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

statusdraft | published | archivedOnly with this status.
category_idintegerOnly in this category.
qstringPart of the title.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/courses

{
    "title": "Siguranța la incendiu",
    "status": "published",
    "estimated_minutes": 20,
    "validity_months": 12,
    "issues_certificate": true,
    "recertifies": false,
    "points": 10
}

Example response

{
    "data": {
        "id": 7,
        "title": "Siguranța la incendiu",
        "description": null,
        "category_id": 1,
        "category": "Obligatorii",
        "author": null,
        "status": "published",
        "estimated_minutes": 20,
        "validity_months": 12,
        "issues_certificate": true,
        "recertifies": false,
        "points": 10,
        "chapters_count": 4,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Learning assignments /learning-assignments

A published course (or a standalone test) given to people: everyone, a unit, an area, a department, a position or named employees. The people are enrolled at once. Deleted only while nobody has started.

Scopes: learning_assignments:read, learning_assignments:write

Endpoints

  • GET /api/v1/learning-assignments List
  • GET /api/v1/learning-assignments/{id} One record
  • POST /api/v1/learning-assignments Create
  • DELETE /api/v1/learning-assignments/{id} Delete

Fields

NameTypeDescription
idintegerread only Identifier.
course_idinteger | null The course (or test_id).
test_idinteger | null A standalone test (or course_id).
available_fromdaterequired From when it can be taken.
due_ondate | null Due date.
enrolls_newcomersboolean Whether people who later match the targets are enrolled automatically.
targetsarrayrequired Who: a list of {type, id}; type is all, employee, position, work_unit, department or geo_area (id empty for all).
enrollments_countintegerread only People enrolled.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

course_idintegerOnly for this course.
test_idintegerOnly for this test.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/learning-assignments

{
    "course_id": 7,
    "available_from": "2026-05-01",
    "due_on": "2026-05-31",
    "enrolls_newcomers": true,
    "targets": [
        {
            "type": "work_unit",
            "id": 12
        }
    ]
}

Example response

{
    "data": {
        "id": 31,
        "course_id": 7,
        "test_id": 1,
        "available_from": "2026-05-01",
        "due_on": "2026-05-31",
        "enrolls_newcomers": true,
        "targets": [
            {
                "type": "work_unit",
                "id": 12
            }
        ],
        "enrollments_count": 14,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Enrolments /enrollments

Who is enrolled in which course or test, and how far they got. Actions: a later due date, one more attempt, excusing someone who has not started.

Scopes: enrollments:read, enrollments:write

Endpoints

  • GET /api/v1/enrollments List
  • GET /api/v1/enrollments/{id} One record
  • POST /api/v1/enrollments/{id}/extend Set a later due date.
  • POST /api/v1/enrollments/{id}/grant-attempt Allow one more test attempt (not once passed).
  • POST /api/v1/enrollments/{id}/excuse Excuse the person (only before they start).

Fields

NameTypeDescription
idintegerread only Identifier.
assignment_idinteger | nullread only The assignment it came from.
employee_idintegerread only The person.
course_idinteger | nullread only The course.
test_idinteger | nullread only The test.
available_fromdate | nullread only Available from.
due_ondate | nullread only Due date.
statusassigned | in_progress | awaiting_grading | completed | failed | excusedread only assigned, in_progress, awaiting_grading, completed, failed or excused.
started_atdatetime | nullread only Started.
completed_atdatetime | nullread only Finished.
scorenumber | nullread only Test score (%).
passedboolean | nullread only Whether the test was passed.
extra_attemptsintegerread only Extra attempts granted.
is_overduebooleanread only Past due and not finished.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

employee_idintegerOnly this person.
course_idintegerOnly this course.
test_idintegerOnly this test.
assignment_idintegerOnly this assignment.
statusassigned | in_progress | awaiting_grading | completed | failed | excusedOnly with this status.

/api/v1/enrollments/{id}/extend: due_on (date) — The new due date, not before the enrolment opens.

Example response

{
    "data": {
        "id": 501,
        "assignment_id": 31,
        "employee_id": 345,
        "course_id": 7,
        "test_id": 1,
        "available_from": "2026-05-01",
        "due_on": "2026-05-31",
        "status": "completed",
        "started_at": "2026-05-01T08:00:00Z",
        "completed_at": "2026-05-01T08:00:00Z",
        "score": 90,
        "passed": true,
        "extra_attempts": 0,
        "is_overdue": false,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Certificates /certificates

Certificates earned on finishing a course, with the code anyone can check on the certificate page.

Scopes: certificates:read

Endpoints

  • GET /api/v1/certificates List
  • GET /api/v1/certificates/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier.
enrollment_idintegerread only The enrolment that earned it.
employee_idintegerread only The person.
course_idintegerread only The course.
numberstringread only Certificate number.
verification_codestringread only The public verification code.
issued_ondateread only Issued on.
expires_ondate | nullread only Valid until; empty: no expiry.
is_validbooleanread only Valid today.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

employee_idintegerOnly this person.
course_idintegerOnly this course.

Example response

{
    "data": {
        "id": 88,
        "enrollment_id": 501,
        "employee_id": 345,
        "course_id": 7,
        "number": "FV-2026-0042",
        "verification_code": "K7Q2M9XA",
        "issued_on": "2026-05-12",
        "expires_on": "2027-05-12",
        "is_valid": true,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Audits /audits

Audits of the units with their findings, item by item. Read only: an audit is conducted in the app.

Scopes: audits:read

Endpoints

  • GET /api/v1/audits List
  • GET /api/v1/audits/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier.
work_unit_idintegerread only The unit audited.
template_namestringread only The checklist used.
auditor_idinteger | nullread only Who conducted it (user).
statusdraft | completedread only draft or completed.
conducted_atdatetime | nullread only When it took place.
completed_atdatetime | nullread only When it was completed.
scorenumber | nullread only Score (%).
pass_scorenumber | nullread only Pass mark (%).
passedboolean | nullread only Passed (completed audits only).
critical_findingsintegerread only Critical items not met.
is_self_checkbooleanread only Conducted by someone working in or managing the unit (not counted in audit KPIs).
notetext | nullread only Notes.
findingsarrayread only Each item: {id, section, item, weight, critical, result (ok, not_ok, na), comment}.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

work_unit_idintegerOnly this unit.
statusdraft | completedOnly with this status.
conducted_fromdateConducted on or after this date.
conducted_todateConducted on or before this date.

Example response

{
    "data": {
        "id": 19,
        "work_unit_id": 12,
        "template_name": "Verificare lunară farmacie",
        "auditor_id": 1,
        "status": "completed",
        "conducted_at": "2026-05-01T08:00:00Z",
        "completed_at": "2026-05-01T08:00:00Z",
        "score": 88.89,
        "pass_score": 80,
        "passed": true,
        "critical_findings": 0,
        "is_self_check": true,
        "note": null,
        "findings": [
            {
                "section": "Frigider",
                "item": "Termometrul este calibrat",
                "weight": 2,
                "critical": true,
                "result": "not_ok",
                "comment": "Ultima calibrare în martie."
            }
        ],
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Audit action plans /audit-action-plans

Corrective actions set after audits. They move on as on the audit page: marked done with a note, verified, reopened with a reason, or cancelled.

Scopes: audit_action_plans:read, audit_action_plans:write

Endpoints

  • GET /api/v1/audit-action-plans List
  • GET /api/v1/audit-action-plans/{id} One record
  • POST /api/v1/audit-action-plans/{id}/mark-done Mark an open action done.
  • POST /api/v1/audit-action-plans/{id}/verify Accept a done action.
  • POST /api/v1/audit-action-plans/{id}/reopen Send a done action back, with the reason.
  • POST /api/v1/audit-action-plans/{id}/cancel Cancel an open or done action.

Fields

NameTypeDescription
idintegerread only Identifier.
audit_idintegerread only The audit.
result_idinteger | nullread only The finding it answers.
descriptiontextread only What must be done.
responsible_idinteger | nullread only Who must do it (user).
due_ondateread only Due date.
statusopen | done | verified | cancelledread only open, done, verified or cancelled.
is_overduebooleanread only Open and past due.
done_notetext | nullread only What was done (and why it was reopened).
done_atdatetime | nullread only Marked done.
verified_byinteger | nullread only Who verified it (user).
verified_atdatetime | nullread only Verified.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

audit_idintegerOnly for this audit.
statusopen | done | verified | cancelledOnly with this status.
responsible_idintegerOnly for this person.

/api/v1/audit-action-plans/{id}/mark-done: note (text) — What was done.

/api/v1/audit-action-plans/{id}/reopen: reason (text) — Why it is not accepted.

Example response

{
    "data": {
        "id": 4,
        "audit_id": 19,
        "result_id": 1,
        "description": "Calibrați termometrul frigiderului și păstrați certificatul.",
        "responsible_id": 1,
        "due_on": "2026-05-20",
        "status": "open",
        "is_overdue": true,
        "done_note": null,
        "done_at": "2026-05-01T08:00:00Z",
        "verified_by": 1,
        "verified_at": "2026-05-01T08:00:00Z",
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Health & safety types /ssm-types

The kinds of health and safety training and checks the company records, with how long each is valid.

Scopes: ssm_types:read

Endpoints

  • GET /api/v1/ssm-types List
  • GET /api/v1/ssm-types/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier.
namestringread only Name.
validity_monthsinteger | nullread only Months it stays valid; empty: no expiry.
is_requiredbooleanread only Required for everyone.
is_activebooleanread only In use.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

is_activebooleantrue or false.

Example response

{
    "data": {
        "id": 2,
        "name": "Instruire periodică",
        "validity_months": 6,
        "is_required": true,
        "is_active": true,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Health & safety records /ssm-records

Who had which health and safety training or check, when, and until when it is valid (computed from the type). One record per call.

Scopes: ssm_records:read, ssm_records:write

Endpoints

  • GET /api/v1/ssm-records List
  • GET /api/v1/ssm-records/{id} One record
  • POST /api/v1/ssm-records Create
  • DELETE /api/v1/ssm-records/{id} Delete

Fields

NameTypeDescription
idintegerread only Identifier.
employee_idintegerrequired The person.
type_idintegerrequired The training or check.
done_ondaterequired When it was done (not in the future).
valid_untildate | nullread only Valid until (from the type).
statevalid | expiring | expired | missing | noneread only Today: valid, expiring, expired.
trainerstring | null Trainer or doctor.
notestring | null Notes.
recorded_byinteger | nullread only Who recorded it (user).
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

employee_idintegerOnly this person.
type_idintegerOnly this type.
valid_until_beforedateValid until this date or earlier.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/ssm-records

{
    "employee_id": 345,
    "type_id": 2,
    "done_on": "2026-04-15"
}

Example response

{
    "data": {
        "id": 120,
        "employee_id": 345,
        "type_id": 2,
        "done_on": "2026-04-15",
        "valid_until": "2026-10-15",
        "state": "valid",
        "trainer": null,
        "note": null,
        "recorded_by": 1,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Documents, requests, surveys and communication

Document categories /document-categories

The categories documents are filed under, for the whole company or for units.

Scopes: document_categories:read

Endpoints

  • GET /api/v1/document-categories List
  • GET /api/v1/document-categories/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier.
namestringread only Name, in the language asked for.
scopecompany | unitread only company (company documents) or unit (unit documents).
sortintegerread only Position in lists.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

scopecompany | unitOnly company or only unit categories.

Example response

{
    "data": {
        "id": 4,
        "name": "Autorizații",
        "scope": "company",
        "sort": 1,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Documents /documents

Permits, contracts and rules, with their expiry. The API reads a document’s details and its current file’s name, size and type; the file itself is downloaded in the app. A file is sent as {"name", "content_base64"} (the same types as in the app — PDF, Office, images, ZIP — at most 50 MB); sending a file to an existing document adds a new version. Deleting moves the document to the trash, where it can be restored in the app.

Scopes: documents:read, documents:write

Endpoints

  • GET /api/v1/documents List
  • GET /api/v1/documents/{id} One record
  • POST /api/v1/documents Create
  • PATCH /api/v1/documents/{id} Change
  • DELETE /api/v1/documents/{id} Delete

Fields

NameTypeDescription
idintegerread only Identifier.
titlestringrequired Title.
descriptiontext | null Description.
category_idinteger | null Category (see /document-categories).
work_unit_idinteger | null The unit it belongs to; empty for a company document. Set on create only.
is_internalboolean Seen only by who manages documents.
expires_ondate | null Expiry date.
expiry_statevalid | expiring | expired | nullread only valid, expiring or expired (empty without an expiry date).
repeat_monthsinteger | null Renewed every this many months.
alert_daysarray | null Days before expiry when reminders go out (at most 6).
versionsintegerread only How many versions of the file there are.
file_namestring | nullread only The current file’s name.
file_sizeinteger | nullread only Its size in bytes.
file_mimestring | nullread only Its type.
filefilerequired The file: {"name": "…", "content_base64": "…"}. Required on create; on update it adds a version.
version_notestring | null A note on the file’s version (shown: the current version’s).
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

work_unit_idintegerOnly this unit’s documents.
category_idintegerOnly this category.
companybooleantrue: company documents only; false: unit documents only.
expires_beforedateExpiring on or before this date.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/documents

{
    "title": "Autorizație sanitară de funcționare",
    "category_id": 4,
    "work_unit_id": 12,
    "is_internal": false,
    "expires_on": "2026-12-31",
    "repeat_months": 12,
    "alert_days": [
        30,
        7
    ],
    "file": {
        "name": "document.pdf",
        "content_base64": "JVBERi0xLjQK…"
    }
}

Example response

{
    "data": {
        "id": 31,
        "title": "Autorizație sanitară de funcționare",
        "description": null,
        "category_id": 4,
        "work_unit_id": 12,
        "is_internal": false,
        "expires_on": "2026-12-31",
        "expiry_state": "valid",
        "repeat_months": 12,
        "alert_days": [
            30,
            7
        ],
        "versions": 2,
        "file_name": "autorizatie.pdf",
        "file_size": 184220,
        "file_mime": "application/pdf",
        "version_note": null,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Service categories /service-categories

What service requests are about (maintenance, IT…), and whether they need approval first.

Scopes: service_categories:read

Endpoints

  • GET /api/v1/service-categories List
  • GET /api/v1/service-categories/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier.
namestringread only Name, in the language asked for.
is_activebooleanread only Whether requests can be filed under it.
needs_approvalbooleanread only Whether a request waits for approval before it opens.
sortintegerread only Position in lists.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

is_activebooleantrue or false.

Example response

{
    "data": {
        "id": 2,
        "name": "Mentenanță și reparații",
        "is_active": true,
        "needs_approval": false,
        "sort": 1,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Service requests /service-requests

Requests to fix or provide something in a unit, with their history. Filed for the person given in requested_by (who must be able to file requests for that unit), or for the key’s creator; the category’s approval flow applies. Moved along with the move action, as the key’s creator may on the screen.

Scopes: service_requests:read, service_requests:write

Endpoints

  • GET /api/v1/service-requests List
  • GET /api/v1/service-requests/{id} One record
  • POST /api/v1/service-requests Create
  • POST /api/v1/service-requests/{id}/move Move the request to another status (in_progress, resolved, unresolved, open, cancelled), as the key’s creator may. Unresolved and reopening need a comment.

Fields

NameTypeDescription
idintegerread only Identifier.
category_idintegerrequired Category (see /service-categories).
work_unit_idintegerrequired The unit it is for.
titlestringrequired Short title.
descriptiontextrequired What is wrong or needed.
requested_byinteger The user it is filed for (a person of the company); the key’s creator when left out.
employee_idintegerread only The employee record of who filed it.
statusawaiting_approval | open | in_progress | resolved | unresolved | rejected | cancelledread only awaiting_approval, open, in_progress, resolved, unresolved, rejected or cancelled.
assigned_tointeger | nullread only The user who took it.
closed_atdatetime | nullread only When it was closed.
historyarrayread only Every move: from, to, comment, by whom, when.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

statusawaiting_approval | open | in_progress | resolved | unresolved | rejected | cancelledOnly with this status.
category_idintegerOnly this category.
work_unit_idintegerOnly this unit.

/api/v1/service-requests/{id}/move: status (awaiting_approval | open | in_progress | resolved | unresolved | rejected | cancelled) — The new status.; comment (text) — Why (required for unresolved and when reopening).

Example request

POST https://compania-mea.harmoniq.ro/api/v1/service-requests

{
    "category_id": 2,
    "work_unit_id": 12,
    "title": "Frigiderul de vaccinuri nu mai răcește",
    "description": "text",
    "requested_by": 17
}

Example response

{
    "data": {
        "id": 88,
        "category_id": 2,
        "work_unit_id": 12,
        "title": "Frigiderul de vaccinuri nu mai răcește",
        "description": "text",
        "requested_by": 17,
        "employee_id": 345,
        "status": "open",
        "assigned_to": 1,
        "closed_at": "2026-05-01T08:00:00Z",
        "history": [
            {
                "status_from": "open",
                "status_to": "in_progress",
                "comment": "Vine tehnicianul mâine.",
                "user_id": 9,
                "at": "2026-05-04T08:00:00Z"
            }
        ],
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Materials /materials

What units can ask for, with the stock held now when it is tracked.

Scopes: materials:read

Endpoints

  • GET /api/v1/materials List
  • GET /api/v1/materials/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier.
namestringread only Name.
descriptiontext | nullread only Description.
unitstringread only Unit of measure.
track_stockbooleanread only Whether stock is tracked.
stocknumber | nullread only Stock now (empty when not tracked).
is_activebooleanread only Whether it can be requested.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

is_activebooleantrue or false.

Example response

{
    "data": {
        "id": 7,
        "name": "Mănuși nitril",
        "description": null,
        "unit": "cutie",
        "track_stock": true,
        "stock": 35,
        "is_active": true,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Material requests /material-requests

Requests for materials, with their items. Filed for the person in requested_by (or the key’s creator), for an active material and a unit they may file for; then taken, completed (which takes the stock out) or cancelled, as the key’s creator may on the screen.

Scopes: material_requests:read, material_requests:write

Endpoints

  • GET /api/v1/material-requests List
  • GET /api/v1/material-requests/{id} One record
  • POST /api/v1/material-requests Create
  • POST /api/v1/material-requests/{id}/take Take a new request to handle.
  • POST /api/v1/material-requests/{id}/complete Complete it: the stock of tracked materials goes down.
  • POST /api/v1/material-requests/{id}/cancel Cancel a request not yet completed.

Fields

NameTypeDescription
idintegerread only Identifier.
work_unit_idintegerrequired The unit it is for.
requested_byinteger The user it is filed for; the key’s creator when left out.
statusnew | taken | completed | cancelledread only new, taken, completed or cancelled.
handled_byinteger | nullread only The user handling it.
notetext | null A note.
itemsarrayrequired What is asked for: [{"material_id": 7, "quantity": 5}] (at least one).
completed_atdatetime | nullread only When it was completed.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

statusnew | taken | completed | cancelledOnly with this status.
work_unit_idintegerOnly this unit.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/material-requests

{
    "work_unit_id": 12,
    "requested_by": 17,
    "items": [
        {
            "material_id": 7,
            "quantity": 5
        }
    ]
}

Example response

{
    "data": {
        "id": 51,
        "work_unit_id": 12,
        "requested_by": 17,
        "status": "new",
        "handled_by": 1,
        "note": null,
        "items": [
            {
                "material_id": 7,
                "quantity": 5
            }
        ],
        "completed_at": "2026-05-01T08:00:00Z",
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Surveys /surveys

Published and closed surveys with their questions, and their results as totals only: how many chose each answer, a scale’s average, and how many answered a free-text question — never what they wrote. An anonymous survey’s results stay hidden while its answers are sealed and while it has fewer than 5 answers.

Scopes: surveys:read

Endpoints

  • GET /api/v1/surveys List
  • GET /api/v1/surveys/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier.
titlestringread only Title.
descriptiontext | nullread only Description.
is_anonymousbooleanread only Whether answers are anonymous.
is_mandatorybooleanread only Whether answering is mandatory.
statuspublished | closedread only published or closed.
opens_atdatetime | nullread only When it opens.
closes_atdatetime | nullread only When it closes.
published_atdatetime | nullread only When it was published.
questionsarrayread only The questions: id, type (single, multiple, scale, text), text, required, options.
resultsobjectread only Totals: hidden, sealed, responses, assigned, completed, and per question how many answered, counts per option or scale value, the average.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

statuspublished | closedpublished or closed.
is_anonymousbooleantrue or false.

Example response

{
    "data": {
        "id": 5,
        "title": "Climatul de lucru",
        "description": null,
        "is_anonymous": true,
        "is_mandatory": false,
        "status": "closed",
        "opens_at": "2026-05-01T08:00:00Z",
        "closes_at": "2026-05-01T08:00:00Z",
        "published_at": "2026-05-01T08:00:00Z",
        "questions": [
            {
                "id": 11,
                "type": "single",
                "text": "Ați recomanda compania?",
                "is_required": true,
                "options": [
                    {
                        "id": 31,
                        "label": "Da"
                    }
                ]
            }
        ],
        "results": {
            "hidden": false,
            "responses": 14,
            "questions": [
                {
                    "question_id": 11,
                    "answered": 14,
                    "counts": {
                        "31": 11
                    },
                    "average": null
                }
            ]
        },
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Survey answers /survey-responses

Answers to named surveys, one response per person with its answers. An anonymous survey’s answers are never given one by one — only its totals, in /surveys.

Scopes: survey_responses:read

Endpoints

  • GET /api/v1/survey-responses List
  • GET /api/v1/survey-responses/{id} One record

Fields

NameTypeDescription
idstringread only Identifier (a UUID).
survey_idintegerread only The survey.
employee_idintegerread only Who answered.
submitted_atdatetime | nullread only When.
answersarrayread only The answers: question, chosen option, value (scale) or text.

Filters

survey_idintegerOnly this survey’s answers.
employee_idintegerOnly this person’s answers.

Example response

{
    "data": {
        "id": "9b2f4c1e-6f0a-4e7c-9a51-3c2d1e0f7a88",
        "survey_id": 5,
        "employee_id": 345,
        "submitted_at": "2026-05-01T08:00:00Z",
        "answers": [
            {
                "question_id": 11,
                "option_id": 31,
                "value": null,
                "text": null
            }
        ]
    }
}

Posts /posts

Social posts for the whole company (not in a group), published. Created by the key’s creator: a post, or news, an announcement, a promotion or a contest when they may announce; the body is plain text, a blank line between paragraphs.

Scopes: posts:read, posts:write

Endpoints

  • GET /api/v1/posts List
  • GET /api/v1/posts/{id} One record
  • POST /api/v1/posts Create

Fields

NameTypeDescription
idintegerread only Identifier.
typepost | news | announcement | promotion | contestrequired post, news, announcement, promotion or contest.
titlestring | null Title (optional).
bodytextrequired The text (plain text when read).
authorstring | nullread only Who wrote it.
comments_enabledboolean Whether people can comment.
comments_countintegerread only How many comments.
published_atdatetime | null When it is published; a future moment schedules it.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

typepost | news | announcement | promotion | contestOnly this type.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/posts

{
    "type": "announcement",
    "title": "Program de sărbători",
    "body": "Între 24 și 26 decembrie programul este 9–14.",
    "comments_enabled": true
}

Example response

{
    "data": {
        "id": 120,
        "type": "announcement",
        "title": "Program de sărbători",
        "body": "Între 24 și 26 decembrie programul este 9–14.",
        "author": "Maria Matei",
        "comments_enabled": true,
        "comments_count": 3,
        "published_at": "2026-05-01T08:00:00Z",
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Knowledge base articles /knowledge-articles

The knowledge base’s published articles (procedures, how-tos), with the body as plain text, in the language asked for (Accept-Language) or the company’s.

Scopes: knowledge_articles:read

Endpoints

  • GET /api/v1/knowledge-articles List
  • GET /api/v1/knowledge-articles/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier.
category_idinteger | nullread only Topic.
categorystring | nullread only Topic name.
titlestringread only Title.
summarytext | nullread only Summary.
bodytext | nullread only The article, as plain text.
is_featuredbooleanread only Whether it is featured.
published_atdatetime | nullread only First published.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

category_idintegerOnly this topic.
is_featuredbooleantrue or false.

Example response

{
    "data": {
        "id": 9,
        "category_id": 2,
        "category": "Operațiuni",
        "title": "Recepția mărfii",
        "summary": null,
        "body": null,
        "is_featured": true,
        "published_at": "2026-05-01T08:00:00Z",
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Customers (CRM)

Customers /crm-contacts

The people the company serves — customers and leads. An email is unique in the company (a second one is refused). Marketing consent is never written with the record: it changes only through the consent action, which keeps its history (given or withdrawn, how, when, by whom). The erase action wipes a person’s data on their request and cannot be undone: name, email, phone, notes and what was written about them go, the figures (purchases, deal values, scores) stay, and the record can no longer be changed.

Scopes: crm_contacts:read, crm_contacts:write

Endpoints

  • GET /api/v1/crm-contacts List
  • GET /api/v1/crm-contacts/{id} One record
  • POST /api/v1/crm-contacts Create
  • PATCH /api/v1/crm-contacts/{id} Change
  • DELETE /api/v1/crm-contacts/{id} Delete
  • POST /api/v1/crm-contacts/{id}/consent Record that the person gave or withdrew their marketing consent, and how.
  • POST /api/v1/crm-contacts/{id}/erase Erase the person’s data at their request (GDPR). Irreversible.

Fields

NameTypeDescription
idintegerread only Identifier.
first_namestring | null First name.
last_namestring | null Last name.
emailstring | null Email address (unique in the company, stored in lowercase).
phonestring | null Phone number, as written.
account_idinteger | null The organisation they belong to (see /crm-accounts).
job_titlestring | null Their role there.
citystring | null City.
countrystring | null Country, as a two-letter ISO code.
localestring | null The language to address them in (ro, en…).
statuslead | customer | inactive lead, customer or inactive. A purchase or a won deal makes a lead a customer.
sourcewalk_in | website | phone | referral | campaign | event | partner | other | null Where they came from: walk_in, website, phone, referral, campaign, event, partner or other.
work_unit_idinteger | null The unit (store, hotel, office) that serves it; empty for none.
owner_idinteger | null The user who looks after it; the key’s creator when left out on create.
tagsarray Tags (lowercase, at most 20).
notestext | null Notes.
marketing_consentbooleanread only Whether they agree to marketing now (read only — changed through the consent action).
consent_updated_atdatetime | nullread only When the consent last changed.
last_activity_atdatetime | nullread only When something was last done with them.
erased_atdatetime | nullread only When their data was erased on request (the record is then read only).
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

qstringName, email or phone containing this.
emailstringThis exact email (case does not matter).
phonestringThis phone number (only its digits are compared).
statuslead | customer | inactiveOnly with this status.
work_unit_idintegerOnly this unit’s.
owner_idintegerOnly this user’s.
account_idintegerOnly this organisation’s.
tagstringOnly with this tag.
consentbooleantrue: only those who agree to marketing; false: only those who do not.

/api/v1/crm-contacts/{id}/consent: granted (boolean) — true: given; false: withdrawn.; channel (form | verbal | email | import | api | other) — How: form, verbal, email, import, api or other (api when left out).; note (string) — A note (for example where the form is kept).

Example request

POST https://compania-mea.harmoniq.ro/api/v1/crm-contacts

{
    "first_name": "Ioana",
    "last_name": "Pop",
    "email": "ioana.pop@example.com",
    "phone": "0722 111 222",
    "account_id": 31,
    "city": "Cluj-Napoca",
    "country": "RO",
    "locale": "ro",
    "status": "customer",
    "source": "walk_in",
    "work_unit_id": 12,
    "owner_id": 17,
    "tags": [
        "loyalty"
    ]
}

Example response

{
    "data": {
        "id": 120,
        "first_name": "Ioana",
        "last_name": "Pop",
        "email": "ioana.pop@example.com",
        "phone": "0722 111 222",
        "account_id": 31,
        "job_title": null,
        "city": "Cluj-Napoca",
        "country": "RO",
        "locale": "ro",
        "status": "customer",
        "source": "walk_in",
        "work_unit_id": 12,
        "owner_id": 17,
        "tags": [
            "loyalty"
        ],
        "notes": null,
        "marketing_consent": true,
        "consent_updated_at": "2026-05-01T08:00:00Z",
        "last_activity_at": "2026-05-01T08:00:00Z",
        "erased_at": "2026-05-01T08:00:00Z",
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Customer organisations /crm-accounts

The companies and institutions the business sells to. The name is unique in the company. Deleting one keeps its contacts and deals, without the link.

Scopes: crm_accounts:read, crm_accounts:write

Endpoints

  • GET /api/v1/crm-accounts List
  • GET /api/v1/crm-accounts/{id} One record
  • POST /api/v1/crm-accounts Create
  • PATCH /api/v1/crm-accounts/{id} Change
  • DELETE /api/v1/crm-accounts/{id} Delete

Fields

NameTypeDescription
idintegerread only Identifier.
namestringrequired Name (unique in the company).
registration_numberstring | null Trade register number.
vat_numberstring | null VAT number.
industrystring | null Industry.
emailstring | null Email address.
phonestring | null Phone number.
websitestring | null Website.
addressstring | null Address.
citystring | null City.
countrystring | null Country, as a two-letter ISO code.
work_unit_idinteger | null The unit (store, hotel, office) that serves it; empty for none.
owner_idinteger | null The user who looks after it; the key’s creator when left out on create.
tagsarray Tags (lowercase, at most 20).
notestext | null Notes.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

qstringName containing this.
work_unit_idintegerOnly this unit’s.
owner_idintegerOnly this user’s.
tagstringOnly with this tag.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/crm-accounts

{
    "name": "Hotel Sigma SRL",
    "registration_number": "J12/345/2019",
    "vat_number": "RO12345678",
    "industry": "Hospitality",
    "email": "office@sigma.example",
    "phone": "0264 000 111",
    "city": "Cluj-Napoca",
    "country": "RO",
    "work_unit_id": 12,
    "owner_id": 17,
    "tags": [
        "corporate",
        "events"
    ]
}

Example response

{
    "data": {
        "id": 31,
        "name": "Hotel Sigma SRL",
        "registration_number": "J12/345/2019",
        "vat_number": "RO12345678",
        "industry": "Hospitality",
        "email": "office@sigma.example",
        "phone": "0264 000 111",
        "website": null,
        "address": null,
        "city": "Cluj-Napoca",
        "country": "RO",
        "work_unit_id": 12,
        "owner_id": 17,
        "tags": [
            "corporate",
            "events"
        ],
        "notes": null,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Sales pipelines /crm-pipelines

The pipelines deals go through, with their stages (set up in the app). Every company has a default one. A stage is open (with the chance a deal there is won) or closes the deal as won or lost.

Scopes: crm_pipelines:read

Endpoints

  • GET /api/v1/crm-pipelines List
  • GET /api/v1/crm-pipelines/{id} One record

Fields

NameTypeDescription
idintegerread only Identifier.
namestringread only Name.
is_defaultbooleanread only Whether new deals go here when no pipeline is given.
sortintegerread only Position in lists.
stagesarrayread only Its stages in order: id, name, kind (open, won or lost), probability (0–100) and sort.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Example response

{
    "data": {
        "id": 1,
        "name": "Sales",
        "is_default": true,
        "sort": 1,
        "stages": [
            {
                "id": 3,
                "name": "Proposal sent",
                "kind": "open",
                "probability": 60,
                "sort": 20
            },
            {
                "id": 5,
                "name": "Won",
                "kind": "won",
                "probability": 100,
                "sort": 40
            }
        ],
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Deals /crm-deals

Sales, bookings or contracts being worked through a pipeline. The pipeline and stage are chosen on create (the default pipeline’s first open stage when left out); afterwards a deal changes stage only through move, win, lose and reopen. Its status always follows its stage, and closed_at is set when it is won or lost. Winning a deal makes its contact a customer.

Scopes: crm_deals:read, crm_deals:write

Endpoints

  • GET /api/v1/crm-deals List
  • GET /api/v1/crm-deals/{id} One record
  • POST /api/v1/crm-deals Create
  • PATCH /api/v1/crm-deals/{id} Change
  • DELETE /api/v1/crm-deals/{id} Delete
  • POST /api/v1/crm-deals/{id}/move Move the deal to another stage (of its pipeline, or of another one — the deal moves with it).
  • POST /api/v1/crm-deals/{id}/win Mark the deal won (its pipeline’s won stage).
  • POST /api/v1/crm-deals/{id}/lose Mark the deal lost (its pipeline’s lost stage), with why.
  • POST /api/v1/crm-deals/{id}/reopen Take a closed deal back to its pipeline’s last open stage.

Fields

NameTypeDescription
idintegerread only Identifier.
titlestringrequired What is being sold.
pipeline_idinteger The pipeline (see /crm-pipelines). Set on create only.
stage_idinteger The stage. Set on create only; then through the actions.
statusopen | won | lostread only open, won or lost — its stage’s kind.
contact_idinteger | null The customer (see /crm-contacts).
account_idinteger | null The organisation (see /crm-accounts).
valuenumber | null What it is worth.
currencystring The value’s currency (ISO code); the company’s CRM currency when left out.
expected_close_ondate | null When it is expected to close.
closed_atdatetime | nullread only When it was won or lost.
lost_reasonstring | nullread only Why it was lost.
sourcewalk_in | website | phone | referral | campaign | event | partner | other | null Where it came from (as for customers).
work_unit_idinteger | null The unit (store, hotel, office) that serves it; empty for none.
owner_idinteger | null The user who looks after it; the key’s creator when left out on create.
notestext | null Notes.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

statusopen | won | lostOnly open, won or lost.
pipeline_idintegerOnly this pipeline’s.
stage_idintegerOnly on this stage.
work_unit_idintegerOnly this unit’s.
owner_idintegerOnly this user’s.
contact_idintegerOnly this customer’s.
account_idintegerOnly this organisation’s.
closed_sincedateClosed on or after this date.
closed_untildateClosed on or before this date.

/api/v1/crm-deals/{id}/move: stage_id (integer) — The stage to move to.

/api/v1/crm-deals/{id}/lose: lost_reason (string) — Why it was lost.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/crm-deals

{
    "title": "Corporate rates 2027",
    "pipeline_id": 1,
    "stage_id": 3,
    "contact_id": 120,
    "account_id": 31,
    "value": 12500.5,
    "currency": "RON",
    "expected_close_on": "2026-12-15",
    "source": "referral",
    "work_unit_id": 12,
    "owner_id": 17
}

Example response

{
    "data": {
        "id": 77,
        "title": "Corporate rates 2027",
        "pipeline_id": 1,
        "stage_id": 3,
        "status": "open",
        "contact_id": 120,
        "account_id": 31,
        "value": 12500.5,
        "currency": "RON",
        "expected_close_on": "2026-12-15",
        "closed_at": "2026-05-01T08:00:00Z",
        "lost_reason": null,
        "source": "referral",
        "work_unit_id": 12,
        "owner_id": 17,
        "notes": null,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Customer activities /crm-activities

What was done, or is to be done, with customers: call, meeting, visit, email, note, task and purchase. A task waits until completed; anything else is done when logged (at done_at when given). A purchase needs its amount and makes its contact a customer. An activity of a deal takes the deal’s customer, organisation and unit when they are not given.

Scopes: crm_activities:read, crm_activities:write

Endpoints

  • GET /api/v1/crm-activities List
  • GET /api/v1/crm-activities/{id} One record
  • POST /api/v1/crm-activities Create
  • PATCH /api/v1/crm-activities/{id} Change
  • DELETE /api/v1/crm-activities/{id} Delete
  • POST /api/v1/crm-activities/{id}/complete Mark a task done (now).
  • POST /api/v1/crm-activities/{id}/reopen Mark it not done again (not for purchases).

Fields

NameTypeDescription
idintegerread only Identifier.
typecall | meeting | visit | email | task | note | purchaserequired call, meeting, visit, email, task, note or purchase.
subjectstringrequired Short description.
bodytext | null Details.
contact_idinteger | null The customer.
account_idinteger | null The organisation.
deal_idinteger | null The deal it belongs to.
amountnumber | null A purchase’s amount, in the company’s CRM currency (empty for the other types).
due_atdatetime | null When a task is due.
done_atdatetime | null When it was done; empty for a task still to do.
work_unit_idinteger | null The unit (store, hotel, office) that serves it; empty for none.
owner_idinteger | null The user who looks after it; the key’s creator when left out on create.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

typecall | meeting | visit | email | task | note | purchaseOnly this type.
work_unit_idintegerOnly this unit’s.
owner_idintegerOnly this user’s.
contact_idintegerOnly this customer’s.
deal_idintegerOnly this deal’s.
donebooleantrue: only done; false: only still to do.
due_beforedatetimeDue before this moment.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/crm-activities

{
    "type": "purchase",
    "subject": "Receipt 1243",
    "contact_id": 120,
    "amount": 240,
    "due_at": "2026-11-03T09:00:00Z",
    "done_at": "2026-11-02T15:20:00Z",
    "work_unit_id": 12,
    "owner_id": 17
}

Example response

{
    "data": {
        "id": 530,
        "type": "purchase",
        "subject": "Receipt 1243",
        "body": null,
        "contact_id": 120,
        "account_id": 1,
        "deal_id": 1,
        "amount": 240,
        "due_at": "2026-11-03T09:00:00Z",
        "done_at": "2026-11-02T15:20:00Z",
        "work_unit_id": 12,
        "owner_id": 17,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Customer feedback /crm-feedback

What customers said: feedback (a 0–10 score — how likely they are to recommend the business — and/or a comment) and complaints. Feedback needs a score or a comment. A complaint stays open until resolved with what was done; it can be reopened. The kind is set on create.

Scopes: crm_feedback:read, crm_feedback:write

Endpoints

  • GET /api/v1/crm-feedback List
  • GET /api/v1/crm-feedback/{id} One record
  • POST /api/v1/crm-feedback Create
  • PATCH /api/v1/crm-feedback/{id} Change
  • DELETE /api/v1/crm-feedback/{id} Delete
  • POST /api/v1/crm-feedback/{id}/resolve Resolve a complaint, saying what was done.
  • POST /api/v1/crm-feedback/{id}/reopen Reopen a resolved complaint.

Fields

NameTypeDescription
idintegerread only Identifier.
kindfeedback | complaintrequired feedback or complaint. Set on create only.
contact_idinteger | null The customer.
account_idinteger | null The organisation.
scoreinteger | null From 0 to 10.
commenttext | null What they said.
channelin_person | phone | email | website | review_site | survey | other | null How it came: in_person, phone, email, website, review_site, survey or other.
received_ondate When it was received (today when left out).
statusopen | resolvedread only open (a complaint not yet resolved) or resolved.
resolved_atdatetime | nullread only When the complaint was resolved.
resolutiontext | nullread only What was done about it.
work_unit_idinteger | null The unit (store, hotel, office) that serves it; empty for none.
owner_idinteger | null The user who looks after it; the key’s creator when left out on create.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

kindfeedback | complaintOnly feedback or only complaints.
statusopen | resolvedOnly open or only resolved.
work_unit_idintegerOnly this unit’s.
contact_idintegerOnly this customer’s.
received_sincedateReceived on or after this date.

/api/v1/crm-feedback/{id}/resolve: resolution (text) — What was done.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/crm-feedback

{
    "kind": "feedback",
    "contact_id": 120,
    "score": 9,
    "comment": "Quick and friendly service.",
    "channel": "in_person",
    "received_on": "2026-11-02",
    "work_unit_id": 12,
    "owner_id": 17
}

Example response

{
    "data": {
        "id": 64,
        "kind": "feedback",
        "contact_id": 120,
        "account_id": 1,
        "score": 9,
        "comment": "Quick and friendly service.",
        "channel": "in_person",
        "received_on": "2026-11-02",
        "status": "resolved",
        "resolved_at": "2026-05-01T08:00:00Z",
        "resolution": null,
        "work_unit_id": 12,
        "owner_id": 17,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

In-store spaces and shelf space

Partners /merch-partners

The producers and brands whose campaigns go into the company's display spaces and onto its shelves: one list for both modules. A name is unique in the company. Writing needs the key creator to hold "Manage partners".

Scopes: merch_partners:read, merch_partners:write

Endpoints

  • GET /api/v1/merch-partners List
  • GET /api/v1/merch-partners/{id} One record
  • POST /api/v1/merch-partners Create
  • PATCH /api/v1/merch-partners/{id} Change

Fields

NameTypeDescription
idintegerread only Identifier.
namestringrequired The partner's name (unique in the company).
codestring | null Internal code.
contact_namestring | null Contact person.
contact_emailstring | null The contact's email.
contact_phonestring | null The contact's phone.
notestext | null Notes.
crm_account_idinteger | null The matching CRM organisation (see /crm-accounts), if any.
is_activeboolean Whether it can get new campaigns.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

qstringName containing the text.
is_activebooleantrue: active ones only; false: inactive ones only.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/merch-partners

{
    "name": "Dermalab",
    "code": "DLB",
    "contact_name": "Ana Ionescu",
    "contact_email": "ana@dermalab.example",
    "contact_phone": "0722 000 111",
    "crm_account_id": 31,
    "is_active": true
}

Example response

{
    "data": {
        "id": 3,
        "name": "Dermalab",
        "code": "DLB",
        "contact_name": "Ana Ionescu",
        "contact_email": "ana@dermalab.example",
        "contact_phone": "0722 000 111",
        "notes": null,
        "crm_account_id": 31,
        "is_active": true,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Zone types /space-zone-types

In-store spaces: the kinds of zone the company has in many locations (a click frame, a counter stand, a totem), with their usual measurements and capacity. The monthly price is written only by who sees money.

Scopes: space_zone_types:read, space_zone_types:write

Endpoints

  • GET /api/v1/space-zone-types List
  • GET /api/v1/space-zone-types/{id} One record
  • POST /api/v1/space-zone-types Create
  • PATCH /api/v1/space-zone-types/{id} Change

Fields

NameTypeDescription
idintegerread only Identifier.
namestringrequired Name.
descriptiontext | null Description.
width_cmnumber | null Width, in centimetres.
height_cmnumber | null Height, in centimetres.
depth_cmnumber | null Depth, in centimetres.
facesinteger How many faces (1 or 2: A, B).
capacityinteger How many items fit on one face.
capacity_labelstring | null What the capacity counts (e.g. "A2 posters").
is_displayboolean Whether it is a display space (can host campaigns).
monthly_pricenumber | null The monthly rate for one unit of capacity (suggests campaign values).
is_activeboolean Whether it is still used.
sortinteger Order in lists.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

is_activebooleantrue: in use only; false: retired only.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/space-zone-types

{
    "name": "Ramă click A2",
    "width_cm": 50,
    "height_cm": 70,
    "faces": 2,
    "capacity": 2,
    "capacity_label": "afișe A2",
    "is_display": true,
    "monthly_price": 150,
    "is_active": true,
    "sort": 10
}

Example response

{
    "data": {
        "id": 2,
        "name": "Ramă click A2",
        "description": null,
        "width_cm": 50,
        "height_cm": 70,
        "depth_cm": 12.5,
        "faces": 2,
        "capacity": 2,
        "capacity_label": "afișe A2",
        "is_display": true,
        "monthly_price": 150,
        "is_active": true,
        "sort": 10,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Zones in locations /space-zones

In-store spaces: each location's zones (where, how big, how much fits on each face), only in the locations the key's creator lays out. A zone stays in its location. A new zone of a type takes the type's measurements for what is missing. Fewer faces or less room than its running campaigns need is refused; a zone with campaigns cannot be deleted.

Scopes: space_zones:read, space_zones:write

Endpoints

  • GET /api/v1/space-zones List
  • GET /api/v1/space-zones/{id} One record
  • POST /api/v1/space-zones Create
  • PATCH /api/v1/space-zones/{id} Change
  • DELETE /api/v1/space-zones/{id} Delete

Fields

NameTypeDescription
idintegerread only Identifier.
work_unit_idintegerrequired The location (unit) — on create only.
zone_type_idinteger | null The zone type (see /space-zone-types), if any.
namestringrequired Name (e.g. "Totem right of the door").
descriptiontext | null Description.
width_cmnumber | null Width, in centimetres.
height_cmnumber | null Height, in centimetres.
depth_cmnumber | null Depth, in centimetres.
area_m2number | null Area, in square metres.
facesinteger How many faces (1 or 2).
capacityinteger How many items fit on one face.
capacity_labelstring | null What the capacity counts.
is_displayboolean Whether it is a display space.
monthly_pricenumber | null Monthly rate (written only by who sees money).
statusactive | unavailable active or unavailable (temporarily).
pin_xnumber | nullread only Position on the location's sketch, percent from the left (placed in the app).
pin_ynumber | nullread only Position on the location's sketch, percent from the top.
sortinteger Order in the location.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

work_unit_idintegerOnly this location's.
zone_type_idintegerOnly of this type.
statusactive | unavailableOnly with this status.
is_displaybooleantrue: display spaces only.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/space-zones

{
    "work_unit_id": 12,
    "zone_type_id": 2,
    "name": "Ramă click intrare",
    "width_cm": 50,
    "height_cm": 70,
    "faces": 2,
    "capacity": 2,
    "capacity_label": "afișe A2",
    "is_display": true,
    "monthly_price": 150,
    "status": "active",
    "sort": 100
}

Example response

{
    "data": {
        "id": 41,
        "work_unit_id": 12,
        "zone_type_id": 2,
        "name": "Ramă click intrare",
        "description": null,
        "width_cm": 50,
        "height_cm": 70,
        "depth_cm": 12.5,
        "area_m2": 12.5,
        "faces": 2,
        "capacity": 2,
        "capacity_label": "afișe A2",
        "is_display": true,
        "monthly_price": 150,
        "status": "active",
        "pin_x": 42.5,
        "pin_y": 88,
        "sort": 100,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Fixtures and rates /shelf-fixture-types

Shelf space: the kinds of fixture (a gondola, an end cap, a fridge) with the rate card — per product facing or per metre of shelf, a month. Rates are written only by who holds the shelf-prices right.

Scopes: shelf_fixture_types:read, shelf_fixture_types:write

Endpoints

  • GET /api/v1/shelf-fixture-types List
  • GET /api/v1/shelf-fixture-types/{id} One record
  • POST /api/v1/shelf-fixture-types Create
  • PATCH /api/v1/shelf-fixture-types/{id} Change

Fields

NameTypeDescription
idintegerread only Identifier.
namestringrequired Name.
face_pricenumber | null Rate per product facing, a month.
meter_pricenumber | null Rate per metre of shelf, a month.
is_activeboolean Whether it is still used.
sortinteger Order in lists.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

is_activebooleantrue: in use only.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/shelf-fixture-types

{
    "name": "Gondolă",
    "face_price": 25,
    "is_active": true,
    "sort": 10
}

Example response

{
    "data": {
        "id": 1,
        "name": "Gondolă",
        "face_price": 25,
        "meter_price": 12.5,
        "is_active": true,
        "sort": 10,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Planograms /shelf-planograms

Shelf space: planograms described once — the fixture, the measurements, the shelves top to bottom with what fits on each (facings or centimetres) and its price factor. Sending shelves replaces the list of shelves; a shelf with campaigns cannot be taken away. A name is unique.

Scopes: shelf_planograms:read, shelf_planograms:write

Endpoints

  • GET /api/v1/shelf-planograms List
  • GET /api/v1/shelf-planograms/{id} One record
  • POST /api/v1/shelf-planograms Create
  • PATCH /api/v1/shelf-planograms/{id} Change

Fields

NameTypeDescription
idintegerread only Identifier.
namestringrequired Name (unique).
codestring | null Internal code.
fixture_type_idinteger | null The fixture type (see /shelf-fixture-types).
categorystring | null Product category.
width_cmnumber | null Width, in centimetres.
height_cmnumber | null Height, in centimetres.
depth_cmnumber | null Depth, in centimetres.
capacity_unitfaces | cm faces: shelf capacity is in product facings; cm: in centimetres.
instructionstext | null How to set it out (formatted text, cleaned).
is_activeboolean Whether it is still set up.
shelvesarrayrequired The shelves, top to bottom: [{label, capacity, price_factor}] (position follows the list; price_factor is written only by who sees prices).
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

fixture_type_idintegerOnly with this fixture.
is_activebooleantrue: active only.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/shelf-planograms

{
    "name": "Capăt de raft vitamine",
    "code": "CR-VIT",
    "fixture_type_id": 2,
    "category": "Suplimente",
    "width_cm": 100,
    "height_cm": 180,
    "capacity_unit": "faces",
    "is_active": true,
    "shelves": [
        {
            "position": 1,
            "label": null,
            "capacity": 8,
            "price_factor": 1.2
        },
        {
            "position": 2,
            "label": "La nivelul ochilor",
            "capacity": 8,
            "price_factor": 1.5
        }
    ]
}

Example response

{
    "data": {
        "id": 4,
        "name": "Capăt de raft vitamine",
        "code": "CR-VIT",
        "fixture_type_id": 2,
        "category": "Suplimente",
        "width_cm": 100,
        "height_cm": 180,
        "depth_cm": 12.5,
        "capacity_unit": "faces",
        "instructions": null,
        "is_active": true,
        "shelves": [
            {
                "position": 1,
                "label": null,
                "capacity": 8,
                "price_factor": 1.2
            },
            {
                "position": 2,
                "label": "La nivelul ochilor",
                "capacity": 8,
                "price_factor": 1.5
            }
        ],
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Placed planograms /shelf-placements

Shelf space: a planogram set up in a location, with its own label, note and costs, only in the locations the key's creator lays out. Deleting takes it out of the location: kept for its campaigns' history, or gone if it had none; never while a campaign runs on it. Costs are written only by who sees prices.

Scopes: shelf_placements:read, shelf_placements:write

Endpoints

  • GET /api/v1/shelf-placements List
  • GET /api/v1/shelf-placements/{id} One record
  • POST /api/v1/shelf-placements Create
  • PATCH /api/v1/shelf-placements/{id} Change
  • DELETE /api/v1/shelf-placements/{id} Delete

Fields

NameTypeDescription
idintegerread only Identifier.
planogram_idintegerrequired The planogram — on create only.
work_unit_idintegerrequired The location — on create only.
labelstring | null A label telling it apart from another placement of the same planogram in the location.
statusactive | removedread only active or removed.
installed_ondate | null Date set up.
removed_ondate | nullread only Date taken out.
local_notetext | null What differs here from the usual planogram.
setup_costnumber | null Set-up cost.
monthly_costnumber | null Monthly cost here (upkeep, depreciation).
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

work_unit_idintegerOnly in this location.
planogram_idintegerOnly this planogram.
statusactive | removedOnly with this status.

Example request

POST https://compania-mea.harmoniq.ro/api/v1/shelf-placements

{
    "planogram_id": 4,
    "work_unit_id": 12,
    "label": "lângă casă",
    "installed_on": "2026-02-18",
    "monthly_cost": 25
}

Example response

{
    "data": {
        "id": 88,
        "planogram_id": 4,
        "work_unit_id": 12,
        "label": "lângă casă",
        "status": "active",
        "installed_on": "2026-02-18",
        "removed_on": "2026-05-01",
        "local_note": null,
        "setup_cost": 12.5,
        "monthly_cost": 25,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Campaigns /merch-campaigns

Campaigns of both modules (module: spaces — display zones; shelf — planogram shelves), written as the key's creator. A campaign starts as a draft: its locations (units) and the rules naming places in them (rules) become its places. The launch action checks room and sends it to each location; cancel stops it. A launched campaign keeps its dates and places; only its text, partner, money and photo requirement change. Value and cost are written only by who sees money.

Scopes: merch_campaigns:read, merch_campaigns:write

Endpoints

  • GET /api/v1/merch-campaigns List
  • GET /api/v1/merch-campaigns/{id} One record
  • POST /api/v1/merch-campaigns Create
  • PATCH /api/v1/merch-campaigns/{id} Change
  • DELETE /api/v1/merch-campaigns/{id} Delete
  • POST /api/v1/merch-campaigns/{id}/launch Launch the draft: check room and send it to each location.
  • POST /api/v1/merch-campaigns/{id}/cancel Cancel a launched campaign: it does not start where it was not put up, and comes down where it is up.

Fields

NameTypeDescription
idintegerread only Identifier.
modulespaces | shelfrequired spaces or shelf — on create only.
titlestringrequired Campaign title.
partner_idinteger | null The paying partner (see /merch-partners); empty: the company's own campaign.
instructionstext | null Implementation instructions (formatted text, cleaned).
starts_ondaterequired First day.
ends_ondaterequired Last day.
implement_daysinteger Days a location has to put it up, from the start.
remove_daysinteger Days a location has to take it down, after the end.
proof_requiredboolean Whether a location must send photos when confirming.
valuenumber | null What the partner pays for the whole campaign, excluding VAT.
costnumber | null What it costs the company (materials, set-up), excluding VAT.
currencystringread only The amounts' currency (from the module's settings).
statusdraft | launched | cancelledread only draft, launched or cancelled.
phasedraft | scheduled | running | ended | cancelledread only draft, scheduled, running, ended or cancelled.
unitsarray The locations (unit ids), together with rules.
rulesarray The rules naming places, together with units. spaces: [{zone_type_id or zone_id, face (A/B), quantity}]; shelf: [{planogram_id or placement_id, position (shelf, from the top), quantity, products}].
targetsarrayread only The resulting places: per location, the zone and face or the placement and shelf, with the quantity.
launched_atdatetime | nullread only When launched.
cancelled_atdatetime | nullread only When cancelled.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

modulespaces | shelfOnly this module.
statusdraft | launched | cancelledOnly with this status.
partner_idintegerOnly this partner's.
active_ondateOnly those running on this day (YYYY-MM-DD).

Example request

POST https://compania-mea.harmoniq.ro/api/v1/merch-campaigns

{
    "module": "shelf",
    "title": "Dermalab – raftul de la ochi",
    "partner_id": 3,
    "starts_on": "2026-11-02",
    "ends_on": "2026-11-30",
    "implement_days": 3,
    "remove_days": 2,
    "proof_required": true,
    "value": 9600,
    "cost": 0,
    "units": [
        12,
        13
    ],
    "rules": [
        {
            "planogram_id": 4,
            "position": 2,
            "quantity": 6,
            "products": "Vitamina D3"
        }
    ]
}

Example response

{
    "data": {
        "id": 15,
        "module": "shelf",
        "title": "Dermalab – raftul de la ochi",
        "partner_id": 3,
        "instructions": null,
        "starts_on": "2026-11-02",
        "ends_on": "2026-11-30",
        "implement_days": 3,
        "remove_days": 2,
        "proof_required": true,
        "value": 9600,
        "cost": 0,
        "currency": "RON",
        "status": "draft",
        "phase": "scheduled",
        "units": [
            12,
            13
        ],
        "rules": [
            {
                "planogram_id": 4,
                "position": 2,
                "quantity": 6,
                "products": "Vitamina D3"
            }
        ],
        "targets": [
            {
                "work_unit_id": 12,
                "placement_id": 88,
                "shelf_id": 210,
                "quantity": 6,
                "products": "Vitamina D3"
            }
        ],
        "launched_at": "2026-05-01T08:00:00Z",
        "cancelled_at": "2026-05-01T08:00:00Z",
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

Implementation in locations /merch-executions

A launched campaign in one location: who puts it up and by when, whether it is up, refused (with the reason), being taken down or done. Actions are done as the key's creator (who implements there or, for reopen, who runs the campaign). Photos are sent as base64 images; a campaign that asks for them cannot be confirmed without.

Scopes: merch_executions:read, merch_executions:write

Endpoints

  • GET /api/v1/merch-executions List
  • GET /api/v1/merch-executions/{id} One record
  • POST /api/v1/merch-executions/{id}/confirm Confirm it is up (with photos, if asked).
  • POST /api/v1/merch-executions/{id}/refuse Say it cannot be put up, with the reason.
  • POST /api/v1/merch-executions/{id}/reopen Ask a location that refused again (who runs the campaign).
  • POST /api/v1/merch-executions/{id}/removed Confirm the materials were taken down.

Fields

NameTypeDescription
idintegerread only Identifier.
campaign_idintegerread only The campaign (see /merch-campaigns).
modulespaces | shelfread only spaces or shelf (the campaign's).
work_unit_idintegerread only The location.
implementer_idinteger | nullread only Who implements it specifically; empty: the location's managers.
statuswaiting | pending | done | refused | removing | removed | cancelledread only waiting (not started yet), pending (to put up), done (up), refused, removing (to take down), removed or cancelled.
latebooleanread only Whether it was put up after the deadline, or is not up and the deadline has passed.
due_ondate | nullread only Put up by.
done_atdatetime | nullread only When confirmed.
done_byinteger | nullread only Who confirmed.
notetext | nullread only Notes from the confirmation.
refused_atdatetime | nullread only When refused.
refused_byinteger | nullread only Who refused.
refusal_reasontext | nullread only Why it cannot be done.
remove_due_ondate | nullread only Taken down by.
removed_atdatetime | nullread only When the take-down was confirmed.
removed_byinteger | nullread only Who confirmed the take-down.
created_atdatetimeread only Created.
updated_atdatetimeread only Last changed.

Filters

campaign_idintegerOnly this campaign's.
work_unit_idintegerOnly in this location.
statuswaiting | pending | done | refused | removing | removed | cancelledOnly with this status.
modulespaces | shelfOnly this module.
latebooleantrue: late ones only.

/api/v1/merch-executions/{id}/confirm: photos (array) — Photos: [{name, content_base64}], JPEG/PNG/WebP, at most 10 MB each, at most 10.; note (text) — Notes.

/api/v1/merch-executions/{id}/refuse: reason (text) — The reason.

/api/v1/merch-executions/{id}/removed: photos (array) — Photos after the take-down (optional): [{name, content_base64}].

Example response

{
    "data": {
        "id": 501,
        "campaign_id": 15,
        "module": "shelf",
        "work_unit_id": 12,
        "implementer_id": 17,
        "status": "pending",
        "late": false,
        "due_on": "2026-11-04",
        "done_at": "2026-05-01T08:00:00Z",
        "done_by": 1,
        "note": null,
        "refused_at": "2026-05-01T08:00:00Z",
        "refused_by": 1,
        "refusal_reason": null,
        "remove_due_on": "2026-05-01",
        "removed_at": "2026-05-01T08:00:00Z",
        "removed_by": 1,
        "created_at": "2026-05-01T08:00:00Z",
        "updated_at": "2026-05-01T08:00:00Z"
    }
}

See how Harmoniq works for your team

Try every module free or book a demo with our team. No card needed.

or we set up a trial company with your structure