API usage examples

Ready-made recipes in curl, Python and JavaScript to query public data, synchronize your database and send ovitrap readings and block visits.

/en-bz/apiBack to reference

First steps

All calls come from the same database and return JSON. Public endpoints do not require authentication; private ones require your access key.

Base address

/en-bz/api

The language prefix is ​​part of the URL (en-bz), but it does not change the data returned — the records come from the bank exactly as they were registered in the field.

Quick test
curl -G -d "municipality=Ponta Pora" --data-urlencode "country=Brasil" \
  /en-bz/api/lastcountingpublic

If the response is a JSON list, the integration is already working.

The key goes in the URL. On all private endpoints — including POST — the key is read from the query string (?key=SUA_CHAVE). Sending it only in the body of the form returns 404 "Wrong key". The other fields continue to appear in the body of the request.

Consult public data

The endpoint /api/lastcountingpublic returns the latest egg counts with epidemiological latitude, longitude, week and year. It is the starting point for maps, panels and studies.

GET/api/lastcountingpublicPublic
curl
curl -G \
  --data-urlencode "municipality=Ponta Pora" \
  --data-urlencode "date_start=2025-01-01" \
  --data-urlencode "date_end=2025-12-31" \
  /en-bz/api/lastcountingpublic
Python
import requests

BASE = "/en-bz/api"

resposta = requests.get(
    BASE + "/lastcountingpublic",
    params={
        "municipality": "Ponta Pora",
        "date_start": "2025-01-01",
        "date_end": "2025-12-31",
    },
    timeout=60,
)
resposta.raise_for_status()

for contagem in resposta.json():
    print(contagem["date"], contagem["ovitrap_id"], contagem["eggs"])
JavaScript
const BASE = "/en-bz/api";

const params = new URLSearchParams({
    state: "MS",
    date_start: "2025-01-01",
});

const resposta = await fetch(BASE + "/lastcountingpublic?" + params);
const contagens = await resposta.json();

console.log(contagens.length, "contagens");

The same location parameters (country, state, municipality) They apply to other public endpoints: ovitraps, blocks, EDLs, EDL maintenance and strategic points. Just changing the endpoint path already changes the data set.

Without any location parameters, the endpoint returns data for the entire Brazil. Filtering by municipality or state makes the response much faster.

Browse all pages

Paginated endpoints support page from 1 to 100. Above that, the API responds with the text "Maximum pagination is 100" and status 200 — that is, the body is no longer a list. Always check the type before iterating.

Python
import requests

BASE = "/en-bz/api"


def baixar_tudo(endpoint, **filtros):
    """Percorre as paginas ate a API devolver uma pagina vazia."""
    registros = []

    for pagina in range(1, 101):
        resposta = requests.get(
            BASE + endpoint,
            params=dict(filtros, page=pagina),
            timeout=60,
        )
        resposta.raise_for_status()
        dados = resposta.json()

        # limite de paginacao atingido: a API devolve uma string, nao uma lista
        if not isinstance(dados, list):
            print("Aviso:", dados)
            break

        if not dados:
            break

        registros.extend(dados)

    return registros


ovitrampas = baixar_tudo("/getmunicipalityovitrapspublic", municipality="Ponta Pora")
print(len(ovitrampas), "ovitrampas")
Did you get to page 100? Narrow the filter instead of trying page 101: look county by county, or use date_start e date_end to divide the period into smaller intervals.

Incremental sync

To maintain a mirrored base, don't download everything again with each run. The endpoint /api/lastcountingpublic accepted id, which returns only the counts from that identifier, and also date (inclusion date) e date_collect (data de coleta).

Python
import json
import os
import requests

BASE = "/en-bz/api"
ESTADO = "ultimo_id.json"


def ultimo_id_lido():
    if os.path.exists(ESTADO):
        with open(ESTADO) as arquivo:
            return json.load(arquivo)["counting_id"]
    return 0


def sincronizar():
    ultimo = ultimo_id_lido()
    novas = []

    for pagina in range(1, 101):
        resposta = requests.get(
            BASE + "/lastcountingpublic",
            params={"municipality": "Ponta Pora", "id": ultimo, "page": pagina},
            timeout=60,
        )
        resposta.raise_for_status()
        dados = resposta.json()

        if not isinstance(dados, list) or not dados:
            break

        novas.extend(dados)

    if novas:
        maior = max(item["counting_id"] for item in novas)
        with open(ESTADO, "w") as arquivo:
            json.dump({"counting_id": maior}, arquivo)

    return novas


