# Maci API > Give your own systems your units, components, service history, F-gas log, alarms and invoice bases, and get alarms and approved invoice bases the moment they happen. The API gives an organisation's own systems access to the data the organisation has in Maci Service, the alarms from Maci Intelligence and, for a service company, its invoice bases. Webhooks post alarms and approved invoice bases to an address of the organisation's own. Login, passkeys, grants, invitations, API keys and administration can only be handled in the web app. The base address is `https://api.maci.energy/v1`. Every call takes an API key, and every answer is JSON. ## Guides - [What you can build](https://api.maci.energy/docs/what-you-can-build.md): Maci Intelligence watches each refrigeration system and warns before it breaks down. The API and the webhooks take what it finds, and the data around each unit, into the systems your people already work in. - [Quickstart](https://api.maci.energy/docs/quickstart.md): From no key to your first answer in a few minutes: make a key in the app, call the API, and read what comes back. - [Authentication](https://api.maci.energy/docs/authentication.md): Every call carries an API key. The key decides where the call works, sandbox or production, and what it may do. - [Lists, limits and versions](https://api.maci.energy/docs/conventions.md): The rules every endpoint follows: the format, paging, repeated calls, how many calls a key may make, and how the API changes. - [Errors](https://api.maci.energy/docs/errors.md): The API answers with an ordinary HTTP status. An error also has a body with a fixed code your system can act on and a message for a person. - [Webhooks](https://api.maci.energy/docs/webhooks.md): Maci can post events to an address of your own, so your system hears about an alarm or an approved invoice basis at once, signed with a secret only you and Maci know. - [AI assistants (MCP)](https://api.maci.energy/docs/ai-assistants.md): Connect Maci to Claude, ChatGPT or another assistant that speaks the Model Context Protocol, and ask it about your refrigeration units in your own words. It reads through the API, so it sees what a key of yours would see, and it cannot change anything. ## Resources - [Units](https://api.maci.energy/docs/units.md): A unit is one refrigeration system, display case, cold room or other piece of equipment with its own QR or NFC tag, and a site is where it stands. - [Components](https://api.maci.energy/docs/components.md): The parts of a unit: compressors, fans, valves, controllers and the rest. - [Photos](https://api.maci.energy/docs/photos.md): Photos of a unit's nameplates, labels and overviews. - [Service](https://api.maci.energy/docs/service.md): A unit's service history: visits, repairs, fault reports and notes, from the app and from your own systems. - [F-gas log](https://api.maci.energy/docs/f-gas-log.md): The F-gas log under regulation (EU) 2024/573 and the unit's status: CO2 equivalent, the leak check interval and when the next check is due. - [Subscriptions](https://api.maci.energy/docs/subscriptions.md): The organisation's Maci Service subscriptions. - [Invoicing](https://api.maci.energy/docs/invoicing.md): A service company's invoice basis, made when a technician approves a service report from speech, and its catalogue. - [Alarms](https://api.maci.energy/docs/alarms.md): Alarms from Maci Intelligence for the units' owner and for the refrigeration firm the owner has chosen as the watch. - [Refrigerants and usage](https://api.maci.energy/docs/refrigerants-and-usage.md): The refrigerant table with GWP, open to every key, and what the key and the organisation have used this month. --- # What you can build Maci Intelligence watches each refrigeration system and warns before it breaks down. The API and the webhooks take what it finds, and the data around each unit, into the systems your people already work in. ## Ideas Act on the alarms where you work, keep your own records in step with Maci, or both: - **Turn an alarm into a work order.** Receive `alarm.raised` by [webhook](https://api.maci.energy/docs/webhooks.md#alarm-webhook) and open a ticket in your service or CAFM system, with the unit, the site and the readings the rule saw. - **Take and close alarms from your own system.** When someone takes the job, [acknowledge the alarm](https://api.maci.energy/docs/alarms.md#acknowledge-alarm), so it stops going on to the next people on the list. [Close it](https://api.maci.energy/docs/alarms.md#close-alarm) as real or false when the visit is done; what was found helps tune the alarm rules. - **Keep your asset register in step.** Read [units, sites and components](https://api.maci.energy/docs/units.md) with refrigerant, charge and CO2 equivalent into your CAFM or ERP, and send changes back. A replaced component keeps its history. - **Plan leak checks across all your sites.** Read each unit's [F-gas status](https://api.maci.energy/docs/f-gas-log.md#get-f-gas-status): the interval, the last check and when the next one is due, and put the deadlines in your planning. - **Write service history from your own tools.** Add [service notes](https://api.maci.energy/docs/service.md#create-service-entry) and [F-gas entries](https://api.maci.energy/docs/f-gas-log.md#create-f-gas-entry) when your technicians work in another app, so each unit's history in Maci stays whole. - **Invoice from approved service reports.** Get each approved [invoice basis](https://api.maci.energy/docs/invoicing.md) by webhook, book it in your accounting system and [mark it exported](https://api.maci.energy/docs/invoicing.md#mark-invoice-drafts-exported). Keep the catalogue in step with [your price list](https://api.maci.energy/docs/invoicing.md#put-price-list-items). - **Report to your clients.** Fetch a period's alarms with [GET /alarms](https://api.maci.energy/docs/alarms.md#list-alarms) for an SLA report: when each alarm came, who took it, how fast, and whether it was real. - **Register units from a photo.** [Send a photo of the nameplate](https://api.maci.energy/docs/photos.md#create-photo) and use [what was read](https://api.maci.energy/docs/photos.md#get-photo-reading) to fill in the manufacturer, model and refrigerant. The readings behind an alarm stay in Maci. The API gives each alarm with the values the rule saw, and everything about the units around it. ## Who builds what | Who | What they usually build | | --- | --- | | **Facility management and owners of many sites** | Alarms into the CAFM system as work orders, the asset register in step, leak check deadlines in the plan, and an alarm report for each client. | | **Refrigeration companies on watch** | The alarms of the units where the owner has chosen you as the watch, straight to your on-call tool; service notes and F-gas entries from your field service app; invoice bases to your accounting system. | | **Software companies and integrators** | The same for your customers. Each customer makes a key for your integration with only the scopes it needs, and can delete it at any time. | ## An example: from alarm to work order **1. Open a work order when an alarm is raised.** Set the address for alarms in the app, check the signature, answer at once, and open the work order. `openWorkOrder` and `signatureIsValid` stand for your own code. ```javascript import express from 'express'; const app = express(); // The raw body, so the signature can be checked before anything else (see Webhooks). app.post('/maci/alarms', express.raw({ type: 'application/json' }), async (req, res) => { if (!signatureIsValid(req.body, req.get('X-Maci-Signature'))) return res.sendStatus(401); res.sendStatus(200); // Answer at once, then do the work. const { event, data: alarm } = JSON.parse(req.body); if (event === 'alarm.raised') { await openWorkOrder({ title: `${alarm.name}: ${alarm.unit} (${alarm.unit_public_id})`, site: alarm.site, priority: alarm.severity === 'HIGH' ? 'urgent' : 'normal', details: alarm.values, maciAlarmId: alarm.id, }); } }); ``` **2. Take the alarm when a person takes the job.** Acknowledge it from your system when a technician accepts the work order, so Maci stops passing it on. Do not acknowledge it as soon as the work order is made: then nobody else is told, even if nobody has seen it. ```bash curl -X POST https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/acknowledge \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/acknowledge', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.post( "https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/acknowledge", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('POST', 'https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/acknowledge', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` **3. Close it when the visit is done,** as real or false, with what was found. ```bash curl -X POST https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/close \ -H "Authorization: Bearer $MACI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "result": "real", "note": "Door left open after a delivery." }' ``` ```javascript const response = await fetch('https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/close', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "result": "real", "note": "Door left open after a delivery." }), }); const data = await response.json(); ``` ```python import os import requests response = requests.post( "https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/close", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, json={ "result": "real", "note": "Door left open after a delivery." }, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('POST', 'https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/close', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], 'json' => [ 'result' => 'real', 'note' => 'Door left open after a delivery.', ], ]); $data = json_decode($response->getBody(), true); ``` ## Start building - [Quickstart](https://api.maci.energy/docs/quickstart.md): Make a key and your first call in a few minutes. - [Webhooks](https://api.maci.energy/docs/webhooks.md): Set up the address, check the signature, answer and retries. - [Alarms](https://api.maci.energy/docs/alarms.md): List, read, take and close alarms. - [Authentication](https://api.maci.energy/docs/authentication.md): Keys, scopes and the sandbox. --- # Quickstart From no key to your first answer in a few minutes: make a key in the app, call the API, and read what comes back. ## Get an API key An admin of your organisation makes keys in the app under [Konto → API-nøgler](https://app.maci.energy/organization/api-keys). Give the key a name, choose the scopes it needs, and choose sandbox or production. The key is shown once, when it is made, so put it in your secret store straight away. Start with a sandbox key. It starts with `maci_sandbox_` and works on test data of your own, a site and two units, so you can build without touching the real units. A production key starts with `maci_live_`. The examples here read the key from the environment variable `MACI_API_KEY`: ```bash export MACI_API_KEY="maci_sandbox_..." ``` ## Make your first call List the units the key can reach. Every call goes to `https://api.maci.energy/v1` with the key in the header `Authorization: Bearer ...`. ```bash curl https://api.maci.energy/v1/units \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/units", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/units', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ```json { "data": [ { "id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "public_id": "MC-0482", "name": "Cold room 2", "category": "cold_room", "status": "active", "organization_id": "a3f1c2d4-6b8e-4f0a-9c1d-2e3f4a5b6c7d", "site_id": "5d0c7e52-8a0b-4c3e-b1a4-7c1f6a2d9e30", "parent_unit_id": null, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "production_year": 2019, "installation_year": 2020, "system_type": "split", "refrigerant": "R449A", "charge_kg": 42, "co2e_tonnes": 58.7, "charge_is_on_parent": false, "premium": true, "hermetically_sealed": false, "leak_detection_system": false, "temp_min_c": 0, "temp_max_c": 4, "supply_voltage_v": 400, "supply_phases": 3, "supply_frequency_hz": 50, "rated_current_a": 12.4, "power_kw": 5.5, "ps_high_bar": 28, "ps_low_bar": 19, "climate_class": "N", "created_at": "2026-10-30T09:15:00Z", "updated_at": "2026-10-30T09:15:00Z", "components": [ { "id": "c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "type": "compressor", "label": "Compressor 1", "quantity": 1, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "status": "active", "installed_at": "2026-10-30", "removed_at": null, "replaces_component_id": null, "attributes": {}, "custom_fields": null, "note": null, "created_at": "2026-10-30T09:15:00Z", "updated_at": "2026-10-30T09:15:00Z" } ] } ], "next_cursor": null } ``` ## Read the answer A list comes as `data` and `next_cursor`. When `next_cursor` is not `null`, send it as `cursor` to get the next page. Times are ISO 8601 in UTC, IDs are UUIDs, and measurements are in SI units. The header `Maci-Environment` says whether the answer came from the sandbox or production, and the `X-RateLimit-*` headers how many calls the key has left this minute. ## Next steps - [Authentication](https://api.maci.energy/docs/authentication.md): Keys, scopes, the sandbox and what a key can reach. - [Units](https://api.maci.energy/docs/units.md): Read and change units, the heart of the API. - [Webhooks](https://api.maci.energy/docs/webhooks.md): Get alarms and invoice bases the moment they happen. - [Errors](https://api.maci.energy/docs/errors.md): What each status means, and how to handle it. --- # Authentication Every call carries an API key. The key decides where the call works, sandbox or production, and what it may do. ## Sending the key Send the key in the header `Authorization: Bearer maci_live_...`. The key is accepted only in that header and only over HTTPS. A call without a valid key gets 401. ```bash Authorization: Bearer maci_live_... ``` ## Making and rotating keys The organisation's admins create, rotate and delete API keys in the app under Konto → API-nøgler. A key is shown once, when it is created. Maci Service (Premium) is bought in the app as a number of subscriptions, and each is on one unit or unassigned. The API only reaches units that have one: a unit without Maci Service is left out of lists and answers 403. The subscriptions endpoints list the organisation's subscriptions and give an unassigned one to a unit, or take one off, so an integration can decide which units have Maci Service. Keys are made under [Konto → API-nøgler](https://app.maci.energy/organization/api-keys). Every key expires, after 90 or 365 days as chosen when it is made. Rotating a key makes a new one with the same name and scopes; the old one keeps working for 24 hours, so your systems can change over without a gap. Deleting a key stops it at once. ## Sandbox and production There is one API, and the key decides where a call works. A production key starts with `maci_live_` and works on the organisation's real units. A sandbox key starts with `maci_sandbox_` and works on the organisation's own sandbox: test data that starts with a site and two units, and that nothing in production can see or be changed by. Addresses, scopes, limits, errors and answers are the same in both, so moving to production is a matter of changing the key. Every answer has the header `Maci-Environment` with `sandbox` or `production`. Sandbox calls do not count towards the monthly limit, and a label (`tag_code`) cannot be used in the sandbox. ## What a key can reach A key can never do more than the organisation itself. An owner's key reaches the organisation's own units. A service company's key reaches the units where the company has an active grant. Grants are checked on every call. Each endpoint asks for a scope, which is chosen per key. The API gives an organisation's own systems access to the data the organisation has in Maci Service, the alarms from Maci Intelligence and, for a service company, its invoice bases. Webhooks post alarms and approved invoice bases to an address of the organisation's own. Login, passkeys, grants, invitations, API keys and administration can only be handled in the web app. ## Scopes Each endpoint asks for one scope. Give a key only the scopes it needs. A call with a key that lacks the scope gets 403 with the code `missing_scope`. | Scope | Endpoints | | --- | --- | | `units:read` | [GET /sites](https://api.maci.energy/docs/units.md#list-sites), [GET /units](https://api.maci.energy/docs/units.md#list-units), [GET /units/{unit}](https://api.maci.energy/docs/units.md#get-unit), [GET /units/{unit}/components](https://api.maci.energy/docs/components.md#list-components), [GET /units/{unit}/photos](https://api.maci.energy/docs/photos.md#list-photos), [GET /photos/{photo}/reading](https://api.maci.energy/docs/photos.md#get-photo-reading) | | `units:write` | [POST /units](https://api.maci.energy/docs/units.md#create-unit), [PATCH /units/{unit}](https://api.maci.energy/docs/units.md#update-unit), [POST /units/{unit}/components](https://api.maci.energy/docs/components.md#create-component), [PATCH /components/{component}](https://api.maci.energy/docs/components.md#update-component), [POST /components/{component}/replace](https://api.maci.energy/docs/components.md#replace-component), [POST /units/{unit}/photos](https://api.maci.energy/docs/photos.md#create-photo) | | `service:read` | [GET /units/{unit}/service-entries](https://api.maci.energy/docs/service.md#list-service-entries) | | `service:write` | [POST /units/{unit}/service-entries](https://api.maci.energy/docs/service.md#create-service-entry) | | `fgas:read` | [GET /units/{unit}/fgas-entries](https://api.maci.energy/docs/f-gas-log.md#list-f-gas-entries), [GET /units/{unit}/fgas-status](https://api.maci.energy/docs/f-gas-log.md#get-f-gas-status) | | `fgas:write` | [POST /units/{unit}/fgas-entries](https://api.maci.energy/docs/f-gas-log.md#create-f-gas-entry) | | `subscriptions:read` | [GET /subscriptions](https://api.maci.energy/docs/subscriptions.md#list-subscriptions) | | `subscriptions:write` | [PUT /units/{unit}/subscription](https://api.maci.energy/docs/subscriptions.md#assign-subscription), [DELETE /units/{unit}/subscription](https://api.maci.energy/docs/subscriptions.md#unassign-subscription) | | `invoices:read` | [GET /invoice-drafts](https://api.maci.energy/docs/invoicing.md#list-invoice-drafts), [GET /invoice-drafts/{draft}](https://api.maci.energy/docs/invoicing.md#get-invoice-draft) | | `invoices:write` | [POST /invoice-drafts/mark-exported](https://api.maci.energy/docs/invoicing.md#mark-invoice-drafts-exported) | | `price-list:read` | [GET /price-list-items](https://api.maci.energy/docs/invoicing.md#list-price-list-items) | | `price-list:write` | [PUT /price-list-items](https://api.maci.energy/docs/invoicing.md#put-price-list-items) | | `alarms:read` | [GET /alarms](https://api.maci.energy/docs/alarms.md#list-alarms), [GET /alarms/{alarm}](https://api.maci.energy/docs/alarms.md#get-alarm) | | `alarms:write` | [POST /alarms/{alarm}/acknowledge](https://api.maci.energy/docs/alarms.md#acknowledge-alarm), [POST /alarms/{alarm}/close](https://api.maci.energy/docs/alarms.md#close-alarm) | | any key | [GET /refrigerants](https://api.maci.energy/docs/refrigerants-and-usage.md#list-refrigerants), [GET /usage](https://api.maci.energy/docs/refrigerants-and-usage.md#get-usage) | --- # Lists, limits and versions The rules every endpoint follows: the format, paging, repeated calls, how many calls a key may make, and how the API changes. ## Format JSON in UTF-8. Times are ISO 8601 in UTC. IDs are UUIDs. Measurements are in SI units. Error messages are in English, or in Danish with `Accept-Language: da`. ## Lists and paging Lists are paged with `limit` (at most 100, default 50) and `cursor`. The answer has `data` and `next_cursor`. Send `next_cursor` as `cursor` to get the next page. It is `null` on the last page. ```bash curl https://api.maci.energy/v1/units?limit=50&cursor=eyJpZCI6IjBiNmY0YzFlIn0 \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units?limit=50&cursor=eyJpZCI6IjBiNmY0YzFlIn0', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/units?limit=50&cursor=eyJpZCI6IjBiNmY0YzFlIn0", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/units?limit=50&cursor=eyJpZCI6IjBiNmY0YzFlIn0', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ## Repeated calls A POST can be sent with the header `Idempotency-Key`. Sent again within 24 hours with the same value and the same body, it gets the first answer back with the header `Idempotency-Replayed: true`, and no second record is created. Use a value that is unique to the record you are making, such as the ID it has in your own system. A network error is then safe to retry. ## Limits Each key can make 60 calls a minute. Past that the answer is 429 with `Retry-After`. Every answer has the headers `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. On top of that there is a fair usage limit of 100,000 production calls a month per organisation, unless otherwise agreed. The organisation's admins are told at 80 % and at 100 %. At 200 % the keys are slowed down to 10 calls a minute until Maci has looked at the usage. | Header | What it says | | --- | --- | | `X-RateLimit-Limit` | Calls a minute the key may make. | | `X-RateLimit-Remaining` | Calls left this minute. | | `X-RateLimit-Reset` | When the minute starts over, as a Unix time. | | `Retry-After` | Only at 429: the seconds to wait. | What a key and the organisation have used this month: [GET /usage](https://api.maci.energy/docs/refrigerants-and-usage.md#get-usage). ## Versions The version is in the path. Changes that break existing integrations come in a new version, and the old one works for at least 12 months after notice. --- # Errors The API answers with an ordinary HTTP status. An error also has a body with a fixed code your system can act on and a message for a person. ## Statuses Errors are returned as JSON with `code` and `message`. At 422 the answer also has `errors` with one field per error. | Status | What it means | | --- | --- | | `400` | Invalid request. | | `401` | The key is missing, deleted or expired. | | `403` | The key does not have the scope, the organisation has no access to the unit, or the subscription is not active. | | `404` | Not found. | | `409` | The request cannot be carried out as things stand, for example because the component is already replaced. | | `422` | Validation failed. The answer has `errors` with one field per error. | | `429` | The request limit is reached. Wait the number of seconds given in `Retry-After`. | ## The error model - `code` (string, required): A fixed code, such as `unauthenticated`, `missing_scope`, `forbidden`, `subscription_inactive`, `blocked`, `not_found`, `conflict`, `validation_failed` or `rate_limited`. - `message` (string, required): An explanation for a person. - `errors` (object): Only at 422. One field per error, each with a list of messages. ```json { "code": "missing_scope", "message": "The key does not have the scope units:write." } ``` ```json { "code": "validation_failed", "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` ## Handling errors - Act on `code`, which does not change. `message` is for a person and may be reworded. - At 429, wait the seconds in `Retry-After` and try again. - At 5xx or a network error, try again with a growing pause. Send `Idempotency-Key` with a POST, so a retry never makes two records. - At 401, the key is missing, deleted or expired: make a new one in the app. --- # Webhooks Maci can post events to an address of your own, so your system hears about an alarm or an approved invoice basis at once, signed with a secret only you and Maci know. ## Setting up a webhook Your system does not have to keep asking the API. Set an address of your own in the app, and Maci posts each event to it as it happens. Each organisation sets its own address and gets a secret: the address for alarms under [API-nøgler](https://app.maci.energy/organization/api-keys), the one for invoice bases under **Fakturagrundlag**, Indstillinger. | Header | What it says | | --- | --- | | `X-Maci-Event` | The event, for example `alarm.raised`. The same as `event` in the body. | | `X-Maci-Signature` | `sha256=` and the HMAC-SHA256 of the raw body, made with the webhook's secret, in hex. | | `Content-Type` | `application/json` | ## Checking the signature Make the HMAC-SHA256 of the body exactly as it arrived, before any JSON parsing, with the secret, and compare it with `X-Maci-Signature` in constant time. If they differ, the call is not from Maci. ```php $expected = 'sha256='.hash_hmac('sha256', $rawBody, $secret); if (! hash_equals($expected, $_SERVER['HTTP_X_MACI_SIGNATURE'] ?? '')) { http_response_code(401); exit; } ``` ```javascript const crypto = require('crypto'); const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); const given = req.get('X-Maci-Signature') || ''; const ok = given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected)); ``` ```python import hashlib import hmac expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() ok = hmac.compare_digest(expected, request.headers.get("X-Maci-Signature", "")) ``` ## Answering and retries - The address must be https and on the public internet. Redirects are not followed. - Answer with a 2xx within 10 seconds and do the work afterwards. Anything else is tried again, with the pauses given for each webhook below. - An event can arrive more than once, for example when your answer came too late. Use `data.id` and `event` to recognise a repeat. - Everything a webhook sends can also be fetched with the API, so a missed event can be caught up. ## Alarm webhook An alarm's event, posted to the organisation's webhook address for alarms. Set the address and get the secret under **API keys** in the app. `X-Maci-Event` is `alarm.raised`, `alarm.cleared`, `alarm.acknowledged` or `alarm.closed`, and `data` is the alarm as `GET /alarms/{alarm}` gives it at the moment it is sent. Answer with a 2xx within 10 seconds; anything else is tried again after 10 seconds, 30 seconds, 2 minutes, 10 minutes and 30 minutes. The SMS and mail to the recipients do not depend on the webhook, and the alarms can always be fetched with `GET /alarms`. Events: `alarm.raised`, `alarm.cleared`, `alarm.acknowledged`, `alarm.closed`. - `event` (string, required): The same as the header `X-Maci-Event`. One of `alarm.raised`, `alarm.cleared`, `alarm.acknowledged`, `alarm.closed`. - `created_at` (string (date-time), required): When the event was sent. - `data` (object, required) See the alarm model. ```json { "event": "alarm.raised", "created_at": "2026-10-30T02:14:07Z", "data": { "id": "7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "unit_public_id": "MC-0482", "unit": "Cold room 2", "site": "Main store", "type": "rum-for-varmt", "name": "Room too warm", "severity": "HIGH", "state": "raised", "open": true, "values": { "room_temp_c": "9.4" }, "raised_at": "2026-10-30T02:14:05Z", "cleared_at": null, "level": 0, "watch": null, "acknowledged_at": null, "acknowledged_by": null, "closed_at": null, "closed_by": null, "result": null, "note": null } } ``` The fields of `data` are those of [the alarm model](https://api.maci.energy/docs/alarms.md#the-alarm-model). ## Invoice basis webhook An approved invoice basis, posted to the service company's webhook address. Set the address and get the secret under **Invoice basis**, Settings, in the app. `X-Maci-Event` is `invoice_draft.approved`, and `data` is the invoice basis as `GET /invoice-drafts/{draft}` gives it. Answer with a 2xx within 10 seconds; anything else is tried again after 1 minute, 5 minutes, 30 minutes and 2 hours. The bases can always be fetched with `GET /invoice-drafts` as well. Events: `invoice_draft.approved`. - `event` (string, required): The same as the header `X-Maci-Event`. One of `invoice_draft.approved`. - `created_at` (string (date-time), required): When the event was sent. - `data` (object, required) See the invoice draft model. ```json { "event": "invoice_draft.approved", "created_at": "2026-10-30T14:02:51Z", "data": { "id": "3c8d2a71-6e0b-4f95-a2d4-8b1e7c5f0a93", "status": "approved", "service_report_id": "9a4f0e2b-7c13-4d68-b5e1-2d9c6a3f8b07", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "unit_public_id": "MC-0482", "unit": "Cold room 2", "customer": { "name": "Example Foods ApS", "cvr": "12345678", "vat_number": null }, "visit_date": "2026-10-30", "technician": "Jonas L.", "currency": "DKK", "total_excl_vat": 1890, "approved_at": "2026-10-30T14:02:49Z", "exported_at": null, "lines": [ { "type": "labour", "item_number": "1000", "description": "Labour", "quantity": 1.5, "unit_of_measure": "hours", "unit_price": 700, "amount_excl_vat": 1050, "source": "speech", "match_sure": true }, { "type": "travel", "item_number": "1010", "description": "Call-out", "quantity": 1, "unit_of_measure": "pcs", "unit_price": 840, "amount_excl_vat": 840, "source": "speech", "match_sure": true } ] } } ``` The fields of `data` are those of [the invoice draft model](https://api.maci.energy/docs/invoicing.md#the-invoice-draft-model). --- # AI assistants (MCP) Connect Maci to Claude, ChatGPT or another assistant that speaks the Model Context Protocol, and ask it about your refrigeration units in your own words. It reads through the API, so it sees what a key of yours would see, and it cannot change anything. ## What it is Maci's MCP server is at `https://mcp.maci.energy/mcp`. An assistant connected to it can answer questions such as: - Which of our units are due a leak check in November? - Are there open alarms at the Vesterbro store, and has anyone taken them? - When was MC-0482 last serviced, and what was done? - How much R404A do we have in our cold rooms, in tonnes of CO2 equivalent? The assistant calls the tools below, each one a GET of this API, and answers from what comes back. Nothing is written, created or deleted through it. ## Connecting an assistant An admin of your organisation connects it once. In the assistant, add a custom connector (in Claude and ChatGPT it is under Settings, Connectors) with the address `https://mcp.maci.energy/mcp`. The assistant then sends you to Maci, where you log in as always, choose the organisation and say yes with your passkey. Only an admin can connect an organisation, and a customer only once at least one unit has Maci Service, as with an API key. A refrigeration company needs none of its own: it reads the units it has a grant on. Setup for Claude Code, Cursor, VS Code and other clients, example questions and skills (step-by-step tasks such as a leak check plan or an alarm report) are in [Maci-Intelligence/developers](https://github.com/Maci-Intelligence/developers) on GitHub, with webhook receivers and scripts for the API. ## Tools The assistant sees only the tools of the scopes it was given. The answers are the API's own, with the fields of the [reference](https://api.maci.energy/docs/units.md). | Tool | What it gives | Scope | | --- | --- | --- | | `list_sites` | The organisation's sites (stores, warehouses, kitchens) and the customers' sites it has units on through a grant. Each has an id, a name and an address. | `units:read` | | `list_units` | The refrigeration units the organisation works with: its own, and customers' units it has a grant on. Each has its Maci-ID (MC-0482), name, site, refrigerant, charge and status. Filter by site, status or what changed since a time. | `units:read` | | `get_unit` | One refrigeration unit's passport: its master data, nameplate, refrigerant, charge and CO2 equivalent, site and components. | `units:read` | | `list_components` | The components of a unit: compressors, evaporators, condensers, controllers, valves and the rest, each with its type and fields. | `units:read` | | `list_service_entries` | The service log of a unit: visits, repairs and checks, oldest first, each with its date, type and text. | `service:read` | | `get_fgas_status` | Whether a unit's leak checks under the F-gas regulation are up to date: how often it must be checked, when it was last checked and when the next is due. Needs Maci Service on the unit. | `fgas:read` | | `list_fgas_entries` | The F-log of a unit, the logbook the F-gas regulation asks for: leak checks, refrigerant added and recovered, repairs, oldest first. Entries are never changed: a correction is a new entry. Needs Maci Service on the unit. | `fgas:read` | | `list_alarms` | Alarms from Maci Intelligence on the units the organisation owns or watches: what was wrong, on which unit, when it started and cleared, and who took it. Filter by open, unit or since a time. | `alarms:read` | | `get_alarm` | One alarm from Maci Intelligence with its values, its state, who was told and who took and closed it. | `alarms:read` | | `get_usage` | How many calls this connection and the organisation have made this month, and the month's limit. | any | ## What an assistant can reach - Only reading: the scopes `units:read`, `service:read`, `fgas:read`, `alarms:read`. No tool changes anything in Maci, so nothing a note or a mail says can make the assistant act there. - Only what your organisation may see in Maci, by the same rules as a key: your own units, and customers' units you have a grant on. - Every tool call is a call to the API: it is logged, it counts towards the month's limit, and it is held to the limit a minute, under [API-nøgler](https://app.maci.energy/organization/api-keys). - Your data goes to the assistant you chose, and from there to its provider. Connect only an assistant your organisation is allowed to use. ## Removing a connection The connections are listed under [API-nøgler](https://app.maci.energy/organization/api-keys), **Forbundne AI-assistenter**, with who made each one and when it was last used. Removing one stops it at once. A connection that is not used for 30 days ends by itself. ## For developers: the OAuth flow The server follows the MCP specification's authorization (2026-07-28). Clients that support it need nothing more than the address. | Part | How | | --- | --- | | Protected resource metadata | `https://mcp.maci.energy/.well-known/oauth-protected-resource/mcp` (RFC 9728) | | Authorization server | `https://app.maci.energy`, metadata at `https://app.maci.energy/.well-known/oauth-authorization-server` (RFC 8414) | | Clients | A Client ID Metadata Document (an https `client_id`), or Dynamic Client Registration (RFC 7591) at `https://app.maci.energy/oauth/register`. Public clients only: `token_endpoint_auth_method` `none`. | | Flow | Authorization code with PKCE, `S256` only. The redirect address must match exactly. Send `resource` with `https://mcp.maci.energy/mcp`: the token works there and nowhere else, not on `https://api.maci.energy/v1`. | | Tokens | An access token (`maci_oauth_…`) lasts 60 minutes. A refresh token (`maci_refresh_…`) is used once and gives a new pair; one used again ends the connection. | | Revocation | `https://app.maci.energy/oauth/revoke` (RFC 7009): a refresh token ends the connection. | --- # Units A unit is one refrigeration system, display case, cold room or other piece of equipment with its own QR or NFC tag, and a site is where it stands. A unit has master data from its nameplate, its refrigerant and charge with the CO2 equivalent worked out, and its active components. The API reaches the units the organisation owns and, for a service company, the units where it has an active grant. ## The unit model - `id` (string (uuid)) - `public_id` (string): The Maci ID, such as `MC-0482`. - `name` (string) - `category` (string) - `status` (string) One of `active`, `decommissioned`. - `organization_id` (string (uuid)): The owner. - `site_id` (string (uuid), nullable) - `parent_unit_id` (string (uuid), nullable) - `manufacturer` (string, nullable) - `model` (string, nullable) - `serial_no` (string, nullable) - `production_year` (integer, nullable) - `installation_year` (integer, nullable) - `system_type` (string, nullable) - `refrigerant` (string, nullable): The designation of the refrigerant. - `charge_kg` (number, nullable) - `co2e_tonnes` (number, nullable): The charge in tonnes of CO2 equivalent. - `charge_is_on_parent` (boolean): `true` when refrigerant and charge are those of the central system. - `premium` (boolean): Whether the unit has Premium. The API only reaches units that have it. - `hermetically_sealed` (boolean) - `leak_detection_system` (boolean) - `temp_min_c` (number, nullable) - `temp_max_c` (number, nullable) - `supply_voltage_v` (integer, nullable) - `supply_phases` (integer, nullable) - `supply_frequency_hz` (integer, nullable) - `rated_current_a` (number, nullable): Rated current in A, from the nameplate. - `power_kw` (number, nullable): Power in kW, from the nameplate. - `ps_high_bar` (number, nullable): Allowable pressure PS on the high side, in bar. - `ps_low_bar` (number, nullable): Allowable pressure PS on the low side, in bar. - `climate_class` (string, nullable) - `created_at` (string (date-time)) - `updated_at` (string (date-time)) - `components` (array): Active components. Only there when one unit is fetched. See the component model. ## The site model - `id` (string (uuid)) - `organization_id` (string (uuid)) - `name` (string) - `address` (string, nullable) - `created_at` (string (date-time)) - `updated_at` (string (date-time)) ## List sites `GET /sites` · scope `units:read` The organisation's sites. The organisation's own sites. For a service company also the sites where the company has access to units. ### Query parameters - `limit` (integer): Records per page. At most 100, default 50. - `cursor` (string): The value of `next_cursor` from the page before. ### Request ```bash curl https://api.maci.energy/v1/sites \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/sites', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/sites", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/sites', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 A page of sites. The fields are those of the site model. ```json { "data": [ { "id": "5d0c7e52-8a0b-4c3e-b1a4-7c1f6a2d9e30", "organization_id": "a3f1c2d4-6b8e-4f0a-9c1d-2e3f4a5b6c7d", "name": "Main store", "address": "Example Street 1, 2100 Copenhagen", "created_at": "2026-10-30T09:15:00Z", "updated_at": "2026-10-30T09:15:00Z" } ], "next_cursor": null } ``` ## List units `GET /units` · scope `units:read` The units the organisation has access to. ### Query parameters - `site_id` (string (uuid)): Only units at this site. - `status` (string): Only units with this status, `active` or `decommissioned`. One of `active`, `decommissioned`. - `updated_since` (string (date-time)): Only units changed at or after this time. - `limit` (integer): Records per page. At most 100, default 50. - `cursor` (string): The value of `next_cursor` from the page before. ### Request ```bash curl https://api.maci.energy/v1/units \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/units", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/units', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 A page of units, without components. The fields are those of the unit model. ```json { "data": [ { "id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "public_id": "MC-0482", "name": "Cold room 2", "category": "cold_room", "status": "active", "organization_id": "a3f1c2d4-6b8e-4f0a-9c1d-2e3f4a5b6c7d", "site_id": "5d0c7e52-8a0b-4c3e-b1a4-7c1f6a2d9e30", "parent_unit_id": null, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "production_year": 2019, "installation_year": 2020, "system_type": "split", "refrigerant": "R449A", "charge_kg": 42, "co2e_tonnes": 58.7, "charge_is_on_parent": false, "premium": true, "hermetically_sealed": false, "leak_detection_system": false, "temp_min_c": 0, "temp_max_c": 4, "supply_voltage_v": 400, "supply_phases": 3, "supply_frequency_hz": 50, "rated_current_a": 12.4, "power_kw": 5.5, "ps_high_bar": 28, "ps_low_bar": 19, "climate_class": "N", "created_at": "2026-10-30T09:15:00Z", "updated_at": "2026-10-30T09:15:00Z", "components": [ { "id": "c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "type": "compressor", "label": "Compressor 1", "quantity": 1, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "status": "active", "installed_at": "2026-10-30", "removed_at": null, "replaces_component_id": null, "attributes": {}, "custom_fields": null, "note": null, "created_at": "2026-10-30T09:15:00Z", "updated_at": "2026-10-30T09:15:00Z" } ] } ], "next_cursor": null } ``` ## Create unit `POST /units` · scope `units:write` Create a unit. The unit is created at one of the organisation's sites. A service company can create at a customer's site when its grant includes creation. ### Headers - `Idempotency-Key` (string): A value of your own choosing, so that a repeated call does not create two records. ### Body (JSON) - `site_id` (string (uuid), required): The site the unit is at. - `name` (string, required): The name of the unit. - `category` (string, required) One of `display_case`, `freezer_case`, `plug_in_cabinet`, `cold_room`, `freezer_room`, `blast_chiller`, `condensing_unit`, `compressor_rack`, `chiller`, `heat_pump`, `other`. - `tag_code` (string, nullable): The code on a pre-printed label that is not in use. The unit gets the Maci ID of the label. Not in the sandbox. - `manufacturer` (string, nullable) - `model` (string, nullable) - `serial_no` (string, nullable) - `production_year` (integer, nullable) - `installation_year` (integer, nullable) - `system_type` (string, nullable) One of `plug_in`, `split`, `central`. - `parent_unit_id` (string (uuid), nullable): The central system the unit is connected to. It has to be one of the owner's units. - `refrigerant` (string, nullable): The designation of the refrigerant, such as `R449A`. See GET /refrigerants. - `refrigerant_id` (string (uuid), nullable): The ID of the refrigerant. Can be used instead of `refrigerant`. - `charge_kg` (number, nullable): The charge in kg. - `hermetically_sealed` (boolean) - `leak_detection_system` (boolean) - `temp_min_c` (number, nullable) - `temp_max_c` (number, nullable) - `supply_voltage_v` (integer, nullable) - `supply_phases` (integer, nullable) One of `1`, `3`. - `supply_frequency_hz` (integer, nullable) One of `50`, `60`. - `rated_current_a` (number, nullable): Rated current in A, from the nameplate. - `power_kw` (number, nullable): Power in kW, from the nameplate. - `ps_high_bar` (number, nullable): Allowable pressure PS on the high side, in bar. - `ps_low_bar` (number, nullable): Allowable pressure PS on the low side, in bar. - `climate_class` (string, nullable) ### Request ```bash curl -X POST https://api.maci.energy/v1/units \ -H "Authorization: Bearer $MACI_API_KEY" \ -H "Idempotency-Key: a-value-of-your-own" \ -H "Content-Type: application/json" \ -d '{ "site_id": "5d0c7e52-8a0b-4c3e-b1a4-7c1f6a2d9e30", "tag_code": "MC-0482", "name": "Cold room 2", "category": "cold_room", "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "production_year": 2019, "installation_year": 2020 }' ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, 'Idempotency-Key': 'a-value-of-your-own', 'Content-Type': 'application/json', }, body: JSON.stringify({ "site_id": "5d0c7e52-8a0b-4c3e-b1a4-7c1f6a2d9e30", "tag_code": "MC-0482", "name": "Cold room 2", "category": "cold_room", "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "production_year": 2019, "installation_year": 2020 }), }); const data = await response.json(); ``` ```python import os import requests response = requests.post( "https://api.maci.energy/v1/units", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}", "Idempotency-Key": "a-value-of-your-own"}, json={ "site_id": "5d0c7e52-8a0b-4c3e-b1a4-7c1f6a2d9e30", "tag_code": "MC-0482", "name": "Cold room 2", "category": "cold_room", "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "production_year": 2019, "installation_year": 2020 }, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('POST', 'https://api.maci.energy/v1/units', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), 'Idempotency-Key' => 'a-value-of-your-own', ], 'json' => [ 'site_id' => '5d0c7e52-8a0b-4c3e-b1a4-7c1f6a2d9e30', 'tag_code' => 'MC-0482', 'name' => 'Cold room 2', 'category' => 'cold_room', 'manufacturer' => 'Example Ltd', 'model' => 'K-100', 'serial_no' => 'SN-204871', 'production_year' => 2019, 'installation_year' => 2020, ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 201 The unit that was created. The fields are those of the unit model. ```json { "id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "public_id": "MC-0482", "name": "Cold room 2", "category": "cold_room", "status": "active", "organization_id": "a3f1c2d4-6b8e-4f0a-9c1d-2e3f4a5b6c7d", "site_id": "5d0c7e52-8a0b-4c3e-b1a4-7c1f6a2d9e30", "parent_unit_id": null, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "production_year": 2019, "installation_year": 2020, "system_type": "split", "refrigerant": "R449A", "charge_kg": 42, "co2e_tonnes": 58.7, "charge_is_on_parent": false, "premium": true, "hermetically_sealed": false, "leak_detection_system": false, "temp_min_c": 0, "temp_max_c": 4, "supply_voltage_v": 400, "supply_phases": 3, "supply_frequency_hz": 50, "rated_current_a": 12.4, "power_kw": 5.5, "ps_high_bar": 28, "ps_low_bar": 19, "climate_class": "N", "created_at": "2026-10-30T09:15:00Z", "updated_at": "2026-10-30T09:15:00Z", "components": [ { "id": "c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "type": "compressor", "label": "Compressor 1", "quantity": 1, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "status": "active", "installed_at": "2026-10-30", "removed_at": null, "replaces_component_id": null, "attributes": {}, "custom_fields": null, "note": null, "created_at": "2026-10-30T09:15:00Z", "updated_at": "2026-10-30T09:15:00Z" } ] } ``` ## Get unit `GET /units/{unit}` · scope `units:read` Master data, refrigerant, charge and active components. ### Path parameters - `unit` (string (uuid), required): The ID of the unit. ### Request ```bash curl https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11 \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 The unit with its active components. The fields are those of the unit model. ```json { "id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "public_id": "MC-0482", "name": "Cold room 2", "category": "cold_room", "status": "active", "site_id": "5d0c7e52-8a0b-4c3e-b1a4-7c1f6a2d9e30", "refrigerant": "R449A", "charge_kg": 42, "co2e_tonnes": 58.7, "components": [ { "id": "c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66", "type": "compressor", "label": "Compressor 1", "manufacturer": "Example Ltd", "model": "K-100", "status": "active", "attributes": { "rated_current_a": 9.8, "motor_power_kw": 3.7 } } ] } ``` ## Update unit `PATCH /units/{unit}` · scope `units:write` Change master data. Send only the fields to change. ### Path parameters - `unit` (string (uuid), required): The ID of the unit. ### Body (JSON) - `name` (string): The name of the unit. - `category` (string) One of `display_case`, `freezer_case`, `plug_in_cabinet`, `cold_room`, `freezer_room`, `blast_chiller`, `condensing_unit`, `compressor_rack`, `chiller`, `heat_pump`, `other`. - `manufacturer` (string, nullable) - `model` (string, nullable) - `serial_no` (string, nullable) - `production_year` (integer, nullable) - `installation_year` (integer, nullable) - `system_type` (string, nullable) One of `plug_in`, `split`, `central`. - `parent_unit_id` (string (uuid), nullable): The central system the unit is connected to. It has to be one of the owner's units. - `refrigerant` (string, nullable): The designation of the refrigerant, such as `R449A`. See GET /refrigerants. - `refrigerant_id` (string (uuid), nullable): The ID of the refrigerant. Can be used instead of `refrigerant`. - `charge_kg` (number, nullable): The charge in kg. - `hermetically_sealed` (boolean) - `leak_detection_system` (boolean) - `temp_min_c` (number, nullable) - `temp_max_c` (number, nullable) - `supply_voltage_v` (integer, nullable) - `supply_phases` (integer, nullable) One of `1`, `3`. - `supply_frequency_hz` (integer, nullable) One of `50`, `60`. - `rated_current_a` (number, nullable): Rated current in A, from the nameplate. - `power_kw` (number, nullable): Power in kW, from the nameplate. - `ps_high_bar` (number, nullable): Allowable pressure PS on the high side, in bar. - `ps_low_bar` (number, nullable): Allowable pressure PS on the low side, in bar. - `climate_class` (string, nullable) ### Request ```bash curl -X PATCH https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11 \ -H "Authorization: Bearer $MACI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Cold room 2", "category": "cold_room", "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "production_year": 2019 }' ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11', { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "name": "Cold room 2", "category": "cold_room", "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "production_year": 2019 }), }); const data = await response.json(); ``` ```python import os import requests response = requests.patch( "https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, json={ "name": "Cold room 2", "category": "cold_room", "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "production_year": 2019 }, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('PATCH', 'https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], 'json' => [ 'name' => 'Cold room 2', 'category' => 'cold_room', 'manufacturer' => 'Example Ltd', 'model' => 'K-100', 'serial_no' => 'SN-204871', 'production_year' => 2019, ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 The unit after the change. The fields are those of the unit model. ```json { "id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "public_id": "MC-0482", "name": "Cold room 2", "category": "cold_room", "status": "active", "organization_id": "a3f1c2d4-6b8e-4f0a-9c1d-2e3f4a5b6c7d", "site_id": "5d0c7e52-8a0b-4c3e-b1a4-7c1f6a2d9e30", "parent_unit_id": null, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "production_year": 2019, "installation_year": 2020, "system_type": "split", "refrigerant": "R449A", "charge_kg": 42, "co2e_tonnes": 58.7, "charge_is_on_parent": false, "premium": true, "hermetically_sealed": false, "leak_detection_system": false, "temp_min_c": 0, "temp_max_c": 4, "supply_voltage_v": 400, "supply_phases": 3, "supply_frequency_hz": 50, "rated_current_a": 12.4, "power_kw": 5.5, "ps_high_bar": 28, "ps_low_bar": 19, "climate_class": "N", "created_at": "2026-10-30T09:15:00Z", "updated_at": "2026-10-30T09:15:00Z", "components": [ { "id": "c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "type": "compressor", "label": "Compressor 1", "quantity": 1, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "status": "active", "installed_at": "2026-10-30", "removed_at": null, "replaces_component_id": null, "attributes": {}, "custom_fields": null, "note": null, "created_at": "2026-10-30T09:15:00Z", "updated_at": "2026-10-30T09:15:00Z" } ] } ``` --- # Components The parts of a unit: compressors, fans, valves, controllers and the rest. A component is never deleted. When it is replaced, the old one is kept with the status `replaced` and the replacement goes into the service history, so the unit's history stays whole. ## The component model - `id` (string (uuid)) - `unit_id` (string (uuid)) - `type` (string) - `label` (string) - `quantity` (integer) - `manufacturer` (string, nullable) - `model` (string, nullable) - `serial_no` (string, nullable) - `status` (string) One of `active`, `replaced`, `removed`. - `installed_at` (string (date), nullable) - `removed_at` (string (date), nullable) - `replaces_component_id` (string (uuid), nullable): The component this one replaced. - `attributes` (object) - `custom_fields` (object, nullable) - `note` (string, nullable) - `created_at` (string (date-time)) - `updated_at` (string (date-time)) ## List components `GET /units/{unit}/components` · scope `units:read` The components of a unit. ### Path parameters - `unit` (string (uuid), required): The ID of the unit. ### Query parameters - `include_replaced` (boolean): `true` includes earlier components. - `limit` (integer): Records per page. At most 100, default 50. - `cursor` (string): The value of `next_cursor` from the page before. ### Request ```bash curl https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/components \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/components', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/components", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/components', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 A page of components. The fields are those of the component model. ```json { "data": [ { "id": "c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "type": "compressor", "label": "Compressor 1", "quantity": 1, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "status": "active", "installed_at": "2026-10-30", "removed_at": null, "replaces_component_id": null, "attributes": {}, "custom_fields": null, "note": null, "created_at": "2026-10-30T09:15:00Z", "updated_at": "2026-10-30T09:15:00Z" } ], "next_cursor": null } ``` ## Create component `POST /units/{unit}/components` · scope `units:write` Add a component. ### Path parameters - `unit` (string (uuid), required): The ID of the unit. ### Headers - `Idempotency-Key` (string): A value of your own choosing, so that a repeated call does not create two records. ### Body (JSON) - `type` (string, required) One of `compressor`, `condenser`, `evaporator`, `fan`, `expansion_device`, `controller`, `defrost_heater`, `sensor`, `valve`, `vessel`, `cabinet`, `maci_sensor`, `other`. - `label` (string, required): The name of the component, such as `Compressor 1`. - `quantity` (integer, nullable) - `manufacturer` (string, nullable) - `model` (string, nullable) - `serial_no` (string, nullable) - `installed_at` (string (date), nullable) - `note` (string, nullable) - `attributes` (object, nullable): The type's own fields, such as `rated_current_a` and `motor_power_kw` for a compressor. Unknown fields are refused with 422. - `custom_fields` (object, nullable): Fields of your own, as name and text. At most 20. ### Request ```bash curl -X POST https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/components \ -H "Authorization: Bearer $MACI_API_KEY" \ -H "Idempotency-Key: a-value-of-your-own" \ -H "Content-Type: application/json" \ -d '{ "type": "fan", "label": "Evaporator fan 1", "quantity": 2, "attributes": { "placement": "evaporator", "motor_type": "EC", "power_w": 38 } }' ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/components', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, 'Idempotency-Key': 'a-value-of-your-own', 'Content-Type': 'application/json', }, body: JSON.stringify({ "type": "fan", "label": "Evaporator fan 1", "quantity": 2, "attributes": { "placement": "evaporator", "motor_type": "EC", "power_w": 38 } }), }); const data = await response.json(); ``` ```python import os import requests response = requests.post( "https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/components", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}", "Idempotency-Key": "a-value-of-your-own"}, json={ "type": "fan", "label": "Evaporator fan 1", "quantity": 2, "attributes": { "placement": "evaporator", "motor_type": "EC", "power_w": 38 } }, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('POST', 'https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/components', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), 'Idempotency-Key' => 'a-value-of-your-own', ], 'json' => [ 'type' => 'fan', 'label' => 'Evaporator fan 1', 'quantity' => 2, 'attributes' => [ 'placement' => 'evaporator', 'motor_type' => 'EC', 'power_w' => 38, ], ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 201 The component that was created. The fields are those of the component model. ```json { "id": "c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "type": "compressor", "label": "Compressor 1", "quantity": 1, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "status": "active", "installed_at": "2026-10-30", "removed_at": null, "replaces_component_id": null, "attributes": {}, "custom_fields": null, "note": null, "created_at": "2026-10-30T09:15:00Z", "updated_at": "2026-10-30T09:15:00Z" } ``` ## Update component `PATCH /components/{component}` · scope `units:write` Change a component. Send only the fields to change. A component that is replaced or removed cannot be changed (409). ### Path parameters - `component` (string (uuid), required): The ID of the component. ### Body (JSON) - `label` (string): The name of the component, such as `Compressor 1`. - `quantity` (integer, nullable) - `manufacturer` (string, nullable) - `model` (string, nullable) - `serial_no` (string, nullable) - `installed_at` (string (date), nullable) - `note` (string, nullable) - `attributes` (object, nullable): The type's own fields, such as `rated_current_a` and `motor_power_kw` for a compressor. Unknown fields are refused with 422. - `custom_fields` (object, nullable): Fields of your own, as name and text. At most 20. ### Request ```bash curl -X PATCH https://api.maci.energy/v1/components/c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66 \ -H "Authorization: Bearer $MACI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "label": "Compressor 1", "quantity": 1, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "installed_at": "2026-10-30" }' ``` ```javascript const response = await fetch('https://api.maci.energy/v1/components/c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66', { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "label": "Compressor 1", "quantity": 1, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "installed_at": "2026-10-30" }), }); const data = await response.json(); ``` ```python import os import requests response = requests.patch( "https://api.maci.energy/v1/components/c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, json={ "label": "Compressor 1", "quantity": 1, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "installed_at": "2026-10-30" }, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('PATCH', 'https://api.maci.energy/v1/components/c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], 'json' => [ 'label' => 'Compressor 1', 'quantity' => 1, 'manufacturer' => 'Example Ltd', 'model' => 'K-100', 'serial_no' => 'SN-204871', 'installed_at' => '2026-10-30', ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 The component after the change. The fields are those of the component model. ```json { "id": "c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "type": "compressor", "label": "Compressor 1", "quantity": 1, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "status": "active", "installed_at": "2026-10-30", "removed_at": null, "replaces_component_id": null, "attributes": {}, "custom_fields": null, "note": null, "created_at": "2026-10-30T09:15:00Z", "updated_at": "2026-10-30T09:15:00Z" } ``` ## Replace component `POST /components/{component}/replace` · scope `units:write` Replace a component. The new component gets the type of the old one. The old one is kept with the status `replaced`, and the replacement is in the service history. ### Path parameters - `component` (string (uuid), required): The ID of the component. ### Headers - `Idempotency-Key` (string): A value of your own choosing, so that a repeated call does not create two records. ### Body (JSON) - `label` (string, required): The name of the component, such as `Compressor 1`. - `quantity` (integer, nullable) - `manufacturer` (string, nullable) - `model` (string, nullable) - `serial_no` (string, nullable) - `installed_at` (string (date), nullable) - `note` (string, nullable) - `attributes` (object, nullable): The type's own fields, such as `rated_current_a` and `motor_power_kw` for a compressor. Unknown fields are refused with 422. - `custom_fields` (object, nullable): Fields of your own, as name and text. At most 20. - `service_text` (string, nullable): Text for the service note about the replacement. ### Request ```bash curl -X POST https://api.maci.energy/v1/components/c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66/replace \ -H "Authorization: Bearer $MACI_API_KEY" \ -H "Idempotency-Key: a-value-of-your-own" \ -H "Content-Type: application/json" \ -d '{ "label": "Compressor 1", "quantity": 1, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "installed_at": "2026-10-30", "attributes": {} }' ``` ```javascript const response = await fetch('https://api.maci.energy/v1/components/c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66/replace', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, 'Idempotency-Key': 'a-value-of-your-own', 'Content-Type': 'application/json', }, body: JSON.stringify({ "label": "Compressor 1", "quantity": 1, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "installed_at": "2026-10-30", "attributes": {} }), }); const data = await response.json(); ``` ```python import os import requests response = requests.post( "https://api.maci.energy/v1/components/c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66/replace", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}", "Idempotency-Key": "a-value-of-your-own"}, json={ "label": "Compressor 1", "quantity": 1, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "installed_at": "2026-10-30", "attributes": {} }, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('POST', 'https://api.maci.energy/v1/components/c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66/replace', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), 'Idempotency-Key' => 'a-value-of-your-own', ], 'json' => [ 'label' => 'Compressor 1', 'quantity' => 1, 'manufacturer' => 'Example Ltd', 'model' => 'K-100', 'serial_no' => 'SN-204871', 'installed_at' => '2026-10-30', 'attributes' => [], ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 201 The new component. The fields are those of the component model. ```json { "id": "c2a9d7f4-1b7e-4e0a-8f55-3a9b1d2c4e66", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "type": "compressor", "label": "Compressor 1", "quantity": 1, "manufacturer": "Example Ltd", "model": "K-100", "serial_no": "SN-204871", "status": "active", "installed_at": "2026-10-30", "removed_at": null, "replaces_component_id": "8d1f3a2c-5b4e-4c7d-9e0f-1a2b3c4d5e6f", "attributes": {}, "custom_fields": null, "note": null, "created_at": "2026-10-30T09:15:00Z", "updated_at": "2026-10-30T09:15:00Z" } ``` --- # Photos Photos of a unit's nameplates, labels and overviews. A nameplate or a label is read by AI into suggestions with a confidence for each field. Nothing on the unit changes until the suggestions are saved with the calls for units and components. ## The photo model - `id` (string (uuid)) - `unit_id` (string (uuid)) - `component_id` (string (uuid), nullable) - `kind` (string) One of `nameplate`, `label`, `overview`. - `label_kind` (string, nullable) - `reading_status` (string, nullable): `null` when the photo is not read. One of `queued`, `running`, `done`, `failed`. - `taken_at` (string (date-time)) ## The photo reading model - `id` (string (uuid)) - `photo_id` (string (uuid)) - `status` (string) One of `queued`, `running`, `done`, `failed`. - `readable` (boolean, nullable) - `fields` (array) See the photo field model. - `components` (array) - `service_label` (object, nullable) - `model` (string, nullable) - `prompt_version` (string) - `applied_at` (string (date-time), nullable) - `created_at` (string (date-time)) ## List photos `GET /units/{unit}/photos` · scope `units:read` Photos of a unit. The unit's nameplates, labels and overviews, with the status of each reading. The photos themselves are seen in the app. ### Path parameters - `unit` (string (uuid), required): The ID of the unit. ### Query parameters - `limit` (integer): Records per page. At most 100, default 50. - `cursor` (string): The value of `next_cursor` from the page before. ### Request ```bash curl https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/photos \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/photos', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/photos", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/photos', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 A page of photos. The fields are those of the photo model. ```json { "data": [ { "id": "4e8a1b2c-3d5f-4a6b-8c9d-0e1f2a3b4c5d", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "component_id": null, "kind": "nameplate", "label_kind": null, "reading_status": "queued", "taken_at": "2026-10-30T09:15:00Z" } ], "next_cursor": null } ``` ## Create photo `POST /units/{unit}/photos` · scope `units:write` Send a photo. A nameplate or a label is read in the background into suggestions. Nothing on the unit changes until the suggestions are saved with the calls for units and components. An overview is never read. ### Path parameters - `unit` (string (uuid), required): The ID of the unit. ### Headers - `Idempotency-Key` (string): A value of your own choosing, so that a repeated call does not create two records. ### Form fields (multipart/form-data) - `kind` (string, required) One of `nameplate`, `label`, `overview`. - `photo` (string (binary), required): JPEG, PNG or WebP of at most 10 MB. - `label_kind` (string, nullable): For a label: which kind. One of `service`, `refrigerant`, `ce`, `leak_check`, `other`. - `component_id` (string (uuid), nullable): A photo of one of the unit's components. ### Request ```bash curl -X POST https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/photos \ -H "Authorization: Bearer $MACI_API_KEY" \ -H "Idempotency-Key: a-value-of-your-own" \ -F "kind=nameplate" \ -F "photo=@nameplate.jpg" ``` ```javascript const form = new FormData(); form.append('kind', 'nameplate'); form.append('photo', fileInput.files[0]); // nameplate.jpg const response = await fetch('https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/photos', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, 'Idempotency-Key': 'a-value-of-your-own', }, body: form, }); const data = await response.json(); ``` ```python import os import requests response = requests.post( "https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/photos", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}", "Idempotency-Key": "a-value-of-your-own"}, data={"kind": "nameplate"}, files={"photo": open("nameplate.jpg", "rb")}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('POST', 'https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/photos', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), 'Idempotency-Key' => 'a-value-of-your-own', ], 'multipart' => [ ['name' => 'kind', 'contents' => 'nameplate'], ['name' => 'photo', 'contents' => fopen('nameplate.jpg', 'r')], ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 201 The photo that was saved. The fields are those of the photo model. ```json { "id": "4e8a1b2c-3d5f-4a6b-8c9d-0e1f2a3b4c5d", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "component_id": null, "kind": "nameplate", "label_kind": null, "reading_status": "queued", "taken_at": "2026-10-30T09:15:00Z" } ``` ## Get photo reading `GET /photos/{photo}/reading` · scope `units:read` What was read on a photo. The newest reading, with each field's value in SI units, the text it was read from, the confidence (high, medium or low) and where it stands in the photo. ### Path parameters - `photo` (string (uuid), required): The ID of the photo. ### Request ```bash curl https://api.maci.energy/v1/photos/4e8a1b2c-3d5f-4a6b-8c9d-0e1f2a3b4c5d/reading \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/photos/4e8a1b2c-3d5f-4a6b-8c9d-0e1f2a3b4c5d/reading', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/photos/4e8a1b2c-3d5f-4a6b-8c9d-0e1f2a3b4c5d/reading", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/photos/4e8a1b2c-3d5f-4a6b-8c9d-0e1f2a3b4c5d/reading', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 The reading. The fields are those of the photo reading model. ```json { "id": "e7981672-d483-458e-a358-811e2de301c8", "photo_id": "4e8a1b2c-3d5f-4a6b-8c9d-0e1f2a3b4c5d", "status": "queued", "readable": false, "fields": [ { "key": "refrigerant", "value": "Value", "raw_text": "R449A 42 kg", "confidence": "high", "bbox": null } ], "components": [ { "type": "rum-for-varmt", "fields": [ { "key": "refrigerant", "value": "Value", "raw_text": "R449A 42 kg", "confidence": "high", "bbox": null } ] } ], "service_label": null, "model": "K-100", "prompt_version": "v3", "applied_at": null, "created_at": "2026-10-30T09:15:00Z" } ``` --- # Service A unit's service history: visits, repairs, fault reports and notes, from the app and from your own systems. ## The service entry model - `id` (string (uuid)) - `unit_id` (string (uuid)) - `component_id` (string (uuid), nullable) - `type` (string) One of `service`, `repair`, `replacement`, `addition`, `inspection`, `other`. - `performed_at` (string (date-time)) - `text` (string, nullable) - `company` (string, nullable): The company that did the work. - `created_at` (string (date-time)) ## List service entries `GET /units/{unit}/service-entries` · scope `service:read` Service history. ### Path parameters - `unit` (string (uuid), required): The ID of the unit. ### Query parameters - `limit` (integer): Records per page. At most 100, default 50. - `cursor` (string): The value of `next_cursor` from the page before. ### Request ```bash curl https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/service-entries \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/service-entries', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/service-entries", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/service-entries', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 A page of service notes. The fields are those of the service entry model. ```json { "data": [ { "id": "bd58f66a-5f7e-494b-a81b-d5429bd71ba2", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "component_id": null, "type": "service", "performed_at": "2026-10-30T09:15:00Z", "text": "Cleaned the condenser and checked the fans.", "company": "Example Cooling ApS", "created_at": "2026-10-30T09:15:00Z" } ], "next_cursor": null } ``` ## Create service entry `POST /units/{unit}/service-entries` · scope `service:write` New service note. ### Path parameters - `unit` (string (uuid), required): The ID of the unit. ### Headers - `Idempotency-Key` (string): A value of your own choosing, so that a repeated call does not create two records. ### Body (JSON) - `type` (string, required): Replacements and additions are written by the system when a component changes. One of `service`, `repair`, `inspection`, `other`. - `text` (string, required) - `performed_at` (string (date-time), nullable): When the work was done. The default is now. ### Request ```bash curl -X POST https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/service-entries \ -H "Authorization: Bearer $MACI_API_KEY" \ -H "Idempotency-Key: a-value-of-your-own" \ -H "Content-Type: application/json" \ -d '{ "type": "service", "text": "Cleaned the condenser and checked the fans.", "performed_at": "2026-10-30T09:15:00Z" }' ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/service-entries', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, 'Idempotency-Key': 'a-value-of-your-own', 'Content-Type': 'application/json', }, body: JSON.stringify({ "type": "service", "text": "Cleaned the condenser and checked the fans.", "performed_at": "2026-10-30T09:15:00Z" }), }); const data = await response.json(); ``` ```python import os import requests response = requests.post( "https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/service-entries", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}", "Idempotency-Key": "a-value-of-your-own"}, json={ "type": "service", "text": "Cleaned the condenser and checked the fans.", "performed_at": "2026-10-30T09:15:00Z" }, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('POST', 'https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/service-entries', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), 'Idempotency-Key' => 'a-value-of-your-own', ], 'json' => [ 'type' => 'service', 'text' => 'Cleaned the condenser and checked the fans.', 'performed_at' => '2026-10-30T09:15:00Z', ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 201 The service note that was created. The fields are those of the service entry model. ```json { "id": "bd58f66a-5f7e-494b-a81b-d5429bd71ba2", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "component_id": null, "type": "service", "performed_at": "2026-10-30T09:15:00Z", "text": "Cleaned the condenser and checked the fans.", "company": "Example Cooling ApS", "created_at": "2026-10-30T09:15:00Z" } ``` --- # F-gas log The F-gas log under regulation (EU) 2024/573 and the unit's status: CO2 equivalent, the leak check interval and when the next check is due. An entry takes the certificates of the company and of the person and cannot be changed afterwards, only corrected by a new entry. Adding entries needs Maci Service Premium on the unit. ## The f gas entry model - `id` (string (uuid)) - `unit_id` (string (uuid)) - `type` (string) - `performed_at` (string (date)) - `refrigerant` (string, nullable) - `quantity_kg` (number, nullable) - `reason` (string, nullable) - `gas_origin` (string, nullable) - `origin_facility_name` (string, nullable) - `origin_facility_address` (string, nullable) - `origin_certificate_no` (string, nullable) - `method` (string, nullable) - `result` (string, nullable) - `leak_location` (string, nullable) - `description` (string, nullable) - `disposal` (string, nullable) - `company_name` (string, nullable) - `company_cvr` (string, nullable) - `company_certificate_no` (string, nullable) - `person_name` (string, nullable) - `person_certificate_no` (string, nullable) - `corrects_entry_id` (string (uuid), nullable): The entry this one corrects. - `is_corrected` (boolean): `true` when a later entry has corrected this one. - `created_at` (string (date-time)) ## The f gas status model - `unit_id` (string (uuid)): The unit that holds the refrigerant. - `refrigerant` (string, nullable) - `covered` (boolean): Whether the unit is covered by leak checks. - `co2e_tonnes` (number, nullable) - `interval_months` (integer, nullable): Months between leak checks. - `last_leak_check_at` (string (date), nullable) - `next_leak_check_due_at` (string (date), nullable) - `follow_up_check` (object, nullable): The follow-up check after a repaired leak. - `state` (string): Where the unit stands towards the next check. - `detection_system_missing` (boolean): `true` when a leak detection system is required but missing. ## List F-gas entries `GET /units/{unit}/fgas-entries` · scope `fgas:read` Entries in the F-gas log. The logbook is kept on the unit that holds the refrigerant. For a unit on a central system it is the logbook of that system. ### Path parameters - `unit` (string (uuid), required): The ID of the unit. ### Query parameters - `limit` (integer): Records per page. At most 100, default 50. - `cursor` (string): The value of `next_cursor` from the page before. ### Request ```bash curl https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/fgas-entries \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/fgas-entries', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/fgas-entries", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/fgas-entries', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 A page of entries. The fields are those of the f gas entry model. ```json { "data": [ { "id": "e03018b5-1854-47dd-ac3b-1275002c8808", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "type": "rum-for-varmt", "performed_at": "2026-10-30", "refrigerant": "R449A", "quantity_kg": 2.5, "reason": "Reason", "gas_origin": "Gas origin", "origin_facility_name": "Example Recovery A/S", "origin_facility_address": "Example Road 2, 8000 Aarhus", "origin_certificate_no": "R-2207", "method": "Method", "result": null, "leak_location": "Flare nut on the liquid line", "description": "Labour", "disposal": "Disposal", "company_name": "Example Cooling ApS", "company_cvr": "12345678", "company_certificate_no": "K-4471", "person_name": "Jonas L.", "person_certificate_no": "P-1182", "corrects_entry_id": null, "is_corrected": false, "created_at": "2026-10-30T09:15:00Z" } ], "next_cursor": null } ``` ## Create F-gas entry `POST /units/{unit}/fgas-entries` · scope `fgas:write` New entry in the F-gas log. Takes the certificates of the company and of the person. An entry cannot be changed afterwards. The unit has to have Premium. ### Path parameters - `unit` (string (uuid), required): The ID of the unit. ### Headers - `Idempotency-Key` (string): A value of your own choosing, so that a repeated call does not create two records. ### Body (JSON) - `type` (string, required) One of `installation`, `top_up`, `recovery`, `leak_check`, `leak_repair`, `detection_system_check`, `decommissioning`. - `performed_at` (string (date), required): The date of the work. - `company_name` (string, required): The company that did the work. - `company_certificate_no` (string, required): The certificate number of the company. - `person_name` (string, required): The person who did the work. - `person_certificate_no` (string, required): The certificate number of the person. - `company_cvr` (string, nullable) - `refrigerant` (string): The designation of the refrigerant. Needed for `installation` and `top_up`. - `refrigerant_id` (string (uuid)): Can be used instead of `refrigerant`. - `quantity_kg` (number): The quantity in kg. Needed for `installation`, `top_up`, `recovery` and `decommissioning`. - `reason` (string): Needed for `top_up`. One of `service`, `leakage`. - `gas_origin` (string): Needed for `top_up`. One of `virgin`, `recycled`, `reclaimed`. - `origin_facility_name` (string): Needed for recycled or reclaimed gas. - `origin_facility_address` (string): Needed for recycled or reclaimed gas. - `origin_certificate_no` (string): Needed for recycled or reclaimed gas. - `method` (string): Needed for `leak_check`. One of `direct`, `indirect`. - `result` (string): For `leak_check`: `tight` or `leak_found`. For `leak_repair`: `repaired` or `not_repaired`. For `detection_system_check`: `ok` or `fault`. - `leak_location` (string): Needed when a leak check finds a leak. - `description` (string): Needed for `leak_repair`. - `disposal` (string): What happened to the refrigerant. Needed for `decommissioning`. ### Request ```bash curl -X POST https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/fgas-entries \ -H "Authorization: Bearer $MACI_API_KEY" \ -H "Idempotency-Key: a-value-of-your-own" \ -H "Content-Type: application/json" \ -d '{ "type": "installation", "performed_at": "2026-10-30", "company_name": "Example Cooling ApS", "company_certificate_no": "K-4471", "person_name": "Jonas L.", "person_certificate_no": "P-1182", "company_cvr": "12345678", "refrigerant": "R449A", "refrigerant_id": "2b7c4d1e-9f0a-4b3c-8d6e-5a4f3e2d1c0b", "quantity_kg": 2.5, "reason": "service", "gas_origin": "virgin" }' ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/fgas-entries', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, 'Idempotency-Key': 'a-value-of-your-own', 'Content-Type': 'application/json', }, body: JSON.stringify({ "type": "installation", "performed_at": "2026-10-30", "company_name": "Example Cooling ApS", "company_certificate_no": "K-4471", "person_name": "Jonas L.", "person_certificate_no": "P-1182", "company_cvr": "12345678", "refrigerant": "R449A", "refrigerant_id": "2b7c4d1e-9f0a-4b3c-8d6e-5a4f3e2d1c0b", "quantity_kg": 2.5, "reason": "service", "gas_origin": "virgin" }), }); const data = await response.json(); ``` ```python import os import requests response = requests.post( "https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/fgas-entries", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}", "Idempotency-Key": "a-value-of-your-own"}, json={ "type": "installation", "performed_at": "2026-10-30", "company_name": "Example Cooling ApS", "company_certificate_no": "K-4471", "person_name": "Jonas L.", "person_certificate_no": "P-1182", "company_cvr": "12345678", "refrigerant": "R449A", "refrigerant_id": "2b7c4d1e-9f0a-4b3c-8d6e-5a4f3e2d1c0b", "quantity_kg": 2.5, "reason": "service", "gas_origin": "virgin" }, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('POST', 'https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/fgas-entries', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), 'Idempotency-Key' => 'a-value-of-your-own', ], 'json' => [ 'type' => 'installation', 'performed_at' => '2026-10-30', 'company_name' => 'Example Cooling ApS', 'company_certificate_no' => 'K-4471', 'person_name' => 'Jonas L.', 'person_certificate_no' => 'P-1182', 'company_cvr' => '12345678', 'refrigerant' => 'R449A', 'refrigerant_id' => '2b7c4d1e-9f0a-4b3c-8d6e-5a4f3e2d1c0b', 'quantity_kg' => 2.5, 'reason' => 'service', 'gas_origin' => 'virgin', ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 201 The entry that was created. The fields are those of the f gas entry model. ```json { "id": "e03018b5-1854-47dd-ac3b-1275002c8808", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "type": "rum-for-varmt", "performed_at": "2026-10-30", "refrigerant": "R449A", "quantity_kg": 2.5, "reason": "Reason", "gas_origin": "Gas origin", "origin_facility_name": "Example Recovery A/S", "origin_facility_address": "Example Road 2, 8000 Aarhus", "origin_certificate_no": "R-2207", "method": "Method", "result": null, "leak_location": "Flare nut on the liquid line", "description": "Labour", "disposal": "Disposal", "company_name": "Example Cooling ApS", "company_cvr": "12345678", "company_certificate_no": "K-4471", "person_name": "Jonas L.", "person_certificate_no": "P-1182", "corrects_entry_id": null, "is_corrected": false, "created_at": "2026-10-30T09:15:00Z" } ``` ## Get F-gas status `GET /units/{unit}/fgas-status` · scope `fgas:read` CO2 equivalent, check interval, latest and next leak check. ### Path parameters - `unit` (string (uuid), required): The ID of the unit. ### Request ```bash curl https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/fgas-status \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/fgas-status', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/fgas-status", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/fgas-status', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 Where the unit stands today. The fields are those of the f gas status model. ```json { "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "refrigerant": "R449A", "covered": true, "co2e_tonnes": 58.7, "interval_months": 6, "last_leak_check_at": "2026-10-30", "next_leak_check_due_at": "2026-10-30", "follow_up_check": null, "state": "State", "detection_system_missing": false } ``` --- # Subscriptions The organisation's Maci Service subscriptions. Each is on one unit or unassigned, and the API only reaches units that have one. Subscriptions are bought in the app; the API decides which units have them. ## The subscription model - `id` (string (uuid)) - `product` (string) One of `service`. - `status` (string): `cancelling` stops at `cancel_at`; `suspended` is on hold for an unpaid invoice. One of `active`, `cancelling`, `suspended`. - `unit_id` (string (uuid), nullable): The unit it is on, or null when it is unassigned. - `interval` (string) One of `month`, `year`. - `paid_through` (string (date-time), nullable) - `cancel_at` (string (date-time), nullable) - `started_at` (string (date-time), nullable) ## List subscriptions `GET /subscriptions` · scope `subscriptions:read` The organisation's Maci Service subscriptions. Each subscription is on one unit (`unit_id`) or unassigned (`unit_id` is null). Unassigned subscriptions are paid for like the others. Subscriptions are bought in the app. ### Query parameters - `unassigned` (boolean): `true` for only the unassigned subscriptions, `false` for only those on a unit. - `limit` (integer): Records per page. At most 100, default 50. - `cursor` (string): The value of `next_cursor` from the page before. ### Request ```bash curl https://api.maci.energy/v1/subscriptions \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/subscriptions', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/subscriptions", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/subscriptions', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 A page of subscriptions. The fields are those of the subscription model. ```json { "data": [ { "id": "6f2d9c1a-8b3e-4d70-a5f1-3c2b1a0e9d84", "product": "service", "status": "active", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "interval": "month", "paid_through": "2026-11-30T09:15:00Z", "cancel_at": null, "started_at": "2026-01-30T09:15:00Z" } ], "next_cursor": null } ``` ## Assign subscription `PUT /units/{unit}/subscription` · scope `subscriptions:write` Give the unit Maci Service from an unassigned subscription. The unit has to be one of the organisation's own units, in use. Without `subscription_id` the oldest unassigned subscription is used. A unit that already has one answers with it. To move a subscription from another unit, take it off that unit first. 409 when the organisation has no unassigned subscription. ### Path parameters - `unit` (string (uuid), required): The ID of the unit. ### Body (JSON) - `subscription_id` (string (uuid)): An unassigned subscription of the organisation's. ### Request ```bash curl -X PUT https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/subscription \ -H "Authorization: Bearer $MACI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "subscription_id": "6f2d9c1a-8b3e-4d70-a5f1-3c2b1a0e9d84" }' ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/subscription', { method: 'PUT', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "subscription_id": "6f2d9c1a-8b3e-4d70-a5f1-3c2b1a0e9d84" }), }); const data = await response.json(); ``` ```python import os import requests response = requests.put( "https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/subscription", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, json={ "subscription_id": "6f2d9c1a-8b3e-4d70-a5f1-3c2b1a0e9d84" }, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('PUT', 'https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/subscription', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], 'json' => [ 'subscription_id' => '6f2d9c1a-8b3e-4d70-a5f1-3c2b1a0e9d84', ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 The subscription, now on the unit. The fields are those of the subscription model. ```json { "id": "6f2d9c1a-8b3e-4d70-a5f1-3c2b1a0e9d84", "product": "service", "status": "active", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "interval": "month", "paid_through": "2026-11-30T09:15:00Z", "cancel_at": null, "started_at": "2026-01-30T09:15:00Z" } ``` ## Unassign subscription `DELETE /units/{unit}/subscription` · scope `subscriptions:write` Take the unit's subscription off it. The subscription stays the organisation's, paid as before, and can be given to another unit. The unit's F-gas log can still be read, but nothing can be added to it. ### Path parameters - `unit` (string (uuid), required): The ID of the unit. ### Request ```bash curl -X DELETE https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/subscription \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/subscription', { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.delete( "https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/subscription", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('DELETE', 'https://api.maci.energy/v1/units/0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11/subscription', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 The subscription, now unassigned. The fields are those of the subscription model. ```json { "id": "6f2d9c1a-8b3e-4d70-a5f1-3c2b1a0e9d84", "product": "service", "status": "active", "unit_id": null, "interval": "month", "paid_through": "2026-11-30T09:15:00Z", "cancel_at": null, "started_at": "2026-01-30T09:15:00Z" } ``` --- # Invoicing A service company's invoice basis, made when a technician approves a service report from speech, and its catalogue. The fields are the same as in the CSV and Excel export, so an accounting system is set up once. When the company sets a webhook address in the app, each approved basis is posted to it as `invoice_draft.approved`, with the body signed in the header `X-Maci-Signature` as `sha256=` and the HMAC-SHA256 of the body with the webhook's secret. Webhook: `invoice_draft.approved` is posted to your own address as it happens. See [Webhooks](https://api.maci.energy/docs/webhooks.md). ## The invoice draft model - `id` (string (uuid)) - `status` (string) One of `draft`, `approved`. - `service_report_id` (string (uuid)) - `unit_id` (string (uuid)) - `unit_public_id` (string, nullable) - `unit` (string, nullable) - `customer` (object) - `visit_date` (string (date)) - `technician` (string, nullable) - `currency` (string) - `total_excl_vat` (number) - `approved_at` (string (date-time), nullable) - `exported_at` (string (date-time), nullable) - `lines` (array) ## The price list item model - `id` (string (uuid)) - `item_number` (string) - `description` (string) - `unit_of_measure` (string) - `price_excl_vat` (number) - `kind` (string) One of `part`, `refrigerant`, `labour`, `travel`, `other`. - `active` (boolean) - `updated_at` (string (date-time), nullable) ## List invoice bases `GET /invoice-drafts` · scope `invoices:read` The organisation's invoice basis. For an accounting system: `status=approved&exported=false` gives what is ready to invoice and not yet taken. ### Query parameters - `status` (string) One of `draft`, `approved`. - `exported` (boolean): `false` for only those not marked exported, `true` for only those marked. - `since` (string (date-time)): Only those changed at or after this time. - `limit` (integer): Records per page. At most 100, default 50. - `cursor` (string): The value of `next_cursor` from the page before. ### Request ```bash curl https://api.maci.energy/v1/invoice-drafts \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/invoice-drafts', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/invoice-drafts", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/invoice-drafts', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 A page of invoice bases with their lines. The fields are those of the invoice draft model. ```json { "data": [ { "id": "3c8d2a71-6e0b-4f95-a2d4-8b1e7c5f0a93", "status": "draft", "service_report_id": "9a4f0e2b-7c13-4d68-b5e1-2d9c6a3f8b07", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "unit_public_id": "MC-0482", "unit": "Cold room 2", "customer": { "name": "Cold room 2", "cvr": "Cvr", "vat_number": null }, "visit_date": "2026-10-30", "technician": "Jonas L.", "currency": "DKK", "total_excl_vat": 1890, "approved_at": "2026-10-30T09:15:00Z", "exported_at": null, "lines": [ { "type": "part", "item_number": "1000", "description": "Labour", "quantity": 1, "unit_of_measure": "hours", "unit_price": 700, "amount_excl_vat": 1050, "source": "speech", "match_sure": true } ] } ], "next_cursor": null } ``` ## Mark invoice bases exported `POST /invoice-drafts/mark-exported` · scope `invoices:write` Mark invoice bases exported. Only approved ones are marked, and only the first time; calling it again with the same IDs is no error. ### Body (JSON) - `ids` (array, required) ### Request ```bash curl -X POST https://api.maci.energy/v1/invoice-drafts/mark-exported \ -H "Authorization: Bearer $MACI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ids": [ "3c8d2a71-6e0b-4f95-a2d4-8b1e7c5f0a93" ] }' ``` ```javascript const response = await fetch('https://api.maci.energy/v1/invoice-drafts/mark-exported', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "ids": [ "3c8d2a71-6e0b-4f95-a2d4-8b1e7c5f0a93" ] }), }); const data = await response.json(); ``` ```python import os import requests response = requests.post( "https://api.maci.energy/v1/invoice-drafts/mark-exported", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, json={ "ids": [ "3c8d2a71-6e0b-4f95-a2d4-8b1e7c5f0a93" ] }, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('POST', 'https://api.maci.energy/v1/invoice-drafts/mark-exported', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], 'json' => [ 'ids' => [ '3c8d2a71-6e0b-4f95-a2d4-8b1e7c5f0a93', ], ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 How many were marked now, the bases as they are, and the IDs not found. The fields are those of the invoice draft model. ```json { "marked": 1, "data": [ { "id": "3c8d2a71-6e0b-4f95-a2d4-8b1e7c5f0a93", "status": "draft", "service_report_id": "9a4f0e2b-7c13-4d68-b5e1-2d9c6a3f8b07", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "unit_public_id": "MC-0482", "unit": "Cold room 2", "customer": { "name": "Cold room 2", "cvr": "Cvr", "vat_number": null }, "visit_date": "2026-10-30", "technician": "Jonas L.", "currency": "DKK", "total_excl_vat": 1890, "approved_at": "2026-10-30T09:15:00Z", "exported_at": null, "lines": [ { "type": "part", "item_number": "1000", "description": "Labour", "quantity": 1, "unit_of_measure": "hours", "unit_price": 700, "amount_excl_vat": 1050, "source": "speech", "match_sure": true } ] } ], "not_found": [ "9bcd14b2-18ed-41bf-a2d2-1978a263be9d" ] } ``` ## Get an invoice basis `GET /invoice-drafts/{draft}` · scope `invoices:read` One invoice basis with all its lines and sources. ### Path parameters - `draft` (string (uuid), required): The ID of the invoice basis. ### Request ```bash curl https://api.maci.energy/v1/invoice-drafts/3c8d2a71-6e0b-4f95-a2d4-8b1e7c5f0a93 \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/invoice-drafts/3c8d2a71-6e0b-4f95-a2d4-8b1e7c5f0a93', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/invoice-drafts/3c8d2a71-6e0b-4f95-a2d4-8b1e7c5f0a93", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/invoice-drafts/3c8d2a71-6e0b-4f95-a2d4-8b1e7c5f0a93', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 The invoice basis. The fields are those of the invoice draft model. ```json { "id": "3c8d2a71-6e0b-4f95-a2d4-8b1e7c5f0a93", "status": "draft", "service_report_id": "9a4f0e2b-7c13-4d68-b5e1-2d9c6a3f8b07", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "unit_public_id": "MC-0482", "unit": "Cold room 2", "customer": { "name": "Cold room 2", "cvr": "Cvr", "vat_number": null }, "visit_date": "2026-10-30", "technician": "Jonas L.", "currency": "DKK", "total_excl_vat": 1890, "approved_at": "2026-10-30T09:15:00Z", "exported_at": null, "lines": [ { "type": "part", "item_number": "1000", "description": "Labour", "quantity": 1, "unit_of_measure": "hours", "unit_price": 700, "amount_excl_vat": 1050, "source": "speech", "match_sure": true } ] } ``` ## List price list items `GET /price-list-items` · scope `price-list:read` The organisation's catalogue. ### Query parameters - `limit` (integer): Records per page. At most 100, default 50. - `cursor` (string): The value of `next_cursor` from the page before. ### Request ```bash curl https://api.maci.energy/v1/price-list-items \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/price-list-items', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/price-list-items", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/price-list-items', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 A page of items. The fields are those of the price list item model. ```json { "data": [ { "id": "f6ec47fe-e08d-464d-aba0-eaf6b286a073", "item_number": "1000", "description": "Labour", "unit_of_measure": "hours", "price_excl_vat": 700, "kind": "part", "active": true, "updated_at": "2026-10-30T09:15:00Z" } ], "next_cursor": null } ``` ## Save price list items `PUT /price-list-items` · scope `price-list:write` Create or update items from the accounting system. Each item is found by its item number, whatever its case or punctuation, and created when it is new. ### Body (JSON) - `items` (array, required) ### Request ```bash curl -X PUT https://api.maci.energy/v1/price-list-items \ -H "Authorization: Bearer $MACI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "item_number": "1000", "description": "Labour", "unit_of_measure": "hours", "price_excl_vat": 700, "kind": "part", "active": true } ] }' ``` ```javascript const response = await fetch('https://api.maci.energy/v1/price-list-items', { method: 'PUT', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "items": [ { "item_number": "1000", "description": "Labour", "unit_of_measure": "hours", "price_excl_vat": 700, "kind": "part", "active": true } ] }), }); const data = await response.json(); ``` ```python import os import requests response = requests.put( "https://api.maci.energy/v1/price-list-items", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, json={ "items": [ { "item_number": "1000", "description": "Labour", "unit_of_measure": "hours", "price_excl_vat": 700, "kind": "part", "active": True } ] }, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('PUT', 'https://api.maci.energy/v1/price-list-items', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], 'json' => [ 'items' => [ [ 'item_number' => '1000', 'description' => 'Labour', 'unit_of_measure' => 'hours', 'price_excl_vat' => 700, 'kind' => 'part', 'active' => true, ], ], ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 How many were created, updated and skipped. ```json { "created": 1, "updated": 1, "skipped": 1 } ``` --- # Alarms Alarms from Maci Intelligence for the units' owner and for the refrigeration firm the owner has chosen as the watch. An alarm is open until someone closes it, also after its cause has cleared. Taking an alarm (`acknowledge`) stops it from going on to the next recipients, as on the unit's card. When the organisation sets a webhook address for alarms in the app, each alarm is also posted to it as `alarm.raised`, `alarm.cleared`, `alarm.acknowledged` and `alarm.closed`, with the alarm as the body's `data` and the body signed in the header `X-Maci-Signature` as `sha256=` and the HMAC-SHA256 of the body with the webhook's secret. The SMS and mail to the recipients do not depend on the webhook. Webhook: `alarm.raised`, `alarm.cleared`, `alarm.acknowledged`, `alarm.closed` is posted to your own address as it happens. See [Webhooks](https://api.maci.energy/docs/webhooks.md). ## The alarm model - `id` (string (uuid)) - `unit_id` (string (uuid)) - `unit_public_id` (string, nullable) - `unit` (string, nullable): The unit's name. - `site` (string, nullable) - `type` (string): The alarm's type from the rule engine, e.g. `rum-for-varmt`. New types can come at any time. - `name` (string): The type's name, in English, or in Danish with `Accept-Language: da`. - `severity` (string): `HIGH` (SMS and mail, and on to the next recipients when nobody takes it) or `MEDIUM` (mail). - `state` (string): `cleared` when the rule engine sees the cause gone. One of `raised`, `cleared`. - `open` (boolean): Not yet closed by a person. - `values` (object): The readings the rule saw. - `raised_at` (string (date-time)) - `cleared_at` (string (date-time), nullable) - `level` (integer): How far it has gone: 0 the watch (or the owner without a watch), 1 the owner, 2 Maci. - `watch` (object, nullable) - `acknowledged_at` (string (date-time), nullable) - `acknowledged_by` (string, nullable): The person, or the key's name with (API). - `closed_at` (string (date-time), nullable) - `closed_by` (string, nullable) - `result` (string, nullable) One of `real`, `false`. - `note` (string, nullable) ## List alarms `GET /alarms` · scope `alarms:read` The alarms of the units the organisation owns or watches. Raised since `since`, by default the last 30 days. The owner's key sees the alarms of its own units; a refrigeration firm's key the alarms of the units where the owner has chosen it as the watch. ### Query parameters - `open` (boolean): `true` for only the alarms not yet closed, `false` for only the closed ones. - `unit_id` (string (uuid)) - `since` (string (date-time)) - `limit` (integer): Records per page. At most 100, default 50. - `cursor` (string): The value of `next_cursor` from the page before. ### Request ```bash curl https://api.maci.energy/v1/alarms \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/alarms', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/alarms", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/alarms', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 A page of alarms. The fields are those of the alarm model. ```json { "data": [ { "id": "7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "unit_public_id": "MC-0482", "unit": "Cold room 2", "site": "Main store", "type": "rum-for-varmt", "name": "Room too warm", "severity": "HIGH", "state": "raised", "open": true, "values": { "room_temp_c": "9.4", "setpoint_c": "3.0" }, "raised_at": "2026-10-30T09:15:00Z", "cleared_at": null, "level": 0, "watch": null, "acknowledged_at": null, "acknowledged_by": null, "closed_at": null, "closed_by": null, "result": null, "note": null } ], "next_cursor": null } ``` ## Get alarm `GET /alarms/{alarm}` · scope `alarms:read` One alarm. ### Path parameters - `alarm` (string (uuid), required): The ID of the alarm. ### Request ```bash curl https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18 \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 The alarm. The fields are those of the alarm model. ```json { "id": "7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "unit_public_id": "MC-0482", "unit": "Cold room 2", "site": "Main store", "type": "rum-for-varmt", "name": "Room too warm", "severity": "HIGH", "state": "raised", "open": true, "values": { "room_temp_c": "9.4", "setpoint_c": "3.0" }, "raised_at": "2026-10-30T09:15:00Z", "cleared_at": null, "level": 0, "watch": null, "acknowledged_at": null, "acknowledged_by": null, "closed_at": null, "closed_by": null, "result": null, "note": null } ``` ## Acknowledge alarm `POST /alarms/{alarm}/acknowledge` · scope `alarms:write` Take the alarm. Someone is on it, so the alarm does not go on to the next recipients. Taking an alarm already taken changes nothing. The card shows the key's name as who took it. ### Path parameters - `alarm` (string (uuid), required): The ID of the alarm. ### Request ```bash curl -X POST https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/acknowledge \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/acknowledge', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.post( "https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/acknowledge", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('POST', 'https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/acknowledge', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 The alarm, taken. The fields are those of the alarm model. ```json { "id": "7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "unit_public_id": "MC-0482", "unit": "Cold room 2", "site": "Main store", "type": "rum-for-varmt", "name": "Room too warm", "severity": "HIGH", "state": "raised", "open": true, "values": { "room_temp_c": "9.4", "setpoint_c": "3.0" }, "raised_at": "2026-10-30T09:15:00Z", "cleared_at": null, "level": 0, "watch": null, "acknowledged_at": "2026-10-30T09:17:42Z", "acknowledged_by": "Ops (API)", "closed_at": null, "closed_by": null, "result": null, "note": null } ``` ## Close alarm `POST /alarms/{alarm}/close` · scope `alarms:write` Close the alarm as real or false. Takes it first if nobody has. What was found helps tune the alarm rules. 409 when it is closed already. ### Path parameters - `alarm` (string (uuid), required): The ID of the alarm. ### Body (JSON) - `result` (string, required) One of `real`, `false`. - `note` (string): What was found on the unit. Write no names of people. ### Request ```bash curl -X POST https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/close \ -H "Authorization: Bearer $MACI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "result": "real", "note": "Door left open after a delivery." }' ``` ```javascript const response = await fetch('https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/close', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "result": "real", "note": "Door left open after a delivery." }), }); const data = await response.json(); ``` ```python import os import requests response = requests.post( "https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/close", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, json={ "result": "real", "note": "Door left open after a delivery." }, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('POST', 'https://api.maci.energy/v1/alarms/7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18/close', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], 'json' => [ 'result' => 'real', 'note' => 'Door left open after a delivery.', ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 The alarm, closed. The fields are those of the alarm model. ```json { "id": "7e3b1c90-2f4a-4d8e-9b61-5a0c3d7f2e18", "unit_id": "0b6f4c1e-5a1d-4a57-9f0e-2f6f3f0c9a11", "unit_public_id": "MC-0482", "unit": "Cold room 2", "site": "Main store", "type": "rum-for-varmt", "name": "Room too warm", "severity": "HIGH", "state": "raised", "open": false, "values": { "room_temp_c": "9.4", "setpoint_c": "3.0" }, "raised_at": "2026-10-30T09:15:00Z", "cleared_at": null, "level": 0, "watch": null, "acknowledged_at": "2026-10-30T09:17:42Z", "acknowledged_by": "Ops (API)", "closed_at": "2026-10-30T10:05:13Z", "closed_by": "Ops (API)", "result": "real", "note": "Door left open after a delivery." } ``` --- # Refrigerants and usage The refrigerant table with GWP, open to every key, and what the key and the organisation have used this month. ## The refrigerant model - `id` (string (uuid)) - `designation` (string): Such as `R449A`. - `group` (string) One of `hfc`, `hfc_blend`, `hfc_hfo_blend`, `hfo`, `natural`. - `gwp` (number) - `is_fgas` (boolean) ## The usage model - `environment` (string): Where the key works. One of `production`, `sandbox`. - `month` (string): The month as `YYYY-MM`. - `key` (object) - `organization` (object) - `rate_limit_per_minute` (integer) ## List refrigerants `GET /refrigerants` · any key The refrigerant table with GWP. Open to every key. ### Request ```bash curl https://api.maci.energy/v1/refrigerants \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/refrigerants', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/refrigerants", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/refrigerants', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 All refrigerants. The fields are those of the refrigerant model. ```json { "data": [ { "id": "2b7c4d1e-9f0a-4b3c-8d6e-5a4f3e2d1c0b", "designation": "R449A", "group": "hfc", "gwp": 1397, "is_fgas": false } ] } ``` ## Get usage `GET /usage` · any key What the key and the organisation have used this month. Open to every key. ### Request ```bash curl https://api.maci.energy/v1/usage \ -H "Authorization: Bearer $MACI_API_KEY" ``` ```javascript const response = await fetch('https://api.maci.energy/v1/usage', { headers: { Authorization: `Bearer ${process.env.MACI_API_KEY}`, }, }); const data = await response.json(); ``` ```python import os import requests response = requests.get( "https://api.maci.energy/v1/usage", headers={"Authorization": f"Bearer {os.environ['MACI_API_KEY']}"}, ) data = response.json() ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.maci.energy/v1/usage', [ 'headers' => [ 'Authorization' => 'Bearer '.getenv('MACI_API_KEY'), ], ]); $data = json_decode($response->getBody(), true); ``` ### Response 200 The usage in the current month (UTC). The fields are those of the usage model. ```json { "environment": "production", "month": "2026-10", "key": { "name": "Ops", "calls_this_month": 1240 }, "organization": { "calls_this_month": 8312, "monthly_limit": 100000 }, "rate_limit_per_minute": 60 } ```