# Tutorial

Connect to DHIS2, read the metadata of a data set and send a week of values to it from a Python service.

This tutorial connects Zato to the public DHIS2 demo instance, reads the metadata of a weekly data set and sends one week of values to it, first as a dry run and then for real. The same steps apply to any DHIS2 instance, with its own address and token.

## How Zato connects to DHIS2 {#how-zato-connects-to-dhis2}

DHIS2 is reached through its Web API, a REST API under `/api`. Zato calls it through [REST outgoing connections](https://zato.io/docs/dev/rest/outconns.html), one per endpoint. The integration logic lives in services, which are Python classes deployed to Zato, and a service invokes a connection by its name.

DHIS2 keeps two kinds of data:

- **Aggregate data** - numbers per data element, organisation unit and period, such as the weekly count of malaria cases at a health facility. Data elements are grouped in data sets, and values are written to `/api/dataValueSets`.
- **Case data** - records of individual people or events, kept in tracker and event programs and written to `/api/tracker`.

Every metadata object, such as a data set, a data element or an organisation unit, has an 11-character UID and can also have a code. Requests refer to objects by either, and this tutorial uses both.

The demo instance runs DHIS2 2.43 at `https://play.im.dhis2.org/stable-2-43-1/` and is reset every night. The tutorial needs a personal access token of a user of that instance.

## Step 1 - The security definition {#step-1-the-security-definition}

DHIS2 authenticates API clients with personal access tokens, sent in the `Authorization` header with the `ApiToken` prefix. A [bearer token definition](https://zato.io/docs/security/bearer-tokens/index.html) in its static form sends it with every request.

1. In the Dashboard, open **Security** ▹ **Bearer tokens**.
2. Click **Create a Bearer token definition** and select the **Static tokens** tab.
3. Enter `DHIS2 Demo` as the **Name**.
4. Leave `Authorization` as the **Header**.
5. Enter `ApiToken` as the **Prefix**.
6. Paste the personal access token into **Token**.
7. Click **OK**.

![Token for the demo instance](https://zatosource-production.b-cdn.net/docs/gfx/dhis2/tutorial-bearer-token-create.webp?v=1791212082)

Dashboard menu: Security > Bearer tokens

## Step 2 - The connections {#step-2-the-connections}

Each DHIS2 endpoint the tutorial calls has a REST outgoing connection of its own. Create them under **Connections** ▹ **Outgoing** ▹ **REST**, each with `https://play.im.dhis2.org` as the **Host**, `DHIS2 Demo` as the **Security** and `JSON` as the **Data format**:

| Name | URL path |
| --- | --- |
| `DHIS2 System Info` | `/stable-2-43-1/api/system/info` |
| `DHIS2 Data Set` | `/stable-2-43-1/api/dataSets/{id}` |
| `DHIS2 Org Units` | `/stable-2-43-1/api/organisationUnits` |
| `DHIS2 Data Values` | `/stable-2-43-1/api/dataValueSets` |

`{id}` is a placeholder that each call fills in, as [Calling REST APIs](https://zato.io/docs/dev/rest/calling-apis.html) describes.

[Video: Connection to the demo instance](https://zatosource-production.b-cdn.net/docs/gfx/dhis2/tutorial-rest-system-info-create.webm?v=1791212153)

Dashboard menu: Connections > Outgoing > REST

Click **Ping** in the row of `DHIS2 System Info` to confirm that Zato reaches the instance.

## Step 3 - The configuration file {#step-3-the-configuration-file}

The services read the identifiers of the objects they work with from a [configuration file](https://zato.io/docs/dev/examples/config.html) rather than from their code. Create `config/user-conf/dhis2_tutorial.ini` in your project:

```ini
# config/user-conf/dhis2_tutorial.ini

[data_set]
id = Nyh6laLdBEJ

[org_unit]
code = OU_559

[element]
malaria = vq2qO3eTrNi
cholera = UsSUX0cpKsH
```

`Nyh6laLdBEJ` is the `IDSR Weekly` data set of the demo instance, `OU_559` is the code of the `Ngelehun CHC` health facility, and the two data elements are the weekly malaria and cholera counts of that data set.

## Step 4 - Read the metadata {#step-4-read-the-metadata}

The first service looks up the organisation unit by its code and reads the data set with the names of its data elements. Deploy it and invoke it from the Dashboard's IDE under **Services** ▹ **IDE**:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class ReadMetadata(Service):
    name = 'dhis2.tutorial.read-metadata'

    def read_org_unit(self) -> 'dict':

        # Find the organisation unit by its code ..
        code = self.config.dhis2_tutorial.org_unit.code
        params = {'filter': f'code:eq:{code}', 'fields': 'id,name'}

        conn = self.rest['DHIS2 Org Units']
        response = conn.get(params=params)

        # .. which is unique, so there is exactly one match.
        org_units = response.data['organisationUnits']
        org_unit = org_units[0]

        return org_unit
```

`params` fills in placeholders in the URL path and sends everything else as query parameters. `fields` selects the attributes DHIS2 returns, and a nested selection such as `dataElement[id,name]` reaches into related objects:

```python
    def read_data_set(self) -> 'dict':

        data_set_id = self.config.dhis2_tutorial.data_set.id
        fields = 'name,periodType,dataSetElements[dataElement[id,name]]'
        params = {'id': data_set_id, 'fields': fields}

        conn = self.rest['DHIS2 Data Set']
        response = conn.get(params=params)
        data_set = response.data

        return data_set

    def handle(self) -> 'None':

        org_unit = self.read_org_unit()
        data_set = self.read_data_set()

        # Each data set element wraps one data element ..
        data_elements = []
        for item in data_set['dataSetElements']:
            data_element = item['dataElement']
            data_elements.append(data_element)

        # .. which is what the response lists.
        self.response.payload = {
            'org_unit': org_unit,
            'data_set': data_set['name'],
            'period_type': data_set['periodType'],
            'data_elements': data_elements,
        }
```

The response shows what the data set expects - weekly values for five data elements, each with the UID it is written under:

```json
{
  "org_unit": {"id": "DiszpKrYNg8", "name": "Ngelehun CHC"},
  "data_set": "IDSR Weekly",
  "period_type": "Weekly",
  "data_elements": [
    {"id": "HS9zqaBdOQ4", "name": "IDSR Plague"},
    {"id": "vq2qO3eTrNi", "name": "IDSR Malaria"},
    {"id": "YazgqXbizv1", "name": "IDSR Measles"},
    {"id": "UsSUX0cpKsH", "name": "IDSR Cholera"},
    {"id": "noIzB569hTM", "name": "IDSR Yellow fever"}
  ]
}
```

## Step 5 - Check the values with a dry run {#step-5-check-the-values-with-a-dry-run}

The second service sends a week of malaria and cholera counts. A data value set names the data set, the period and the organisation unit once, and lists the values under them:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class SendWeeklyReport(Service):
    name = 'dhis2.tutorial.send-weekly-report'
    input = 'period', 'malaria', 'cholera'

    def build_payload(self) -> 'dict':

        request = self.request.input
        tutorial = self.config.dhis2_tutorial
        element = tutorial.element

        # One value per data element ..
        data_values = [
            {'dataElement': element.malaria, 'value': request.malaria},
            {'dataElement': element.cholera, 'value': request.cholera},
        ]

        # .. for one organisation unit and one week.
        payload = {
            'dataSet': tutorial.data_set.id,
            'period': request.period,
            'orgUnit': tutorial.org_unit.code,
            'dataValues': data_values,
        }

        return payload
```

The organisation unit is given by its code and the data elements by their UIDs, so `orgUnitIdScheme=code` tells DHIS2 how to read the first. `dryRun=true` makes DHIS2 validate the values and return the import summary without storing anything:

```python
    def handle(self) -> 'None':

        payload = self.build_payload()
        params = {'orgUnitIdScheme': 'code', 'dryRun': 'true'}

        # Send the values ..
        conn = self.rest['DHIS2 Data Values']
        response = conn.post(payload, params)

        # .. log each value DHIS2 would not accept ..
        import_summary = response.data['response']
        for conflict in import_summary['conflicts']:
            self.logger.warning('DHIS2 conflict: %s', conflict['value'])

        # .. and return the counts of what it would do with the rest.
        self.response.payload = {
            'status': import_summary['status'],
            'import_count': import_summary['importCount'],
        }
```

Invoke it with a valid malaria count and a cholera count that is not a whole number. `2026W40` is the DHIS2 identifier of ISO week 40 of 2026. DHIS2 takes every value as a string, numbers included, and rejects the whole request when a value is a JSON number:

```json
{"period": "2026W40", "malaria": "12", "cholera": "1.5"}
```

The data element only accepts zero or positive integers, so DHIS2 ignores that value and reports it as a conflict:

```json
{
  "status": "WARNING",
  "import_count": {"imported": 1, "updated": 0, "ignored": 1, "deleted": 0}
}
```

The server log carries the reason, which reads ``Value #1 value `1.5` is no valid INTEGER_ZERO_OR_POSITIVE``. `#1` is the position of the value in `dataValues`, counted from zero. A data element that does not belong to the data set makes DHIS2 reject the whole set instead, with the status `ERROR` and HTTP 409.

## Step 6 - Send the values {#step-6-send-the-values}

Remove `'dryRun': 'true'` from `params`, deploy the service again and invoke it with valid counts:

```json
{"period": "2026W40", "malaria": "12", "cholera": "0"}
```

The status is `SUCCESS` and both values are counted under `imported`. Sending the same period again replaces the stored values and counts them under `updated`, so a corrected report is the same call with new numbers.

The values are visible in the DHIS2 Data Entry app, under `Ngelehun CHC`, `IDSR Weekly` and week 40 of 2026.

## Aggregate vs case data {#aggregate-vs-case-data}

Every flow that writes to DHIS2 starts with the choice this tutorial made without stating it. A value that answers "how many" for a place and a period is aggregate data and goes to a data set through `/api/dataValueSets`, as above. A record that describes one patient, one sample or one event - with a date, a place and attributes of its own - is case data and goes to a tracker or event program through `/api/tracker`.

The two are often the same information at different levels. The [lab results](https://zato.io/docs/dev/healthcare/dhis2/lab-results/index.html) integration writes each result to its case and also sends the weekly count of results to a data set. DHIS2 can compute aggregate figures from case data itself through program indicators, so a service sends counts to a data set when the data set is what the reporting chain reads, and when the cases themselves are not in DHIS2.

## See also {#see-also}

- [Security](https://zato.io/docs/dev/healthcare/dhis2/security.html) - Tokens, passwords, event hooks, the Route API and case data in the audit log
- [Lab results](https://zato.io/docs/dev/healthcare/dhis2/lab-results/index.html) - Laboratory results written to tracker cases from HL7 v2 and FHIR
- [Calling REST APIs](https://zato.io/docs/dev/rest/calling-apis.html) - Query parameters, payloads and response objects

## Learn more {#learn-more}

- [Healthcare interface engine](https://zato.io/docs/dev/healthcare/) - Clinical messages, FHIR, conversion and operations
- [HL7 v2 parsing](https://zato.io/docs/dev/healthcare/hl7/v2/parsing/) - Messages as typed objects, with field access and validation
- [MLLP channels](https://zato.io/docs/dev/healthcare/hl7v2/mllp/) - Receiving and sending HL7 v2 over MLLP sockets
- [FHIR integrations](https://zato.io/docs/dev/healthcare/hl7/fhir/) - Read and write FHIR resources, with paths, bundles and extensions
- [HL7 v2 to FHIR](https://zato.io/docs/dev/healthcare/hl7/to-fhir/) - Converting v2 messages into FHIR bundles, codes and references included
- [EDIFACT](https://zato.io/docs/dev/healthcare/edifact/) - Parsing interchanges, dialects and the transports they arrive on
- [Transformation](https://zato.io/docs/dev/healthcare/transformation/) - One message in, another standard out, in ordinary Python
- [Audit log](https://zato.io/docs/dev/healthcare/audit-log.html) - Every message with its acknowledgment, searchable by patient identifier
- [AI in clinical interfaces](https://zato.io/docs/dev/healthcare/ai/) - Services calling LLMs and AI agents calling clinical services as tools
