Ao final deste tutorial, você terá:
- Publicado uma passagem em sandbox via
POST /ec-api/v1/passagens. - Recebido o webhook
pe.passagem.reservada(lock por canal autorizado). - Recebido o webhook
pe.passagem.pagacomnsu,txid,metodoPagamento,origemLiquidacaoereservaId. - Consultado o extrato e verificado o crédito na subconta.
Tempo estimado: 15 minutos.
- Credenciais de sandbox:
MV_EC_KEY(Basic Auth) eMV_EC_ID. - URL de webhook acessível publicamente (use ngrok ou similar para desenvolvimento local).
curlinstalado.
export MV_EC_KEY="Basic <sua_credencial_base64>"
export MV_EC_ID="<seu_ec_id>"
export MV_BASE_URL="https://hml.api.pedagioeletronico.com.br"Substitua os valores pelos fornecidos no onboarding de sandbox.
Antes de publicar, confirme que as credenciais estão corretas:
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: $MV_EC_KEY" \
-H "x-ec-id: $MV_EC_ID" \
"$MV_BASE_URL/ec-api/v1/saldo"Resposta esperada: 200. Se receber 401, verifique MV_EC_KEY e MV_EC_ID.
curl -s -X POST "$MV_BASE_URL/ec-api/v1/passagens" \
-H "Authorization: $MV_EC_KEY" \
-H "x-ec-id: $MV_EC_ID" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: TUTORIAL-001" \
-d '{
"referenciaExterna": "TUTORIAL-001",
"placa": "ABC1D23",
"valor": 12.50,
"tipo": "FREEFLOW",
"praca": { "id": "imigrantes-km245", "nome": "Imigrantes KM 245", "sentido": "SUL" },
"categoria": 1,
"capturadoEm": "2026-04-23T14:32:10Z"
}'Resposta esperada:
{
"success": true,
"data": {
"passagemId": "01JVZ8F4WH9R2T7K5M3N8P6Q4X",
"referenciaExterna": "TUTORIAL-001",
"status": "PENDENTE",
"criadoEm": "2026-04-23T14:32:11Z"
}
}Guarde o passagemId para os próximos passos.
Se você repetir a chamada com a mesma referenciaExterna (e o mesmo Idempotency-Key), a API retorna o registro existente sem criar duplicata.
Registre a URL onde você quer receber os eventos do ciclo de vida da passagem:
curl -s -X POST "$MV_BASE_URL/ec-api/v1/webhooks" \
-H "Authorization: $MV_EC_KEY" \
-H "x-ec-id: $MV_EC_ID" \
-H "Content-Type: application/json" \
-d '{
"url": "https://sua-url.example.com/webhooks/movvia",
"eventos": ["pe.passagem.reservada", "pe.passagem.paga", "pe.passagem.expirada", "pe.passagem.estornada", "pe.repasse.confirmado"]
}'A resposta inclui um webhookSecret — guarde-o; será usado para validar a assinatura HMAC dos eventos recebidos.
Em sandbox, a Movvia simula automaticamente o fluxo completo após a publicação da passagem: emite pe.passagem.reservada em segundos e pe.passagem.paga em seguida. Você não precisa de um canal real para testar.
Em segundos após a publicação, seu endpoint recebe:
POST {sua_url_webhook}
x-movvia-signature: <hmac-sha256-do-payload-bruto>
Content-Type: application/json
{
"evento": "pe.passagem.reservada",
"versao": "2",
"id": "evt_8a3f1b2c9d4e",
"timestamp": "2026-04-23T14:35:00Z",
"ecId": "ec_sandbox_001",
"dados": {
"passagemId": "01JVZ8F4WH9R2T7K5M3N8P6Q4X",
"referenciaExterna": "TUTORIAL-001",
"reservaId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"valor": 12.50,
"expiresAt": "2026-04-23T14:50:00Z",
"canal": { "tipo": "PARCEIRO", "id": "sandbox-canal-simulado" }
}
}Logo depois, o canal confirma o pagamento e você recebe o evento final:
POST {sua_url_webhook}
x-movvia-signature: <hmac-sha256-do-payload-bruto>
Content-Type: application/json
{
"evento": "pe.passagem.paga",
"versao": "2",
"id": "evt_3c4d5e6f7a8b",
"timestamp": "2026-04-23T14:38:11Z",
"ecId": "ec_sandbox_001",
"dados": {
"passagemId": "01JVZ8F4WH9R2T7K5M3N8P6Q4X",
"referenciaExterna": "TUTORIAL-001",
"reservaId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"valor": 12.50,
"valorRepasseLiquido": 12.13,
"comprovante": {
"nsu": "000132547890",
"txid": "MV20260423143810AB7F2C9D",
"metodoPagamento": "PIX",
"origemLiquidacao": "PARCEIRO",
"canalId": "sandbox-canal-simulado",
"pagoEm": "2026-04-23T14:38:10Z"
}
}
}Valide a assinatura antes de processar:
const crypto = require('crypto');
function validarWebhook(payloadBruto, assinaturaRecebida, segredoWebhook) {
const assinaturaEsperada = crypto
.createHmac('sha256', segredoWebhook)
.update(payloadBruto, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(assinaturaEsperada),
Buffer.from(assinaturaRecebida)
);
}
// No seu handler:
app.post('/webhooks/movvia', (req, res) => {
const assinatura = req.headers['x-movvia-signature'];
// IMPORTANTE: usar o body bruto, não JSON.stringify(req.body) — espaços diferentes invalidam a assinatura
const valido = validarWebhook(req.rawBody, assinatura, process.env.MV_WEBHOOK_SECRET);
if (!valido) return res.status(401).send('Assinatura inválida');
res.status(200).send('OK');
// processar evento de forma assíncrona
});import hashlib, hmac
def validar_webhook(payload_bruto: bytes, assinatura_recebida: str, segredo: str) -> bool:
esperada = hmac.new(
segredo.encode(), payload_bruto, hashlib.sha256
).hexdigest()
return hmac.compare_digest(esperada, assinatura_recebida)Convenção idêntica à API Parceiros — ver Webhooks Parceiros para detalhes de retry, idempotência e timing.
Após pe.passagem.paga, verifique o crédito no extrato da subconta:
curl -s "$MV_BASE_URL/ec-api/v1/extrato?desde=2026-04-23" \
-H "Authorization: $MV_EC_KEY" \
-H "x-ec-id: $MV_EC_ID"Você deve ver uma linha de CREDITO com referenciaExterna: "TUTORIAL-001", valor: 12.13 (valorRepasseLiquido) e metodoPagamento: "PIX".
- Leia o ciclo de repasse para entender quando esse crédito vira dinheiro na sua conta.
- Leia disputas e estornos para preparar o fluxo de erros.
- Veja os casos de uso completos: pedágio free flow e pedágio de barreira.