{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-parceiros/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":["admonition"]},"type":"markdown"},"seo":{"title":"Validar HMAC-SHA256","description":"APIs públicas da Movvia para parceiros, estabelecimentos comerciais e clientes de dados veiculares.","meta":[{"name":"theme-color","content":"#7E3DEE"},{"name":"apple-mobile-web-app-title","content":"Movvia Docs"},{"name":"application-name","content":"Movvia Docs"}],"llmstxt":{"hide":false,"sections":[{"title":"Table of contents","includeFiles":["**/*"],"excludeFiles":[]}],"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"validar-hmac-sha256","__idx":0},"children":["Validar HMAC-SHA256"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Todo webhook enviado pela Movvia inclui o header ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-movvia-signature"]}," com a assinatura HMAC-SHA256 do payload. Sempre valide a assinatura antes de processar."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"como-a-assinatura-é-gerada","__idx":1},"children":["Como a assinatura é gerada"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"HMAC-SHA256(secret, payload_utf8) → hex string\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Onde:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["secret"]}," é o ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["webhook_secret"]}," fornecido no onboarding."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["payload_utf8"]}," é o body JSON bruto (bytes), não o objeto deserializado."]}]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"success","name":"Use o body bruto"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Deserializar e re-serializar o JSON pode alterar a ordem das chaves e quebrar a verificação. Leia o body como bytes antes de qualquer parse."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"nodejs","__idx":2},"children":["Node.js"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import crypto from 'crypto';\nimport express from 'express';\n\nconst app = express();\n\n// Capturar body como Buffer antes do parse\napp.use(express.json({\n  verify: (req: any, _res, buf) => { req.rawBody = buf; }\n}));\n\nfunction verificarAssinatura(\n  rawBody: Buffer,\n  signature: string,\n  secret: string\n): boolean {\n  const expected = crypto\n    .createHmac('sha256', secret)\n    .update(rawBody)\n    .digest('hex');\n  return crypto.timingSafeEqual(\n    Buffer.from(signature, 'hex'),\n    Buffer.from(expected, 'hex')\n  );\n}\n\napp.post('/webhook', (req: any, res) => {\n  const sig = req.headers['x-movvia-signature'] as string;\n\n  if (!sig || !verificarAssinatura(req.rawBody, sig, process.env.MV_WEBHOOK_SECRET!)) {\n    return res.status(401).json({ erro: 'Assinatura inválida' });\n  }\n\n  // processar req.body com segurança\n  console.log('Evento válido:', req.body.evento);\n  res.status(200).send('OK');\n});\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"python","__idx":3},"children":["Python"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"import hashlib\nimport hmac\nimport os\nfrom flask import Flask, request, abort\n\napp = Flask(__name__)\nSECRET = os.environ['MV_WEBHOOK_SECRET'].encode()\n\ndef verificar_assinatura(payload: bytes, signature: str) -> bool:\n    expected = hmac.new(SECRET, payload, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(signature, expected)\n\n@app.route('/webhook', methods=['POST'])\ndef webhook():\n    sig = request.headers.get('x-movvia-signature', '')\n    payload = request.get_data()  # body bruto\n\n    if not verificar_assinatura(payload, sig):\n        abort(401)\n\n    evento = request.json\n    print(f\"Evento válido: {evento['evento']}\")\n    return 'OK', 200\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"go","__idx":4},"children":["Go"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"go","header":{"controls":{"copy":{}}},"source":"package main\n\nimport (\n    \"crypto/hmac\"\n    \"crypto/sha256\"\n    \"encoding/hex\"\n    \"io\"\n    \"net/http\"\n    \"os\"\n)\n\nfunc verificarAssinatura(payload []byte, signature, secret string) bool {\n    mac := hmac.New(sha256.New, []byte(secret))\n    mac.Write(payload)\n    expected := hex.EncodeToString(mac.Sum(nil))\n    return hmac.Equal([]byte(signature), []byte(expected))\n}\n\nfunc webhookHandler(w http.ResponseWriter, r *http.Request) {\n    sig := r.Header.Get(\"x-movvia-signature\")\n    body, _ := io.ReadAll(r.Body)\n\n    if !verificarAssinatura(body, sig, os.Getenv(\"MV_WEBHOOK_SECRET\")) {\n        http.Error(w, \"Assinatura inválida\", http.StatusUnauthorized)\n        return\n    }\n\n    w.WriteHeader(http.StatusOK)\n}\n","lang":"go"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"erros-comuns","__idx":5},"children":["Erros comuns"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Problema"},"children":["Problema"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Causa provável"},"children":["Causa provável"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Solução"},"children":["Solução"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Assinatura sempre inválida"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Body deserializado antes da verificação"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Usar body bruto (bytes)"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Comparação insegura"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["=="]}," em vez de ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["timingSafeEqual"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["compare_digest"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Sempre usar comparação em tempo constante"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Secret incorreto"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["webhook_secret"]}," diferente do ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client_secret"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["São credenciais distintas — verificar o correto no onboarding"]}]}]}]}]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"success","name":"Testar localmente"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use o endpoint de simulação do sandbox para gerar eventos reais com assinatura válida. Veja o ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/parceiros/tutorials/quickstart"},"children":["Quickstart"]},"."]}]}]},"headings":[{"value":"Validar HMAC-SHA256","id":"validar-hmac-sha256","depth":1},{"value":"Como a assinatura é gerada","id":"como-a-assinatura-é-gerada","depth":2},{"value":"Node.js","id":"nodejs","depth":2},{"value":"Python","id":"python","depth":2},{"value":"Go","id":"go","depth":2},{"value":"Erros comuns","id":"erros-comuns","depth":2}],"frontmatter":{"title":"Validar HMAC-SHA256 — Parceiros Movvia","description":"Como verificar a assinatura de webhooks HMAC-SHA256 em Node, Python e Go na API de Parceiros Movvia.","seo":{"title":"Validar HMAC-SHA256"}},"lastModified":"2026-04-25T15:17:56.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/parceiros/tutorials/validar-hmac","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}