API do Chegou

Envie entregas de qualquer programa (ERP, loja online, folha de cálculo) e saiba quando são entregues.

Como começar

1. No backoffice, em Definições → API para outros programas, crie uma chave com o nome do sistema (ex.: «ERP»). A chave aparece uma só vez: guarde-a no seu programa. Pode revogá-la quando quiser.

2. Envie-a em todos os pedidos: Authorization: Bearer chg_… (ou o cabeçalho X-API-Key).

3. O endereço base é o da sua instalação, por exemplo https://empresa.chegou.pt. Tudo em JSON, UTF-8.

Use a chave só no servidor do seu programa, nunca num navegador ou numa app instalada nos telemóveis. Há limite de pedidos por minuto e por chave; uma chave de só leitura consulta mas não altera nada.

Criar entregas

POST/api/v1/orders

Envie uma entrega (objeto) ou até 200 (lista). A ref é única: se reenviar a mesma, o Chegou não duplica. Se a entrega ainda não saiu para a rota, atualiza o que mudou; se já está em rota ou concluída, recusa a alteração.

curl -X POST https://empresa.chegou.pt/api/v1/orders \
  -H "Authorization: Bearer chg_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "ref": "FT 2026/1234",
    "customer": "Café Central",
    "address": "Rua do Souto 24, 4700-329 Braga",
    "phone": "253000000",
    "win_from": "09:00", "win_to": "12:00",
    "lines": [
      { "item_ref": "A100", "description": "Água 0,5 L (pack 24)", "unit": "CX", "qty": 10 }
    ]
  }'
CampoDescrição
refobrigatórioReferência do documento no seu sistema (única). Até 60 caracteres.
customerobrigatórioNome do cliente.
addressobrigatórioMorada de entrega, com código postal. O Chegou encontra as coordenadas.
lat, lngopcionalCoordenadas, se já as tem (sempre as duas). Evitam procurar a morada.
phone, emailopcionalContactos do cliente (o email recebe o aviso de seguimento, se estiver ativo).
notesopcionalInstruções para o motorista (porta, andar, horários).
win_from, win_toopcionalJanela de entrega, HH:MM.
service_minopcionalMinutos de paragem (1–240).
doc_dateopcionalData do documento, AAAA-MM-DD.
customer_code, nifopcionalCódigo e NIF do cliente, para o ligar à ficha de cliente.
linesopcionalLista de { item_ref, description, unit, qty, price } (até 200 linhas).

Resposta (uma entrega): 201 se criou, 200 se atualizou ou estava igual, 409 se já saiu para a rota e não se pode alterar, 400 se o pedido é inválido.

{ "ref": "FT 2026/1234", "resultado": "criada" }

Resultados possíveis: criada, atualizada, igual, recusada (já saiu para a rota) e erro (com o motivo em erro). Numa lista, a resposta traz resultados (um por entrega, pela mesma ordem) e as contagens; uma entrega com erro não impede as outras.

Consultar o estado

GET/api/v1/orders/{ref}

{
  "ref": "FT 2026/1234",
  "estado": "entregue",            // pendente | atribuida | em_rota | entregue | falhada
  "cliente": "Café Central",
  "morada": "Rua do Souto 24, 4700-329 Braga",
  "janela": { "de": "09:00", "ate": "12:00" },
  "motorista": "Ana",
  "previsao_chegada": null,
  "entregue_em": "2026-10-20T10:42:11.000Z",
  "recebido_por": "Maria Silva",
  "motivo_falha": null,
  "seguimento": "https://empresa.chegou.pt/t/…",
  "comprovativo_pdf": "https://empresa.chegou.pt/api/comprovativo/cafe-central/….pdf",
  "linhas": [ { "artigo": "A100", "descricao": "Água 0,5 L (pack 24)", "unidade": "CX", "quantidade": 10, "entregue": 10, "devolvida": null } ]
}

As ligações seguimento e comprovativo_pdf só aparecem se o endereço público estiver definido em Definições → Notificações; o comprovativo só depois de a entrega estar concluída. A API nunca devolve preços.

GET/api/v1/orders?estado=entregue&desde=2026-10-01&ate=2026-10-31&limite=100

Lista as entregas mais recentes (até 500), sem as linhas. Todos os filtros são opcionais.

Cancelar

DELETE/api/v1/orders/{ref}

Só se a entrega ainda não saiu (pendente ou numa rota por iniciar). Em rota, concluída ou falhada devolve 409.

Avisos (webhooks)