print(len(sincronizar()), "contagens novas")

Always keep the largest counting_id received, not the date of execution: a count entered in the field today may refer to a previous week, and the filter by id does not let these records escape.

Export to CSV

Public counts already come with coordinates, so you can generate a spreadsheet or feed a map without any additional joining.

Python
import csv
import requests

BASE = "/en-bz/api"

COLUNAS = [
    "counting_id",
    "municipality",
    "state_code",
    "ovitrap_id",
    "latitude",
    "longitude",
    "week",
    "year",
    "eggs",
    "date",
    "date_collect",
]

resposta = requests.get(
    BASE + "/lastcountingpublic",
    params={"state": "MS", "date_start": "2025-01-01"},
    timeout=60,
)
contagens = resposta.json()

with open("contagens.csv", "w", newline="", encoding="utf-8") as arquivo:
    escritor = csv.DictWriter(arquivo, fieldnames=COLUNAS, extrasaction="ignore")
    escritor.writeheader()
    escritor.writerows(contagens)

print("Arquivo contagens.csv gerado com", len(contagens), "linhas")

Sending data

The following examples use the private API and require a key. The geographical scope of the key defines what it can record — a municipal key only records in the municipality itself.

POST/api/postcountingPrivate

Send the reading of an existing ovitrap

Sends the egg count of an already registered ovitrap. The ovitrap is located by the ovitrap_group_id within the municipality of the key, and the epidemiological week is calculated from the field date.

curl
curl -X POST \
  -d "ovitrap_group_id=97" \
  -d "ovitrap_lat=-7.000000" \
  -d "ovitrap_lng=-8.000000" \
  -d "date=2025-01-20" \
  -d "counting_observation_id=1" \
  -d "counting_eggs=5" \
  "/en-bz/api/postcounting?key=SUA_CHAVE"
Python
import requests

BASE = "/en-bz/api"
CHAVE = "SUA_CHAVE"

resposta = requests.post(
    BASE + "/postcounting",
    params={"key": CHAVE},          # a chave vai na URL
    data={                          # os dados vao no corpo
        "ovitrap_group_id": 97,
        "ovitrap_lat": -7.000000,
        "ovitrap_lng": -8.000000,
        "date": "2025-01-20",
        "counting_observation_id": 1,
        "counting_eggs": 5,
    },
    timeout=60,
)

print(resposta.status_code, resposta.json())
# 200 "Contagem registrada"
Attention: If the coordinates sent are different from those registered, the ovitrap is updated with the new position. Always send the real coordinate of the point, not that of the agent at the time of reading.
POST/api/postcountingPrivate

Install a new ovitrap next to the reading

If the ovitrap_group_id does not yet exist in the municipality, the ovitrap is created in the same request. Also send the address and, if applicable, the type (ovitrap_type_id: 1 urban, pattern; 2 rural).

Python
import requests

BASE = "/en-bz/api"
CHAVE = "SUA_CHAVE"

resposta = requests.post(
    BASE + "/postcounting",
    params={"key": CHAVE},
    data={
        "ovitrap_group_id": 96,
        "ovitrap_address_district": "Centro",
        "ovitrap_address_street": "Rua das Flores",
        "ovitrap_address_number": "123",
        "ovitrap_address_complement": "",
        "ovitrap_address_sector": "Setor 04",
        "ovitrap_responsable": "Maria Souza",
        "ovitrap_block_id": "12",
        "ovitrap_type_id": 2,          # 1 = urbana (padrao), 2 = rural
        "ovitrap_lat": -7.000000,
        "ovitrap_lng": -8.000000,
        "date": "2025-01-20",
        "counting_date_collect": "2025-01-27",
        "counting_observation_id": 1,
        "counting_eggs": 5,
    },
    timeout=60,
)

print(resposta.status_code, resposta.json())
Specific answers:
  • 400 — lack ovitrap_lat, ovitrap_lng ou ovitrap_group_id.
  • 400ovitrap_type_id different from 1 or 2.
  • 404 — There is already a count for this ovitrap, year and week.
  • 500 — the date sent falls in a future or invalid epidemiological week.
POST/api/postactionPrivate

Register a block visit

Use block_id to record the visit in an already registered block. If the block does not yet exist, send block_group_id with the block data and it is automatically created within the key municipality.

Python — existing block
import requests

BASE = "/en-bz/api"
CHAVE = "SUA_CHAVE"

