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-za/api The language prefix is part of the URL (en-za), but it does not change the data returned — the records come from the bank exactly as they were registered in the field.
curl -G -d "municipality=Ponta Pora" --data-urlencode "country=Brasil" \
/en-za/api/lastcountingpublic If the response is a JSON list, the integration is already working.
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.
/api/lastcountingpublicPubliccurl -G \
--data-urlencode "municipality=Ponta Pora" \
--data-urlencode "date_start=2025-01-01" \
--data-urlencode "date_end=2025-12-31" \
/en-za/api/lastcountingpublic import requests
BASE = "/en-za/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"]) const BASE = "/en-za/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.
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.
import requests
BASE = "/en-za/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") 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).
import json
import os
import requests
BASE = "/en-za/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.
import csv
import requests
BASE = "/en-za/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.
/api/postcountingPrivateSend 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 -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-za/api/postcounting?key=SUA_CHAVE" import requests
BASE = "/en-za/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" /api/postcountingPrivateInstall 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).
import requests
BASE = "/en-za/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()) 400— lackovitrap_lat,ovitrap_lngouovitrap_group_id.400—ovitrap_type_iddifferent 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.
/api/postactionPrivateRegister 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.
import requests
BASE = "/en-za/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()) 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.
# leitura de uma ovitrampa (identificada pela ovitrampa + data)
curl -X POST -d "ovitrap_group_id=97" -d "date=2025-01-20" \
"/en-za/api/postdeletecounting?key=SUA_CHAVE"
# a ovitrampa inteira
curl -X POST -d "ovitrap_group_id=97" \
"/en-za/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-za/api/postdeleteaction?key=SUA_CHAVE"
# o quarteirao inteiro
curl -X POST -d "block_group_id=97" \
"/en-za/api/postdeleteblock?key=SUA_CHAVE" /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:
| Situation | Code | What to do |
|---|---|---|
"Wrong key" | 404 | Check that the key is in the query string and not in the body. |
| Duplicate count | 404 | There is already reading for the ovitrap this week; please remove before resending. |
| Duplicate visit | 409 | There is already a visit to the block this week. |
| Out of scope | 403 | The block does not belong to the municipality of Chave. Don't repeat. |
| Missing required field | 400 | Correct the submission; repeating returns the same error. |
| Invalid week or year | 500 | The date falls outside the accepted epidemiological week. Correct the field date. |
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
idlowering 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_idand theblock_idreceived — 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.