Em vez de consultar o estado de vez em quando, deixe o Chegou avisar o seu programa. Em Definições → Integrações → Avisos ao seu programa indique um endereço https público, os eventos que quer e guarde o segredo (aparece uma só vez).

EventoQuando
entrega.em_rotaO motorista vai a caminho da entrega.
entrega.entregueEntrega concluída (com quem recebeu).
entrega.falhadaEntrega não realizada (com o motivo).
entrega.reabertaUma entrega ou falha foi anulada e volta a estar aberta.

O Chegou faz POST com Content-Type: application/json:

{
  "id": "evt_1234",                // identificador do envio (igual nas repetições)
  "evento": "entrega.entregue",
  "criado_em": "2026-10-20T10:42:12.000Z",
  "entrega": { "ref": "FT 2026/1234", "estado": "entregue", ... }   // o mesmo formato de GET /api/v1/orders/{ref}
}

Cabeçalhos: X-Chegou-Event, X-Chegou-Delivery (o id) e X-Chegou-Signature: t=<segundos>,v1=<assinatura>, onde a assinatura é o HMAC-SHA256, em hexadecimal, de <t>.<corpo> com o segredo do aviso. Confirme sempre a assinatura e rejeite pedidos com t muito antigo (por exemplo, mais de 5 minutos).

// Node.js
const crypto = require('node:crypto');
function valido(segredo, cabecalho, corpoBruto) {
  const [t, v1] = cabecalho.split(',').map((p) => p.split('=')[1]);
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const esperado = crypto.createHmac('sha256', segredo).update(t + '.' + corpoBruto).digest('hex');
  return v1.length === esperado.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(esperado));
}

Responda com um código 2xx em poucos segundos. Se não responder (ou responder erro), o Chegou repete ao fim de 1 min, 5 min, 30 min, 2 h e 6 h e depois desiste desse envio; o mesmo id pode chegar mais de uma vez, por isso trate-o como idempotente. Ao fim de 30 falhas seguidas o aviso desativa-se sozinho. Por omissão só se avisa das entregas criadas pela API. Os envios não seguem redirecionamentos.

Exemplos prontos

Código completo, só com a biblioteca padrão de cada linguagem: enviar uma entrega, consultar o estado e confirmar a assinatura de um aviso. Ficam também no repositório, em exemplos-api/.

Python 3

# Exemplo de utilização da API do Chegou em Python 3 (só biblioteca padrão).
# Defina CHEGOU_URL (ex.: https://empresa.chegou.pt) e CHEGOU_CHAVE (chg_…) no ambiente.
import hashlib
import hmac
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request

BASE = os.environ.get("CHEGOU_URL", "https://empresa.chegou.pt")
CHAVE = os.environ.get("CHEGOU_CHAVE", "")


def chamar(metodo, caminho, corpo=None):
    """Faz um pedido à API e devolve (código HTTP, dados em JSON)."""
    dados = json.dumps(corpo).encode("utf-8") if corpo is not None else None
    pedido = urllib.request.Request(BASE + caminho, data=dados, method=metodo, headers={
        "Authorization": "Bearer " + CHAVE,
        "Content-Type": "application/json",
    })
    try:
        with urllib.request.urlopen(pedido, timeout=20) as resposta:
            return resposta.status, json.loads(resposta.read() or b"{}")
    except urllib.error.HTTPError as erro:  # 400, 401, 409… trazem {"error": "…"}
        return erro.code, json.loads(erro.read() or b"{}")


def enviar_entrega(entrega):
    """Cria (ou atualiza) uma entrega. Repetir o pedido com a mesma ref nunca duplica."""
    return chamar("POST", "/api/v1/orders", entrega)


def estado_da_entrega(ref):
    return chamar("GET", "/api/v1/orders/" + urllib.parse.quote(ref, safe=""))


def assinatura_valida(segredo, cabecalho, corpo_bruto, tolerancia=300):
    """Confirma um aviso (webhook). `corpo_bruto` são os bytes recebidos, sem os reescrever."""
    partes = dict(p.split("=", 1) for p in cabecalho.split(","))
    if abs(time.time() - int(partes["t"])) > tolerancia:
        return False
    esperado = hmac.new(segredo.encode(), partes["t"].encode() + b"." + corpo_bruto, hashlib.sha256).hexdigest()
    return hmac.compare_digest(partes["v1"], esperado)