visita = {
    "block_id": 97,
    "date": "2025-01-20",

    "block_land_residence": 10,
    "action_land_residence_out": 2,
    "action_land_residence_breedings": 1,
    "action_land_residence_treated": 1,

    "action_deposit_a1_quantity": 4,
    "action_deposit_a1_eliminated": 2,
    "action_deposit_a1_treated": 1,
    "action_deposit_a1_larvicid": 10,

    "action_observation": "Foco encontrado em pneus nos fundos do imovel.",
}

resposta = requests.post(
    BASE + "/postaction", params={"key": CHAVE}, data=visita, timeout=60
)

if resposta.status_code == 409:
    print("Ja existe visita para esse quarteirao nessa semana")
else:
    print(resposta.status_code, resposta.json())
Python — creating the block
visita = {
    "block_group_id": 97,
    "date": "2025-01-20",
    "block_address_district": "Centro Historico",
    "block_address_sector": "Setor 04 - Norte",
    "block_coordinates": "[[-7.123, -34.845], [-7.124, -34.846]]",
    "block_lat": -7.123456,
    "block_lng": -34.845678,
    "block_responsable": "Joao da Silva",

    "block_land_residence": 10,
    "action_land_residence_out": 2,
    "action_land_residence_breedings": 1,
    "action_land_residence_treated": 1,

    "action_observation": "Area com alta densidade de recipientes descartaveis.",
}

resposta = requests.post(
    BASE + "/postaction", params={"key": CHAVE}, data=visita, timeout=60
)
print(resposta.status_code, resposta.json())

The complete list of real estate and deposit fields (a1, a2, b, c, d1, d2, e) is in endpoint reference. Unsent numeric fields assume zero, but any value that is not a number returns 400.

Remove records

Removals are definitive and always restricted to the key municipality.

curl
# leitura de uma ovitrampa (identificada pela ovitrampa + data)
curl -X POST -d "ovitrap_group_id=97" -d "date=2025-01-20" \
  "/en-bz/api/postdeletecounting?key=SUA_CHAVE"

# a ovitrampa inteira
curl -X POST -d "ovitrap_group_id=97" \
  "/en-bz/api/postdeleteovitrap?key=SUA_CHAVE"

# visita de um quarteirao (quarteirao + data)
curl -X POST -d "block_group_id=97" -d "date=2025-01-20" \
  "/en-bz/api/postdeleteaction?key=SUA_CHAVE"

# o quarteirao inteiro
curl -X POST -d "block_group_id=97" \
  "/en-bz/api/postdeleteblock?key=SUA_CHAVE"
To correct a count posted with the wrong value, remove the reading with /api/postdeletecounting and send it again with /api/postcounting — resending over it returns a duplication error.

Error handling

The response body is always a JSON string with the message. It is worth dealing with each case, because not every error deserves another attempt:

SituationCodeWhat to do
"Wrong key"404Check that the key is in the query string and not in the body.
Duplicate count404There is already reading for the ovitrap this week; please remove before resending.
Duplicate visit409There is already a visit to the block this week.
Out of scope403The block does not belong to the municipality of Chave. Don't repeat.
Missing required field400Correct the submission; repeating returns the same error.
Invalid week or year500The date falls outside the accepted epidemiological week. Correct the field date.
Python
import time
import requests

NAO_REPETIR = {400, 403, 404, 409}


def enviar(url, chave, dados, tentativas=3):
    for tentativa in range(tentativas):
        try:
            resposta = requests.post(
                url, params={"key": chave}, data=dados, timeout=60
            )
        except requests.RequestException as erro:
            print("Falha de rede:", erro)
            time.sleep(5 * (tentativa + 1))
            continue

        if resposta.status_code in NAO_REPETIR:
            print("Erro definitivo:", resposta.status_code, resposta.json())
            return None

        if resposta.ok:
            return resposta.json()

        # 500 e demais erros de servidor: espera progressiva e tenta de novo
        time.sleep(5 * (tentativa + 1))

    return None

Good practices

  • Filter by municipality or state whenever possible — without filtering, the query scans the entire country.
  • Prefer incremental synchronization by id lowering the entire base every day.
  • Send requests in series. The API is not designed for dozens of simultaneous calls per client.
  • Check that the response is a list before iterating: paging limit messages come with status 200.
  • Save the counting_id and the block_id received — they are what allow you to match Conta Ovos records with those in your system.
  • Treat the epidemiological week as the determining field: Conta Ovos only accepts one reading per ovitrap per week, and one visit per block per week.
  • Follow the page mudanças before updating your integration.