Skip to content
Last updated

Publicar primeira passagem

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.paga com nsu, txid, metodoPagamento, origemLiquidacao e reservaId.
  • Consultado o extrato e verificado o crédito na subconta.

Tempo estimado: 15 minutos.

Pré-requisitos

  • Credenciais de sandbox: MV_EC_KEY (Basic Auth) e MV_EC_ID.
  • URL de webhook acessível publicamente (use ngrok ou similar para desenvolvimento local).
  • curl instalado.

Passo 1 — Configurar variáveis de ambiente

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.

Passo 2 — Verificar autenticação

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.

Passo 3 — Publicar uma passagem

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.

Idempotência

Se você repetir a chamada com a mesma referenciaExterna (e o mesmo Idempotency-Key), a API retorna o registro existente sem criar duplicata.

Passo 4 — Configurar o endpoint de webhooks

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.

Ambiente de sandbox

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.

Passo 5 — Receber pe.passagem.reservada

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" }
  }
}

Passo 6 — Receber pe.passagem.paga

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.

Passo 7 — Consultar o extrato

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".

Próximo passo