if __name__ == "__main__":
    codigo, resposta = enviar_entrega({
        "ref": "FT 2026/1234",
        "customer": "Café Central",
        "address": "Rua do Souto 24, 4700-329 Braga",
        "phone": "253000000",
        "win_from": "09:00", "win_to": "12:00",
        "lines": [{"item_ref": "A100", "description": "Água 0,5 L (pack 24)", "unit": "CX", "qty": 10}],
    })
    print(codigo, resposta)  # 201 {'ref': 'FT 2026/1234', 'resultado': 'criada'}
    print(estado_da_entrega("FT 2026/1234"))

PHP 8

<?php
// Exemplo de utilização da API do Chegou em PHP 8 (sem bibliotecas).
// Defina CHEGOU_URL (ex.: https://empresa.chegou.pt) e CHEGOU_CHAVE (chg_…) no ambiente.
$BASE  = getenv('CHEGOU_URL') ?: 'https://empresa.chegou.pt';
$CHAVE = getenv('CHEGOU_CHAVE') ?: '';
if (!preg_match('/^[\x21-\x7E]*$/', $CHAVE)) { fwrite(STDERR, "CHEGOU_CHAVE inválida.\n"); exit(1); } // sem espaços nem quebras de linha: a chave vai num cabeçalho HTTP

/** Faz um pedido à API e devolve [código HTTP, dados em JSON]. */
function chamar(string $metodo, string $caminho, ?array $corpo = null): array {
    global $BASE, $CHAVE;
    $contexto = stream_context_create(['http' => [
        'method' => $metodo,
        'header' => "Authorization: Bearer $CHAVE\r\nContent-Type: application/json\r\n",
        'content' => $corpo === null ? '' : json_encode($corpo, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
        'ignore_errors' => true, // 400, 401, 409… trazem {"error": "…"}
        'timeout' => 20,
    ]]);
    $texto = file_get_contents($BASE . $caminho, false, $contexto);
    preg_match('#HTTP/\S+ (\d+)#', $http_response_header[0] ?? '', $m);
    return [(int)($m[1] ?? 0), json_decode($texto ?: '{}', true)];
}

/** Cria (ou atualiza) uma entrega. Repetir o pedido com a mesma ref nunca duplica. */
function enviarEntrega(array $entrega): array { return chamar('POST', '/api/v1/orders', $entrega); }

function estadoDaEntrega(string $ref): array { return chamar('GET', '/api/v1/orders/' . rawurlencode($ref)); }

/** Confirma um aviso (webhook). $corpoBruto é o texto recebido, sem o reescrever. */
function assinaturaValida(string $segredo, string $cabecalho, string $corpoBruto, int $tolerancia = 300): bool {
    parse_str(str_replace(',', '&', $cabecalho), $p);
    if (!isset($p['t'], $p['v1']) || !is_string($p['t']) || !is_string($p['v1']) || !ctype_digit($p['t']) || abs(time() - (int)$p['t']) > $tolerancia) return false; // só texto e dígitos: um cabeçalho com v1[]= não rebenta nem passa
    $esperado = hash_hmac('sha256', $p['t'] . '.' . $corpoBruto, $segredo);
    return hash_equals($esperado, $p['v1']);
}

if (PHP_SAPI === 'cli' && realpath($argv[0] ?? '') === __FILE__) {
    [$codigo, $resposta] = enviarEntrega([
        'ref' => 'FT 2026/1234',
        'customer' => 'Café Central',
        'address' => 'Rua do Souto 24, 4700-329 Braga',
        'phone' => '253000000',
        'win_from' => '09:00', 'win_to' => '12:00',
        'lines' => [['item_ref' => 'A100', 'description' => 'Água 0,5 L (pack 24)', 'unit' => 'CX', 'qty' => 10]],
    ]);
    echo $codigo, ' ', json_encode($resposta, JSON_UNESCAPED_UNICODE), "\n"; // 201 {"ref":"FT 2026/1234","resultado":"criada"}
    echo json_encode(estadoDaEntrega('FT 2026/1234'), JSON_UNESCAPED_UNICODE), "\n";
}

Na assinatura, use sempre o corpo tal como chegou (bytes/texto brutos), nunca o JSON já lido e escrito de novo: qualquer espaço a mais muda a assinatura.

Erros

CódigoSignificado
400Pedido inválido (a mensagem em error diz o campo).
401Chave em falta, inválida ou revogada.
403A chave é só de leitura.
404Entrega não encontrada.
409A entrega já saiu e não se pode alterar ou cancelar.
429Demasiados pedidos. Espere um minuto.

As mensagens vêm em { "error": "…" }.

Boas práticas