# 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
}
```
