[{"data":1,"prerenderedAt":3031},["ShallowReactive",2],{"docs:\u002Fdocs\u002Fwebhooks":3,"docs-surround:\u002Fdocs\u002Fwebhooks":3008,"docs-navigation":3015},{"id":4,"title":5,"body":6,"description":3001,"extension":3002,"meta":3003,"navigation":954,"path":3004,"seo":3005,"stem":3006,"__hash__":3007},"docs\u002Fdocs\u002F4.webhooks.md","Webhooks",{"type":7,"value":8,"toc":2986},"minimark",[9,13,18,47,51,287,291,302,701,780,787,791,858,862,869,877,916,923,928,1431,1434,1628,1632,1935,1945,1949,2299,2308,2312,2830,2844,2848,2851,2908,2915,2919,2944,2948,2982],[10,11,12],"p",{},"Paygumi te avisa por webhook cada vez que cambia un pago o una suscripción, para que sepas en segundos si el pago de un periodo fue aprobado, quedó pendiente o fue rechazado, y qué acceso dar a tu usuario.",[14,15,17],"h2",{"id":16},"configurar-un-endpoint","Configurar un endpoint",[10,19,20,21,25,26,30,31,34,35,38,39,42,43,46],{},"En el panel, en ",[22,23,24],"strong",{},"Webhooks → Endpoints",", registra una URL ",[27,28,29],"code",{},"https"," de tu backend y elige los tipos de evento (o ",[27,32,33],{},"*"," para todos). Cada entorno tiene sus propios endpoints (hasta 5) y cada endpoint su propio secreto ",[27,36,37],{},"whsec_…",", que usas para verificar la firma. Con ",[22,40,41],{},"Enviar ping"," recibes un evento ",[27,44,45],{},"ping"," de prueba.",[14,48,50],{"id":49},"catálogo-de-eventos","Catálogo de eventos",[52,53,54,67],"table",{},[55,56,57],"thead",{},[58,59,60,64],"tr",{},[61,62,63],"th",{},"Tipo",[61,65,66],{},"Cuándo",[68,69,70,81,91,105,115,125,143,153,163,177,187,197,207,228,248,258,268,278],"tbody",{},[58,71,72,78],{},[73,74,75],"td",{},[27,76,77],{},"payment.approved",[73,79,80],{},"Cobro del periodo aprobado.",[58,82,83,88],{},[73,84,85],{},[27,86,87],{},"payment.pending",[73,89,90],{},"Wompi dejó la transacción pendiente.",[58,92,93,98],{},[73,94,95],{},[27,96,97],{},"payment.declined",[73,99,100,101,104],{},"Intento rechazado (incluye ",[27,102,103],{},"next_retry_at"," si habrá reintento).",[58,106,107,112],{},[73,108,109],{},[27,110,111],{},"payment.uncollectible",[73,113,114],{},"Reintentos agotados.",[58,116,117,122],{},[73,118,119],{},[27,120,121],{},"subscription.created",[73,123,124],{},"Se creó una suscripción.",[58,126,127,132],{},[73,128,129],{},[27,130,131],{},"subscription.activated",[73,133,134,135,138,139,142],{},"La suscripción pasó a ",[27,136,137],{},"active"," (también al recuperarse de ",[27,140,141],{},"unpaid",").",[58,144,145,150],{},[73,146,147],{},[27,148,149],{},"subscription.past_due",[73,151,152],{},"Un cobro fue rechazado y hay reintentos pendientes.",[58,154,155,160],{},[73,156,157],{},[27,158,159],{},"subscription.unpaid",[73,161,162],{},"Se agotaron los reintentos.",[58,164,165,174],{},[73,166,167,170,171],{},[27,168,169],{},"subscription.paused"," \u002F ",[27,172,173],{},"subscription.resumed",[73,175,176],{},"Se pausó o se reanudó.",[58,178,179,184],{},[73,180,181],{},[27,182,183],{},"subscription.canceled",[73,185,186],{},"Se canceló.",[58,188,189,194],{},[73,190,191],{},[27,192,193],{},"subscription.plan_changed",[73,195,196],{},"Cambió el plan.",[58,198,199,204],{},[73,200,201],{},[27,202,203],{},"subscription.updated",[73,205,206],{},"Cambio sin transición de estado (p. ej. cancelación o cambio de plan programados).",[58,208,209,214],{},[73,210,211],{},[27,212,213],{},"subscription.access_changed",[73,215,216,217,220,221,224,225,142],{},"Cambió ",[27,218,219],{},"access_status"," sin cambio de estado (p. ej. vencieron los días de gracia: ",[27,222,223],{},"granted"," → ",[27,226,227],{},"restricted",[58,229,230,235],{},[73,231,232],{},[27,233,234],{},"checkout_session.completed",[73,236,237,238,241,242,245,246,142],{},"El cliente registró su tarjeta en la página de pago (incluye ",[27,239,240],{},"payment_method"," y, en modo ",[27,243,244],{},"subscription",", la ",[27,247,244],{},[58,249,250,255],{},[73,251,252],{},[27,253,254],{},"checkout_session.expired",[73,256,257],{},"La sesión venció sin completarse.",[58,259,260,265],{},[73,261,262],{},[27,263,264],{},"payment_method.added",[73,266,267],{},"Se registró una tarjeta nueva.",[58,269,270,275],{},[73,271,272],{},[27,273,274],{},"payment_method.invalidated",[73,276,277],{},"La fuente de pago dejó de ser válida.",[58,279,280,284],{},[73,281,282],{},[27,283,45],{},[73,285,286],{},"Prueba manual desde el panel (no se suscribe).",[14,288,290],{"id":289},"payload","Payload",[10,292,293,294,297,298,301],{},"Todos los eventos tienen el mismo sobre. ",[27,295,296],{},"data"," trae el ",[22,299,300],{},"estado completo"," de los objetos al momento del evento (estados en minúsculas):",[303,304,309],"pre",{"className":305,"code":306,"language":307,"meta":308,"style":308},"language-json shiki shiki-themes github-light github-dark","{\n  \"id\": \"evt_01JB86R800ABCDEFGHJKMNPQRS\",\n  \"type\": \"payment.declined\",\n  \"created_at\": \"2026-11-01T05:00:12Z\",\n  \"api_version\": \"2026-10-01\",\n  \"livemode\": true,\n  \"initiated_by\": \"system\",\n  \"data\": {\n    \"subscription\": {\n      \"id\": \"sub_01JB86R800ABCDEFGHJKMNPQRS\",\n      \"status\": \"past_due\",\n      \"access_status\": \"granted\",\n      \"access_until\": \"2026-11-04T05:00:00Z\",\n      \"customer\": { \"id\": \"cus_01JB86R800ABCDEFGHJKMNPQRS\", \"external_id\": \"user_42\" },\n      \"plan\": { \"id\": \"pln_01JB86R800ABCDEFGHJKMNPQRS\", \"name\": \"Pro\" },\n      \"current_period\": { \"start\": \"2026-11-01T05:00:00Z\", \"end\": \"2026-12-01T05:00:00Z\" }\n    },\n    \"charge\": {\n      \"id\": \"chg_01JB86R800ABCDEFGHJKMNPQRS\",\n      \"status\": \"failed\",\n      \"amount_in_cents\": 4990000,\n      \"currency\": \"COP\",\n      \"period\": { \"start\": \"2026-11-01T05:00:00Z\", \"end\": \"2026-12-01T05:00:00Z\" },\n      \"attempt\": 1,\n      \"failure_reason\": \"INSUFFICIENT_FUNDS\",\n      \"next_retry_at\": \"2026-11-02T05:00:00Z\"\n    }\n  }\n}\n","json","",[27,310,311,320,337,350,363,376,389,402,411,419,432,445,458,471,502,529,558,564,572,584,596,609,622,646,659,672,683,689,695],{"__ignoreMap":308},[312,313,316],"span",{"class":314,"line":315},"line",1,[312,317,319],{"class":318},"sVt8B","{\n",[312,321,323,327,330,334],{"class":314,"line":322},2,[312,324,326],{"class":325},"sj4cs","  \"id\"",[312,328,329],{"class":318},": ",[312,331,333],{"class":332},"sZZnC","\"evt_01JB86R800ABCDEFGHJKMNPQRS\"",[312,335,336],{"class":318},",\n",[312,338,340,343,345,348],{"class":314,"line":339},3,[312,341,342],{"class":325},"  \"type\"",[312,344,329],{"class":318},[312,346,347],{"class":332},"\"payment.declined\"",[312,349,336],{"class":318},[312,351,353,356,358,361],{"class":314,"line":352},4,[312,354,355],{"class":325},"  \"created_at\"",[312,357,329],{"class":318},[312,359,360],{"class":332},"\"2026-11-01T05:00:12Z\"",[312,362,336],{"class":318},[312,364,366,369,371,374],{"class":314,"line":365},5,[312,367,368],{"class":325},"  \"api_version\"",[312,370,329],{"class":318},[312,372,373],{"class":332},"\"2026-10-01\"",[312,375,336],{"class":318},[312,377,379,382,384,387],{"class":314,"line":378},6,[312,380,381],{"class":325},"  \"livemode\"",[312,383,329],{"class":318},[312,385,386],{"class":325},"true",[312,388,336],{"class":318},[312,390,392,395,397,400],{"class":314,"line":391},7,[312,393,394],{"class":325},"  \"initiated_by\"",[312,396,329],{"class":318},[312,398,399],{"class":332},"\"system\"",[312,401,336],{"class":318},[312,403,405,408],{"class":314,"line":404},8,[312,406,407],{"class":325},"  \"data\"",[312,409,410],{"class":318},": {\n",[312,412,414,417],{"class":314,"line":413},9,[312,415,416],{"class":325},"    \"subscription\"",[312,418,410],{"class":318},[312,420,422,425,427,430],{"class":314,"line":421},10,[312,423,424],{"class":325},"      \"id\"",[312,426,329],{"class":318},[312,428,429],{"class":332},"\"sub_01JB86R800ABCDEFGHJKMNPQRS\"",[312,431,336],{"class":318},[312,433,435,438,440,443],{"class":314,"line":434},11,[312,436,437],{"class":325},"      \"status\"",[312,439,329],{"class":318},[312,441,442],{"class":332},"\"past_due\"",[312,444,336],{"class":318},[312,446,448,451,453,456],{"class":314,"line":447},12,[312,449,450],{"class":325},"      \"access_status\"",[312,452,329],{"class":318},[312,454,455],{"class":332},"\"granted\"",[312,457,336],{"class":318},[312,459,461,464,466,469],{"class":314,"line":460},13,[312,462,463],{"class":325},"      \"access_until\"",[312,465,329],{"class":318},[312,467,468],{"class":332},"\"2026-11-04T05:00:00Z\"",[312,470,336],{"class":318},[312,472,474,477,480,483,485,488,491,494,496,499],{"class":314,"line":473},14,[312,475,476],{"class":325},"      \"customer\"",[312,478,479],{"class":318},": { ",[312,481,482],{"class":325},"\"id\"",[312,484,329],{"class":318},[312,486,487],{"class":332},"\"cus_01JB86R800ABCDEFGHJKMNPQRS\"",[312,489,490],{"class":318},", ",[312,492,493],{"class":325},"\"external_id\"",[312,495,329],{"class":318},[312,497,498],{"class":332},"\"user_42\"",[312,500,501],{"class":318}," },\n",[312,503,505,508,510,512,514,517,519,522,524,527],{"class":314,"line":504},15,[312,506,507],{"class":325},"      \"plan\"",[312,509,479],{"class":318},[312,511,482],{"class":325},[312,513,329],{"class":318},[312,515,516],{"class":332},"\"pln_01JB86R800ABCDEFGHJKMNPQRS\"",[312,518,490],{"class":318},[312,520,521],{"class":325},"\"name\"",[312,523,329],{"class":318},[312,525,526],{"class":332},"\"Pro\"",[312,528,501],{"class":318},[312,530,532,535,537,540,542,545,547,550,552,555],{"class":314,"line":531},16,[312,533,534],{"class":325},"      \"current_period\"",[312,536,479],{"class":318},[312,538,539],{"class":325},"\"start\"",[312,541,329],{"class":318},[312,543,544],{"class":332},"\"2026-11-01T05:00:00Z\"",[312,546,490],{"class":318},[312,548,549],{"class":325},"\"end\"",[312,551,329],{"class":318},[312,553,554],{"class":332},"\"2026-12-01T05:00:00Z\"",[312,556,557],{"class":318}," }\n",[312,559,561],{"class":314,"line":560},17,[312,562,563],{"class":318},"    },\n",[312,565,567,570],{"class":314,"line":566},18,[312,568,569],{"class":325},"    \"charge\"",[312,571,410],{"class":318},[312,573,575,577,579,582],{"class":314,"line":574},19,[312,576,424],{"class":325},[312,578,329],{"class":318},[312,580,581],{"class":332},"\"chg_01JB86R800ABCDEFGHJKMNPQRS\"",[312,583,336],{"class":318},[312,585,587,589,591,594],{"class":314,"line":586},20,[312,588,437],{"class":325},[312,590,329],{"class":318},[312,592,593],{"class":332},"\"failed\"",[312,595,336],{"class":318},[312,597,599,602,604,607],{"class":314,"line":598},21,[312,600,601],{"class":325},"      \"amount_in_cents\"",[312,603,329],{"class":318},[312,605,606],{"class":325},"4990000",[312,608,336],{"class":318},[312,610,612,615,617,620],{"class":314,"line":611},22,[312,613,614],{"class":325},"      \"currency\"",[312,616,329],{"class":318},[312,618,619],{"class":332},"\"COP\"",[312,621,336],{"class":318},[312,623,625,628,630,632,634,636,638,640,642,644],{"class":314,"line":624},23,[312,626,627],{"class":325},"      \"period\"",[312,629,479],{"class":318},[312,631,539],{"class":325},[312,633,329],{"class":318},[312,635,544],{"class":332},[312,637,490],{"class":318},[312,639,549],{"class":325},[312,641,329],{"class":318},[312,643,554],{"class":332},[312,645,501],{"class":318},[312,647,649,652,654,657],{"class":314,"line":648},24,[312,650,651],{"class":325},"      \"attempt\"",[312,653,329],{"class":318},[312,655,656],{"class":325},"1",[312,658,336],{"class":318},[312,660,662,665,667,670],{"class":314,"line":661},25,[312,663,664],{"class":325},"      \"failure_reason\"",[312,666,329],{"class":318},[312,668,669],{"class":332},"\"INSUFFICIENT_FUNDS\"",[312,671,336],{"class":318},[312,673,675,678,680],{"class":314,"line":674},26,[312,676,677],{"class":325},"      \"next_retry_at\"",[312,679,329],{"class":318},[312,681,682],{"class":332},"\"2026-11-02T05:00:00Z\"\n",[312,684,686],{"class":314,"line":685},27,[312,687,688],{"class":318},"    }\n",[312,690,692],{"class":314,"line":691},28,[312,693,694],{"class":318},"  }\n",[312,696,698],{"class":314,"line":697},29,[312,699,700],{"class":318},"}\n",[702,703,704,722,761],"ul",{},[705,706,707,710,711,713,714,717,718,721],"li",{},[27,708,709],{},"livemode"," es ",[27,712,386],{}," en producción y ",[27,715,716],{},"false"," en sandbox. Los envíos de prueba llevan además ",[27,719,720],{},"\"test\": true",".",[705,723,724,727,728,731,732,735,736,739,740,743,744,749,750,753,754,757,758,721],{},[27,725,726],{},"initiated_by"," dice quién causó el evento: ",[27,729,730],{},"system"," (cobros y reintentos), ",[27,733,734],{},"tenant_api"," (tu backend), ",[27,737,738],{},"dashboard"," (el panel) o ",[27,741,742],{},"customer_portal"," (tu cliente desde el ",[745,746,748],"a",{"href":747},"\u002Fdocs\u002Fportal-del-cliente","portal","). Si el cliente canceló desde el portal, ",[27,751,752],{},"data.cancellation"," trae ",[27,755,756],{},"reason"," y ",[27,759,760],{},"comment",[705,762,763,764,329,767,490,769,771,772,775,776,721],{},"El campo clave es ",[27,765,766],{},"data.subscription.access_status",[27,768,223],{},[27,770,227],{}," o ",[27,773,774],{},"revoked",". Ver ",[745,777,779],{"href":778},"\u002Fdocs\u002Fsuscripciones-y-acceso","estados de acceso",[10,781,782,783,721],{},"Todos los campos en la ",[745,784,786],{"href":785},"\u002Fdocs\u002Fapi\u002Feventos","referencia de eventos",[14,788,790],{"id":789},"cabeceras","Cabeceras",[303,792,796],{"className":793,"code":794,"language":795,"meta":308,"style":308},"language-http shiki shiki-themes github-light github-dark","POST \u002Fwebhooks\u002Fpaygumi HTTP\u002F1.1\nContent-Type: application\u002Fjson\nPaygumi-Event-Id: evt_01JB86R800ABCDEFGHJKMNPQRS\nPaygumi-Timestamp: 1793509212\nPaygumi-Signature: v1=a6e678f685978f4335967817ea566984611995345a4b65a347ccd2bd51afe0ed\n","http",[27,797,798,816,828,838,848],{"__ignoreMap":308},[312,799,800,804,807,810,813],{"class":314,"line":315},[312,801,803],{"class":802},"szBVR","POST",[312,805,806],{"class":318}," \u002Fwebhooks\u002Fpaygumi ",[312,808,809],{"class":802},"HTTP",[312,811,812],{"class":318},"\u002F",[312,814,815],{"class":325},"1.1\n",[312,817,818,822,825],{"class":314,"line":322},[312,819,821],{"class":820},"s9eBZ","Content-Type",[312,823,824],{"class":802},":",[312,826,827],{"class":332}," application\u002Fjson\n",[312,829,830,833,835],{"class":314,"line":339},[312,831,832],{"class":820},"Paygumi-Event-Id",[312,834,824],{"class":802},[312,836,837],{"class":332}," evt_01JB86R800ABCDEFGHJKMNPQRS\n",[312,839,840,843,845],{"class":314,"line":352},[312,841,842],{"class":820},"Paygumi-Timestamp",[312,844,824],{"class":802},[312,846,847],{"class":332}," 1793509212\n",[312,849,850,853,855],{"class":314,"line":365},[312,851,852],{"class":820},"Paygumi-Signature",[312,854,824],{"class":802},[312,856,857],{"class":332}," v1=a6e678f685978f4335967817ea566984611995345a4b65a347ccd2bd51afe0ed\n",[14,859,861],{"id":860},"verificar-la-firma","Verificar la firma",[10,863,864,865,868],{},"La firma es un HMAC-SHA256, en hexadecimal, del timestamp, un punto y el cuerpo ",[22,866,867],{},"exacto"," que recibiste:",[303,870,875],{"className":871,"code":873,"language":874,"meta":308},[872],"language-text","Paygumi-Signature: v1=hex(HMAC_SHA256(secreto, Paygumi-Timestamp + \".\" + cuerpo))\n","text",[27,876,873],{"__ignoreMap":308},[702,878,879,885,892,899,902],{},[705,880,881,882,721],{},"La llave del HMAC es el texto completo del secreto, incluido el prefijo ",[27,883,884],{},"whsec_",[705,886,887,888,891],{},"Firma sobre los ",[22,889,890],{},"bytes crudos"," del cuerpo, antes de parsear el JSON: si lo parseas y lo vuelves a serializar, la firma no coincide.",[705,893,894,895,898],{},"Rechaza timestamps con más de ",[22,896,897],{},"5 minutos"," de diferencia con tu reloj (evita que reenvíen un evento capturado).",[705,900,901],{},"Compara en tiempo constante.",[705,903,904,907,908,911,912,915],{},[22,905,906],{},"Rotación de secreto:"," durante las 24 h siguientes a rotar el secreto en el panel, la cabecera trae dos firmas separadas por coma, ",[27,909,910],{},"v1=\u003Cnuevo>,v1=\u003Canterior>",". Acepta el evento si ",[22,913,914],{},"alguna"," coincide con tu secreto.",[10,917,918,919,721],{},"Los siguientes ejemplos implementan exactamente eso. Están probados contra el ",[745,920,922],{"href":921},"#vector-de-prueba","vector de prueba",[924,925,927],"h3",{"id":926},"nodejs","Node.js",[303,929,934],{"className":930,"code":931,"filename":932,"language":933,"meta":308,"style":308},"language-js shiki shiki-themes github-light github-dark","import crypto from 'node:crypto'\n\nconst TOLERANCE_SECONDS = 300\n\n\u002F**\n * Verifica un webhook de Paygumi.\n * @param {string} secret Secreto del endpoint (whsec_…).\n * @param {string} timestamp Cabecera Paygumi-Timestamp.\n * @param {string} signatureHeader Cabecera Paygumi-Signature.\n * @param {Buffer|string} rawBody Cuerpo crudo, tal como llegó.\n * @param {number} [now] Segundos Unix actuales.\n *\u002F\nexport function verifyPaygumiSignature(secret, timestamp, signatureHeader, rawBody, now = Math.floor(Date.now() \u002F 1000)) {\n  if (!\u002F^\\d+$\u002F.test(timestamp ?? '')) return false\n  if (Math.abs(now - Number(timestamp)) > TOLERANCE_SECONDS) return false\n\n  const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest()\n\n  return (signatureHeader ?? '').split(',').some((part) => {\n    const [scheme, hex] = part.trim().split('=')\n    if (scheme !== 'v1' || !\u002F^[0-9a-f]{64}$\u002F.test(hex ?? '')) return false\n    return crypto.timingSafeEqual(Buffer.from(hex, 'hex'), expected)\n  })\n}\n","verify.mjs","js",[27,935,936,950,956,970,974,980,985,1003,1017,1031,1046,1061,1066,1128,1175,1209,1213,1263,1267,1308,1349,1398,1422,1427],{"__ignoreMap":308},[312,937,938,941,944,947],{"class":314,"line":315},[312,939,940],{"class":802},"import",[312,942,943],{"class":318}," crypto ",[312,945,946],{"class":802},"from",[312,948,949],{"class":332}," 'node:crypto'\n",[312,951,952],{"class":314,"line":322},[312,953,955],{"emptyLinePlaceholder":954},true,"\n",[312,957,958,961,964,967],{"class":314,"line":339},[312,959,960],{"class":802},"const",[312,962,963],{"class":325}," TOLERANCE_SECONDS",[312,965,966],{"class":802}," =",[312,968,969],{"class":325}," 300\n",[312,971,972],{"class":314,"line":352},[312,973,955],{"emptyLinePlaceholder":954},[312,975,976],{"class":314,"line":365},[312,977,979],{"class":978},"sJ8bj","\u002F**\n",[312,981,982],{"class":314,"line":378},[312,983,984],{"class":978}," * Verifica un webhook de Paygumi.\n",[312,986,987,990,993,997,1000],{"class":314,"line":391},[312,988,989],{"class":978}," * ",[312,991,992],{"class":802},"@param",[312,994,996],{"class":995},"sScJk"," {string}",[312,998,999],{"class":318}," secret",[312,1001,1002],{"class":978}," Secreto del endpoint (whsec_…).\n",[312,1004,1005,1007,1009,1011,1014],{"class":314,"line":404},[312,1006,989],{"class":978},[312,1008,992],{"class":802},[312,1010,996],{"class":995},[312,1012,1013],{"class":318}," timestamp",[312,1015,1016],{"class":978}," Cabecera Paygumi-Timestamp.\n",[312,1018,1019,1021,1023,1025,1028],{"class":314,"line":413},[312,1020,989],{"class":978},[312,1022,992],{"class":802},[312,1024,996],{"class":995},[312,1026,1027],{"class":318}," signatureHeader",[312,1029,1030],{"class":978}," Cabecera Paygumi-Signature.\n",[312,1032,1033,1035,1037,1040,1043],{"class":314,"line":421},[312,1034,989],{"class":978},[312,1036,992],{"class":802},[312,1038,1039],{"class":995}," {Buffer|string}",[312,1041,1042],{"class":318}," rawBody",[312,1044,1045],{"class":978}," Cuerpo crudo, tal como llegó.\n",[312,1047,1048,1050,1052,1055,1058],{"class":314,"line":434},[312,1049,989],{"class":978},[312,1051,992],{"class":802},[312,1053,1054],{"class":995}," {number}",[312,1056,1057],{"class":318}," [now]",[312,1059,1060],{"class":978}," Segundos Unix actuales.\n",[312,1062,1063],{"class":314,"line":447},[312,1064,1065],{"class":978}," *\u002F\n",[312,1067,1068,1071,1074,1077,1080,1084,1086,1089,1091,1094,1096,1099,1101,1104,1106,1109,1112,1115,1117,1120,1122,1125],{"class":314,"line":460},[312,1069,1070],{"class":802},"export",[312,1072,1073],{"class":802}," function",[312,1075,1076],{"class":995}," verifyPaygumiSignature",[312,1078,1079],{"class":318},"(",[312,1081,1083],{"class":1082},"s4XuR","secret",[312,1085,490],{"class":318},[312,1087,1088],{"class":1082},"timestamp",[312,1090,490],{"class":318},[312,1092,1093],{"class":1082},"signatureHeader",[312,1095,490],{"class":318},[312,1097,1098],{"class":1082},"rawBody",[312,1100,490],{"class":318},[312,1102,1103],{"class":1082},"now",[312,1105,966],{"class":802},[312,1107,1108],{"class":318}," Math.",[312,1110,1111],{"class":995},"floor",[312,1113,1114],{"class":318},"(Date.",[312,1116,1103],{"class":995},[312,1118,1119],{"class":318},"() ",[312,1121,812],{"class":802},[312,1123,1124],{"class":325}," 1000",[312,1126,1127],{"class":318},")) {\n",[312,1129,1130,1133,1136,1139,1141,1144,1147,1150,1152,1154,1157,1160,1163,1166,1169,1172],{"class":314,"line":473},[312,1131,1132],{"class":802},"  if",[312,1134,1135],{"class":318}," (",[312,1137,1138],{"class":802},"!",[312,1140,812],{"class":332},[312,1142,1143],{"class":802},"^",[312,1145,1146],{"class":325},"\\d",[312,1148,1149],{"class":802},"+$",[312,1151,812],{"class":332},[312,1153,721],{"class":318},[312,1155,1156],{"class":995},"test",[312,1158,1159],{"class":318},"(timestamp ",[312,1161,1162],{"class":802},"??",[312,1164,1165],{"class":332}," ''",[312,1167,1168],{"class":318},")) ",[312,1170,1171],{"class":802},"return",[312,1173,1174],{"class":325}," false\n",[312,1176,1177,1179,1182,1185,1188,1191,1194,1197,1200,1202,1205,1207],{"class":314,"line":504},[312,1178,1132],{"class":802},[312,1180,1181],{"class":318}," (Math.",[312,1183,1184],{"class":995},"abs",[312,1186,1187],{"class":318},"(now ",[312,1189,1190],{"class":802},"-",[312,1192,1193],{"class":995}," Number",[312,1195,1196],{"class":318},"(timestamp)) ",[312,1198,1199],{"class":802},">",[312,1201,963],{"class":325},[312,1203,1204],{"class":318},") ",[312,1206,1171],{"class":802},[312,1208,1174],{"class":325},[312,1210,1211],{"class":314,"line":531},[312,1212,955],{"emptyLinePlaceholder":954},[312,1214,1215,1218,1221,1223,1226,1229,1231,1234,1237,1240,1242,1245,1247,1250,1252,1254,1257,1260],{"class":314,"line":560},[312,1216,1217],{"class":802},"  const",[312,1219,1220],{"class":325}," expected",[312,1222,966],{"class":802},[312,1224,1225],{"class":318}," crypto.",[312,1227,1228],{"class":995},"createHmac",[312,1230,1079],{"class":318},[312,1232,1233],{"class":332},"'sha256'",[312,1235,1236],{"class":318},", secret).",[312,1238,1239],{"class":995},"update",[312,1241,1079],{"class":318},[312,1243,1244],{"class":332},"`${",[312,1246,1088],{"class":318},[312,1248,1249],{"class":332},"}.`",[312,1251,142],{"class":318},[312,1253,1239],{"class":995},[312,1255,1256],{"class":318},"(rawBody).",[312,1258,1259],{"class":995},"digest",[312,1261,1262],{"class":318},"()\n",[312,1264,1265],{"class":314,"line":566},[312,1266,955],{"emptyLinePlaceholder":954},[312,1268,1269,1272,1275,1277,1279,1281,1284,1286,1289,1291,1294,1297,1300,1302,1305],{"class":314,"line":574},[312,1270,1271],{"class":802},"  return",[312,1273,1274],{"class":318}," (signatureHeader ",[312,1276,1162],{"class":802},[312,1278,1165],{"class":332},[312,1280,142],{"class":318},[312,1282,1283],{"class":995},"split",[312,1285,1079],{"class":318},[312,1287,1288],{"class":332},"','",[312,1290,142],{"class":318},[312,1292,1293],{"class":995},"some",[312,1295,1296],{"class":318},"((",[312,1298,1299],{"class":1082},"part",[312,1301,1204],{"class":318},[312,1303,1304],{"class":802},"=>",[312,1306,1307],{"class":318}," {\n",[312,1309,1310,1313,1316,1319,1321,1324,1327,1330,1333,1336,1339,1341,1343,1346],{"class":314,"line":586},[312,1311,1312],{"class":802},"    const",[312,1314,1315],{"class":318}," [",[312,1317,1318],{"class":325},"scheme",[312,1320,490],{"class":318},[312,1322,1323],{"class":325},"hex",[312,1325,1326],{"class":318},"] ",[312,1328,1329],{"class":802},"=",[312,1331,1332],{"class":318}," part.",[312,1334,1335],{"class":995},"trim",[312,1337,1338],{"class":318},"().",[312,1340,1283],{"class":995},[312,1342,1079],{"class":318},[312,1344,1345],{"class":332},"'='",[312,1347,1348],{"class":318},")\n",[312,1350,1351,1354,1357,1360,1363,1366,1369,1371,1373,1376,1379,1381,1383,1385,1388,1390,1392,1394,1396],{"class":314,"line":598},[312,1352,1353],{"class":802},"    if",[312,1355,1356],{"class":318}," (scheme ",[312,1358,1359],{"class":802},"!==",[312,1361,1362],{"class":332}," 'v1'",[312,1364,1365],{"class":802}," ||",[312,1367,1368],{"class":802}," !",[312,1370,812],{"class":332},[312,1372,1143],{"class":802},[312,1374,1375],{"class":325},"[0-9a-f]",[312,1377,1378],{"class":802},"{64}$",[312,1380,812],{"class":332},[312,1382,721],{"class":318},[312,1384,1156],{"class":995},[312,1386,1387],{"class":318},"(hex ",[312,1389,1162],{"class":802},[312,1391,1165],{"class":332},[312,1393,1168],{"class":318},[312,1395,1171],{"class":802},[312,1397,1174],{"class":325},[312,1399,1400,1403,1405,1408,1411,1413,1416,1419],{"class":314,"line":611},[312,1401,1402],{"class":802},"    return",[312,1404,1225],{"class":318},[312,1406,1407],{"class":995},"timingSafeEqual",[312,1409,1410],{"class":318},"(Buffer.",[312,1412,946],{"class":995},[312,1414,1415],{"class":318},"(hex, ",[312,1417,1418],{"class":332},"'hex'",[312,1420,1421],{"class":318},"), expected)\n",[312,1423,1424],{"class":314,"line":624},[312,1425,1426],{"class":318},"  })\n",[312,1428,1429],{"class":314,"line":648},[312,1430,700],{"class":318},[10,1432,1433],{},"Con Express, lee el cuerpo crudo solo en la ruta del webhook:",[303,1435,1437],{"className":930,"code":1436,"language":933,"meta":308,"style":308},"app.post('\u002Fwebhooks\u002Fpaygumi', express.raw({ type: 'application\u002Fjson' }), (req, res) => {\n  const ok = verifyPaygumiSignature(\n    process.env.PAYGUMI_WEBHOOK_SECRET,\n    req.get('Paygumi-Timestamp'),\n    req.get('Paygumi-Signature'),\n    req.body, \u002F\u002F Buffer\n  )\n  if (!ok) return res.sendStatus(400)\n  const event = JSON.parse(req.body.toString('utf8'))\n  \u002F\u002F Encola el evento y responde rápido.\n  res.sendStatus(200)\n})\n",[27,1438,1439,1481,1495,1505,1521,1534,1542,1547,1573,1604,1609,1623],{"__ignoreMap":308},[312,1440,1441,1444,1447,1449,1452,1455,1458,1461,1464,1467,1470,1472,1475,1477,1479],{"class":314,"line":315},[312,1442,1443],{"class":318},"app.",[312,1445,1446],{"class":995},"post",[312,1448,1079],{"class":318},[312,1450,1451],{"class":332},"'\u002Fwebhooks\u002Fpaygumi'",[312,1453,1454],{"class":318},", express.",[312,1456,1457],{"class":995},"raw",[312,1459,1460],{"class":318},"({ type: ",[312,1462,1463],{"class":332},"'application\u002Fjson'",[312,1465,1466],{"class":318}," }), (",[312,1468,1469],{"class":1082},"req",[312,1471,490],{"class":318},[312,1473,1474],{"class":1082},"res",[312,1476,1204],{"class":318},[312,1478,1304],{"class":802},[312,1480,1307],{"class":318},[312,1482,1483,1485,1488,1490,1492],{"class":314,"line":322},[312,1484,1217],{"class":802},[312,1486,1487],{"class":325}," ok",[312,1489,966],{"class":802},[312,1491,1076],{"class":995},[312,1493,1494],{"class":318},"(\n",[312,1496,1497,1500,1503],{"class":314,"line":339},[312,1498,1499],{"class":318},"    process.env.",[312,1501,1502],{"class":325},"PAYGUMI_WEBHOOK_SECRET",[312,1504,336],{"class":318},[312,1506,1507,1510,1513,1515,1518],{"class":314,"line":352},[312,1508,1509],{"class":318},"    req.",[312,1511,1512],{"class":995},"get",[312,1514,1079],{"class":318},[312,1516,1517],{"class":332},"'Paygumi-Timestamp'",[312,1519,1520],{"class":318},"),\n",[312,1522,1523,1525,1527,1529,1532],{"class":314,"line":365},[312,1524,1509],{"class":318},[312,1526,1512],{"class":995},[312,1528,1079],{"class":318},[312,1530,1531],{"class":332},"'Paygumi-Signature'",[312,1533,1520],{"class":318},[312,1535,1536,1539],{"class":314,"line":378},[312,1537,1538],{"class":318},"    req.body, ",[312,1540,1541],{"class":978},"\u002F\u002F Buffer\n",[312,1543,1544],{"class":314,"line":391},[312,1545,1546],{"class":318},"  )\n",[312,1548,1549,1551,1553,1555,1558,1560,1563,1566,1568,1571],{"class":314,"line":404},[312,1550,1132],{"class":802},[312,1552,1135],{"class":318},[312,1554,1138],{"class":802},[312,1556,1557],{"class":318},"ok) ",[312,1559,1171],{"class":802},[312,1561,1562],{"class":318}," res.",[312,1564,1565],{"class":995},"sendStatus",[312,1567,1079],{"class":318},[312,1569,1570],{"class":325},"400",[312,1572,1348],{"class":318},[312,1574,1575,1577,1580,1582,1585,1587,1590,1593,1596,1598,1601],{"class":314,"line":413},[312,1576,1217],{"class":802},[312,1578,1579],{"class":325}," event",[312,1581,966],{"class":802},[312,1583,1584],{"class":325}," JSON",[312,1586,721],{"class":318},[312,1588,1589],{"class":995},"parse",[312,1591,1592],{"class":318},"(req.body.",[312,1594,1595],{"class":995},"toString",[312,1597,1079],{"class":318},[312,1599,1600],{"class":332},"'utf8'",[312,1602,1603],{"class":318},"))\n",[312,1605,1606],{"class":314,"line":421},[312,1607,1608],{"class":978},"  \u002F\u002F Encola el evento y responde rápido.\n",[312,1610,1611,1614,1616,1618,1621],{"class":314,"line":434},[312,1612,1613],{"class":318},"  res.",[312,1615,1565],{"class":995},[312,1617,1079],{"class":318},[312,1619,1620],{"class":325},"200",[312,1622,1348],{"class":318},[312,1624,1625],{"class":314,"line":447},[312,1626,1627],{"class":318},"})\n",[924,1629,1631],{"id":1630},"python","Python",[303,1633,1637],{"className":1634,"code":1635,"filename":1636,"language":1630,"meta":308,"style":308},"language-python shiki shiki-themes github-light github-dark","import hashlib\nimport hmac\nimport time\n\nTOLERANCE_SECONDS = 300\n\n\ndef verify_paygumi_signature(secret, timestamp, signature_header, raw_body, now=None):\n    \"\"\"Verifica un webhook de Paygumi. raw_body son los bytes crudos del cuerpo.\"\"\"\n    if not timestamp or not (timestamp.isascii() and timestamp.isdigit()):\n        return False\n    now = int(time.time()) if now is None else now\n    if abs(now - int(timestamp)) > TOLERANCE_SECONDS:\n        return False\n\n    expected = hmac.new(\n        secret.encode(\"utf-8\"),\n        timestamp.encode(\"ascii\") + b\".\" + raw_body,\n        hashlib.sha256,\n    ).hexdigest()\n\n    for part in (signature_header or \"\").split(\",\"):\n        scheme, _, signature = part.strip().partition(\"=\")\n        if scheme == \"v1\" and hmac.compare_digest(signature, expected):\n            return True\n    return False\n","verify.py",[27,1638,1639,1646,1653,1660,1664,1673,1677,1681,1700,1705,1729,1737,1768,1790,1796,1800,1810,1820,1845,1850,1855,1859,1886,1901,1921,1929],{"__ignoreMap":308},[312,1640,1641,1643],{"class":314,"line":315},[312,1642,940],{"class":802},[312,1644,1645],{"class":318}," hashlib\n",[312,1647,1648,1650],{"class":314,"line":322},[312,1649,940],{"class":802},[312,1651,1652],{"class":318}," hmac\n",[312,1654,1655,1657],{"class":314,"line":339},[312,1656,940],{"class":802},[312,1658,1659],{"class":318}," time\n",[312,1661,1662],{"class":314,"line":352},[312,1663,955],{"emptyLinePlaceholder":954},[312,1665,1666,1669,1671],{"class":314,"line":365},[312,1667,1668],{"class":325},"TOLERANCE_SECONDS",[312,1670,966],{"class":802},[312,1672,969],{"class":325},[312,1674,1675],{"class":314,"line":378},[312,1676,955],{"emptyLinePlaceholder":954},[312,1678,1679],{"class":314,"line":391},[312,1680,955],{"emptyLinePlaceholder":954},[312,1682,1683,1686,1689,1692,1694,1697],{"class":314,"line":404},[312,1684,1685],{"class":802},"def",[312,1687,1688],{"class":995}," verify_paygumi_signature",[312,1690,1691],{"class":318},"(secret, timestamp, signature_header, raw_body, now",[312,1693,1329],{"class":802},[312,1695,1696],{"class":325},"None",[312,1698,1699],{"class":318},"):\n",[312,1701,1702],{"class":314,"line":413},[312,1703,1704],{"class":332},"    \"\"\"Verifica un webhook de Paygumi. raw_body son los bytes crudos del cuerpo.\"\"\"\n",[312,1706,1707,1709,1712,1715,1718,1720,1723,1726],{"class":314,"line":421},[312,1708,1353],{"class":802},[312,1710,1711],{"class":802}," not",[312,1713,1714],{"class":318}," timestamp ",[312,1716,1717],{"class":802},"or",[312,1719,1711],{"class":802},[312,1721,1722],{"class":318}," (timestamp.isascii() ",[312,1724,1725],{"class":802},"and",[312,1727,1728],{"class":318}," timestamp.isdigit()):\n",[312,1730,1731,1734],{"class":314,"line":434},[312,1732,1733],{"class":802},"        return",[312,1735,1736],{"class":325}," False\n",[312,1738,1739,1742,1744,1747,1750,1753,1756,1759,1762,1765],{"class":314,"line":447},[312,1740,1741],{"class":318},"    now ",[312,1743,1329],{"class":802},[312,1745,1746],{"class":325}," int",[312,1748,1749],{"class":318},"(time.time()) ",[312,1751,1752],{"class":802},"if",[312,1754,1755],{"class":318}," now ",[312,1757,1758],{"class":802},"is",[312,1760,1761],{"class":325}," None",[312,1763,1764],{"class":802}," else",[312,1766,1767],{"class":318}," now\n",[312,1769,1770,1772,1775,1777,1779,1781,1783,1785,1787],{"class":314,"line":460},[312,1771,1353],{"class":802},[312,1773,1774],{"class":325}," abs",[312,1776,1187],{"class":318},[312,1778,1190],{"class":802},[312,1780,1746],{"class":325},[312,1782,1196],{"class":318},[312,1784,1199],{"class":802},[312,1786,963],{"class":325},[312,1788,1789],{"class":318},":\n",[312,1791,1792,1794],{"class":314,"line":473},[312,1793,1733],{"class":802},[312,1795,1736],{"class":325},[312,1797,1798],{"class":314,"line":504},[312,1799,955],{"emptyLinePlaceholder":954},[312,1801,1802,1805,1807],{"class":314,"line":531},[312,1803,1804],{"class":318},"    expected ",[312,1806,1329],{"class":802},[312,1808,1809],{"class":318}," hmac.new(\n",[312,1811,1812,1815,1818],{"class":314,"line":560},[312,1813,1814],{"class":318},"        secret.encode(",[312,1816,1817],{"class":332},"\"utf-8\"",[312,1819,1520],{"class":318},[312,1821,1822,1825,1828,1830,1833,1836,1839,1842],{"class":314,"line":566},[312,1823,1824],{"class":318},"        timestamp.encode(",[312,1826,1827],{"class":332},"\"ascii\"",[312,1829,1204],{"class":318},[312,1831,1832],{"class":802},"+",[312,1834,1835],{"class":802}," b",[312,1837,1838],{"class":332},"\".\"",[312,1840,1841],{"class":802}," +",[312,1843,1844],{"class":318}," raw_body,\n",[312,1846,1847],{"class":314,"line":574},[312,1848,1849],{"class":318},"        hashlib.sha256,\n",[312,1851,1852],{"class":314,"line":586},[312,1853,1854],{"class":318},"    ).hexdigest()\n",[312,1856,1857],{"class":314,"line":598},[312,1858,955],{"emptyLinePlaceholder":954},[312,1860,1861,1864,1867,1870,1873,1875,1878,1881,1884],{"class":314,"line":611},[312,1862,1863],{"class":802},"    for",[312,1865,1866],{"class":318}," part ",[312,1868,1869],{"class":802},"in",[312,1871,1872],{"class":318}," (signature_header ",[312,1874,1717],{"class":802},[312,1876,1877],{"class":332}," \"\"",[312,1879,1880],{"class":318},").split(",[312,1882,1883],{"class":332},"\",\"",[312,1885,1699],{"class":318},[312,1887,1888,1891,1893,1896,1899],{"class":314,"line":624},[312,1889,1890],{"class":318},"        scheme, _, signature ",[312,1892,1329],{"class":802},[312,1894,1895],{"class":318}," part.strip().partition(",[312,1897,1898],{"class":332},"\"=\"",[312,1900,1348],{"class":318},[312,1902,1903,1906,1909,1912,1915,1918],{"class":314,"line":648},[312,1904,1905],{"class":802},"        if",[312,1907,1908],{"class":318}," scheme ",[312,1910,1911],{"class":802},"==",[312,1913,1914],{"class":332}," \"v1\"",[312,1916,1917],{"class":802}," and",[312,1919,1920],{"class":318}," hmac.compare_digest(signature, expected):\n",[312,1922,1923,1926],{"class":314,"line":661},[312,1924,1925],{"class":802},"            return",[312,1927,1928],{"class":325}," True\n",[312,1930,1931,1933],{"class":314,"line":674},[312,1932,1402],{"class":802},[312,1934,1736],{"class":325},[10,1936,1937,1938,1941,1942,721],{},"En Flask, ",[27,1939,1940],{},"request.get_data()"," devuelve los bytes crudos; en Django, ",[27,1943,1944],{},"request.body",[924,1946,1948],{"id":1947},"php","PHP",[303,1950,1954],{"className":1951,"code":1952,"filename":1953,"language":1947,"meta":308,"style":308},"language-php shiki shiki-themes github-light github-dark","\u003C?php\n\nconst PAYGUMI_TOLERANCE_SECONDS = 300;\n\n\u002F**\n * Verifica un webhook de Paygumi. $rawBody es file_get_contents('php:\u002F\u002Finput').\n *\u002F\nfunction verify_paygumi_signature(string $secret, string $timestamp, string $signatureHeader, string $rawBody, ?int $now = null): bool\n{\n    if (!ctype_digit($timestamp)) {\n        return false;\n    }\n    $now = $now ?? time();\n    if (abs($now - (int) $timestamp) > PAYGUMI_TOLERANCE_SECONDS) {\n        return false;\n    }\n\n    $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);\n\n    foreach (explode(',', $signatureHeader) as $part) {\n        $pieces = explode('=', trim($part), 2);\n        if (count($pieces) === 2 && $pieces[0] === 'v1' && hash_equals($expected, $pieces[1])) {\n            return true;\n        }\n    }\n    return false;\n}\n","verify.php",[27,1955,1956,1964,1968,1983,1987,1991,1996,2000,2049,2053,2067,2076,2080,2097,2125,2133,2137,2141,2169,2173,2196,2223,2269,2278,2283,2287,2295],{"__ignoreMap":308},[312,1957,1958,1961],{"class":314,"line":315},[312,1959,1960],{"class":802},"\u003C?",[312,1962,1963],{"class":325},"php\n",[312,1965,1966],{"class":314,"line":322},[312,1967,955],{"emptyLinePlaceholder":954},[312,1969,1970,1972,1975,1977,1980],{"class":314,"line":339},[312,1971,960],{"class":802},[312,1973,1974],{"class":325}," PAYGUMI_TOLERANCE_SECONDS",[312,1976,966],{"class":802},[312,1978,1979],{"class":325}," 300",[312,1981,1982],{"class":318},";\n",[312,1984,1985],{"class":314,"line":352},[312,1986,955],{"emptyLinePlaceholder":954},[312,1988,1989],{"class":314,"line":365},[312,1990,979],{"class":978},[312,1992,1993],{"class":314,"line":378},[312,1994,1995],{"class":978}," * Verifica un webhook de Paygumi. $rawBody es file_get_contents('php:\u002F\u002Finput').\n",[312,1997,1998],{"class":314,"line":391},[312,1999,1065],{"class":978},[312,2001,2002,2005,2007,2009,2012,2015,2017,2020,2022,2025,2027,2030,2033,2036,2038,2041,2044,2046],{"class":314,"line":404},[312,2003,2004],{"class":802},"function",[312,2006,1688],{"class":995},[312,2008,1079],{"class":318},[312,2010,2011],{"class":802},"string",[312,2013,2014],{"class":318}," $secret, ",[312,2016,2011],{"class":802},[312,2018,2019],{"class":318}," $timestamp, ",[312,2021,2011],{"class":802},[312,2023,2024],{"class":318}," $signatureHeader, ",[312,2026,2011],{"class":802},[312,2028,2029],{"class":318}," $rawBody, ",[312,2031,2032],{"class":802},"?int",[312,2034,2035],{"class":318}," $now ",[312,2037,1329],{"class":802},[312,2039,2040],{"class":325}," null",[312,2042,2043],{"class":318},")",[312,2045,824],{"class":802},[312,2047,2048],{"class":802}," bool\n",[312,2050,2051],{"class":314,"line":413},[312,2052,319],{"class":318},[312,2054,2055,2057,2059,2061,2064],{"class":314,"line":421},[312,2056,1353],{"class":802},[312,2058,1135],{"class":318},[312,2060,1138],{"class":802},[312,2062,2063],{"class":325},"ctype_digit",[312,2065,2066],{"class":318},"($timestamp)) {\n",[312,2068,2069,2071,2074],{"class":314,"line":434},[312,2070,1733],{"class":802},[312,2072,2073],{"class":325}," false",[312,2075,1982],{"class":318},[312,2077,2078],{"class":314,"line":447},[312,2079,688],{"class":318},[312,2081,2082,2085,2087,2089,2091,2094],{"class":314,"line":460},[312,2083,2084],{"class":318},"    $now ",[312,2086,1329],{"class":802},[312,2088,2035],{"class":318},[312,2090,1162],{"class":802},[312,2092,2093],{"class":325}," time",[312,2095,2096],{"class":318},"();\n",[312,2098,2099,2101,2103,2105,2108,2110,2112,2115,2118,2120,2122],{"class":314,"line":473},[312,2100,1353],{"class":802},[312,2102,1135],{"class":318},[312,2104,1184],{"class":325},[312,2106,2107],{"class":318},"($now ",[312,2109,1190],{"class":802},[312,2111,1135],{"class":318},[312,2113,2114],{"class":802},"int",[312,2116,2117],{"class":318},") $timestamp) ",[312,2119,1199],{"class":802},[312,2121,1974],{"class":325},[312,2123,2124],{"class":318},") {\n",[312,2126,2127,2129,2131],{"class":314,"line":504},[312,2128,1733],{"class":802},[312,2130,2073],{"class":325},[312,2132,1982],{"class":318},[312,2134,2135],{"class":314,"line":531},[312,2136,688],{"class":318},[312,2138,2139],{"class":314,"line":560},[312,2140,955],{"emptyLinePlaceholder":954},[312,2142,2143,2146,2148,2151,2153,2155,2158,2160,2163,2166],{"class":314,"line":566},[312,2144,2145],{"class":318},"    $expected ",[312,2147,1329],{"class":802},[312,2149,2150],{"class":325}," hash_hmac",[312,2152,1079],{"class":318},[312,2154,1233],{"class":332},[312,2156,2157],{"class":318},", $timestamp ",[312,2159,721],{"class":802},[312,2161,2162],{"class":332}," '.'",[312,2164,2165],{"class":802}," .",[312,2167,2168],{"class":318}," $rawBody, $secret);\n",[312,2170,2171],{"class":314,"line":574},[312,2172,955],{"emptyLinePlaceholder":954},[312,2174,2175,2178,2180,2183,2185,2187,2190,2193],{"class":314,"line":586},[312,2176,2177],{"class":802},"    foreach",[312,2179,1135],{"class":318},[312,2181,2182],{"class":325},"explode",[312,2184,1079],{"class":318},[312,2186,1288],{"class":332},[312,2188,2189],{"class":318},", $signatureHeader) ",[312,2191,2192],{"class":802},"as",[312,2194,2195],{"class":318}," $part) {\n",[312,2197,2198,2201,2203,2206,2208,2210,2212,2214,2217,2220],{"class":314,"line":598},[312,2199,2200],{"class":318},"        $pieces ",[312,2202,1329],{"class":802},[312,2204,2205],{"class":325}," explode",[312,2207,1079],{"class":318},[312,2209,1345],{"class":332},[312,2211,490],{"class":318},[312,2213,1335],{"class":325},[312,2215,2216],{"class":318},"($part), ",[312,2218,2219],{"class":325},"2",[312,2221,2222],{"class":318},");\n",[312,2224,2225,2227,2229,2232,2235,2238,2241,2244,2247,2250,2252,2254,2256,2258,2261,2264,2266],{"class":314,"line":611},[312,2226,1905],{"class":802},[312,2228,1135],{"class":318},[312,2230,2231],{"class":325},"count",[312,2233,2234],{"class":318},"($pieces) ",[312,2236,2237],{"class":802},"===",[312,2239,2240],{"class":325}," 2",[312,2242,2243],{"class":802}," &&",[312,2245,2246],{"class":318}," $pieces[",[312,2248,2249],{"class":325},"0",[312,2251,1326],{"class":318},[312,2253,2237],{"class":802},[312,2255,1362],{"class":332},[312,2257,2243],{"class":802},[312,2259,2260],{"class":325}," hash_equals",[312,2262,2263],{"class":318},"($expected, $pieces[",[312,2265,656],{"class":325},[312,2267,2268],{"class":318},"])) {\n",[312,2270,2271,2273,2276],{"class":314,"line":624},[312,2272,1925],{"class":802},[312,2274,2275],{"class":325}," true",[312,2277,1982],{"class":318},[312,2279,2280],{"class":314,"line":648},[312,2281,2282],{"class":318},"        }\n",[312,2284,2285],{"class":314,"line":661},[312,2286,688],{"class":318},[312,2288,2289,2291,2293],{"class":314,"line":674},[312,2290,1402],{"class":802},[312,2292,2073],{"class":325},[312,2294,1982],{"class":318},[312,2296,2297],{"class":314,"line":685},[312,2298,700],{"class":318},[10,2300,2301,2302,757,2305,721],{},"Las cabeceras llegan en ",[27,2303,2304],{},"$_SERVER['HTTP_PAYGUMI_TIMESTAMP']",[27,2306,2307],{},"$_SERVER['HTTP_PAYGUMI_SIGNATURE']",[924,2309,2311],{"id":2310},"go","Go",[303,2313,2317],{"className":2314,"code":2315,"filename":2316,"language":2310,"meta":308,"style":308},"language-go shiki shiki-themes github-light github-dark","package paygumi\n\nimport (\n    \"crypto\u002Fhmac\"\n    \"crypto\u002Fsha256\"\n    \"encoding\u002Fhex\"\n    \"strconv\"\n    \"strings\"\n    \"time\"\n)\n\nconst signatureTolerance = 5 * time.Minute\n\n\u002F\u002F VerifySignature verifica un webhook de Paygumi. body son los bytes crudos del cuerpo.\nfunc VerifySignature(secret, timestamp, signatureHeader string, body []byte, now time.Time) bool {\n    ts, err := strconv.ParseInt(timestamp, 10, 64)\n    if err != nil || ts \u003C 0 {\n        return false\n    }\n    if diff := now.Sub(time.Unix(ts, 0)); diff > signatureTolerance || diff \u003C -signatureTolerance {\n        return false\n    }\n\n    mac := hmac.New(sha256.New, []byte(secret))\n    mac.Write([]byte(timestamp + \".\"))\n    mac.Write(body)\n    expected := mac.Sum(nil)\n\n    for _, part := range strings.Split(signatureHeader, \",\") {\n        scheme, sig, ok := strings.Cut(strings.TrimSpace(part), \"=\")\n        if !ok || scheme != \"v1\" {\n            continue\n        }\n        got, err := hex.DecodeString(sig)\n        if err == nil && hmac.Equal(got, expected) {\n            return true\n        }\n    }\n    return false\n}\n","verify.go",[27,2318,2319,2327,2331,2338,2349,2358,2367,2376,2385,2394,2398,2402,2420,2424,2429,2481,2508,2534,2540,2544,2591,2597,2601,2605,2626,2648,2657,2676,2680,2705,2731,2751,2757,2762,2779,2800,2808,2813,2818,2825],{"__ignoreMap":308},[312,2320,2321,2324],{"class":314,"line":315},[312,2322,2323],{"class":802},"package",[312,2325,2326],{"class":995}," paygumi\n",[312,2328,2329],{"class":314,"line":322},[312,2330,955],{"emptyLinePlaceholder":954},[312,2332,2333,2335],{"class":314,"line":339},[312,2334,940],{"class":802},[312,2336,2337],{"class":318}," (\n",[312,2339,2340,2343,2346],{"class":314,"line":352},[312,2341,2342],{"class":332},"    \"",[312,2344,2345],{"class":995},"crypto\u002Fhmac",[312,2347,2348],{"class":332},"\"\n",[312,2350,2351,2353,2356],{"class":314,"line":365},[312,2352,2342],{"class":332},[312,2354,2355],{"class":995},"crypto\u002Fsha256",[312,2357,2348],{"class":332},[312,2359,2360,2362,2365],{"class":314,"line":378},[312,2361,2342],{"class":332},[312,2363,2364],{"class":995},"encoding\u002Fhex",[312,2366,2348],{"class":332},[312,2368,2369,2371,2374],{"class":314,"line":391},[312,2370,2342],{"class":332},[312,2372,2373],{"class":995},"strconv",[312,2375,2348],{"class":332},[312,2377,2378,2380,2383],{"class":314,"line":404},[312,2379,2342],{"class":332},[312,2381,2382],{"class":995},"strings",[312,2384,2348],{"class":332},[312,2386,2387,2389,2392],{"class":314,"line":413},[312,2388,2342],{"class":332},[312,2390,2391],{"class":995},"time",[312,2393,2348],{"class":332},[312,2395,2396],{"class":314,"line":421},[312,2397,1348],{"class":318},[312,2399,2400],{"class":314,"line":434},[312,2401,955],{"emptyLinePlaceholder":954},[312,2403,2404,2406,2409,2411,2414,2417],{"class":314,"line":447},[312,2405,960],{"class":802},[312,2407,2408],{"class":325}," signatureTolerance",[312,2410,966],{"class":802},[312,2412,2413],{"class":325}," 5",[312,2415,2416],{"class":802}," *",[312,2418,2419],{"class":318}," time.Minute\n",[312,2421,2422],{"class":314,"line":460},[312,2423,955],{"emptyLinePlaceholder":954},[312,2425,2426],{"class":314,"line":473},[312,2427,2428],{"class":978},"\u002F\u002F VerifySignature verifica un webhook de Paygumi. body son los bytes crudos del cuerpo.\n",[312,2430,2431,2434,2437,2439,2441,2443,2445,2447,2449,2452,2454,2457,2460,2463,2465,2467,2469,2471,2474,2476,2479],{"class":314,"line":504},[312,2432,2433],{"class":802},"func",[312,2435,2436],{"class":995}," VerifySignature",[312,2438,1079],{"class":318},[312,2440,1083],{"class":1082},[312,2442,490],{"class":318},[312,2444,1088],{"class":1082},[312,2446,490],{"class":318},[312,2448,1093],{"class":1082},[312,2450,2451],{"class":802}," string",[312,2453,490],{"class":318},[312,2455,2456],{"class":1082},"body",[312,2458,2459],{"class":318}," []",[312,2461,2462],{"class":802},"byte",[312,2464,490],{"class":318},[312,2466,1103],{"class":1082},[312,2468,2093],{"class":995},[312,2470,721],{"class":318},[312,2472,2473],{"class":995},"Time",[312,2475,1204],{"class":318},[312,2477,2478],{"class":802},"bool",[312,2480,1307],{"class":318},[312,2482,2483,2486,2489,2492,2495,2498,2501,2503,2506],{"class":314,"line":531},[312,2484,2485],{"class":318},"    ts, err ",[312,2487,2488],{"class":802},":=",[312,2490,2491],{"class":318}," strconv.",[312,2493,2494],{"class":995},"ParseInt",[312,2496,2497],{"class":318},"(timestamp, ",[312,2499,2500],{"class":325},"10",[312,2502,490],{"class":318},[312,2504,2505],{"class":325},"64",[312,2507,1348],{"class":318},[312,2509,2510,2512,2515,2518,2521,2523,2526,2529,2532],{"class":314,"line":560},[312,2511,1353],{"class":802},[312,2513,2514],{"class":318}," err ",[312,2516,2517],{"class":802},"!=",[312,2519,2520],{"class":325}," nil",[312,2522,1365],{"class":802},[312,2524,2525],{"class":318}," ts ",[312,2527,2528],{"class":802},"\u003C",[312,2530,2531],{"class":325}," 0",[312,2533,1307],{"class":318},[312,2535,2536,2538],{"class":314,"line":566},[312,2537,1733],{"class":802},[312,2539,1174],{"class":325},[312,2541,2542],{"class":314,"line":574},[312,2543,688],{"class":318},[312,2545,2546,2548,2551,2553,2556,2559,2562,2565,2568,2570,2573,2575,2578,2581,2583,2585,2588],{"class":314,"line":586},[312,2547,1353],{"class":802},[312,2549,2550],{"class":318}," diff ",[312,2552,2488],{"class":802},[312,2554,2555],{"class":318}," now.",[312,2557,2558],{"class":995},"Sub",[312,2560,2561],{"class":318},"(time.",[312,2563,2564],{"class":995},"Unix",[312,2566,2567],{"class":318},"(ts, ",[312,2569,2249],{"class":325},[312,2571,2572],{"class":318},")); diff ",[312,2574,1199],{"class":802},[312,2576,2577],{"class":318}," signatureTolerance ",[312,2579,2580],{"class":802},"||",[312,2582,2550],{"class":318},[312,2584,2528],{"class":802},[312,2586,2587],{"class":802}," -",[312,2589,2590],{"class":318},"signatureTolerance {\n",[312,2592,2593,2595],{"class":314,"line":598},[312,2594,1733],{"class":802},[312,2596,1174],{"class":325},[312,2598,2599],{"class":314,"line":611},[312,2600,688],{"class":318},[312,2602,2603],{"class":314,"line":624},[312,2604,955],{"emptyLinePlaceholder":954},[312,2606,2607,2610,2612,2615,2618,2621,2623],{"class":314,"line":648},[312,2608,2609],{"class":318},"    mac ",[312,2611,2488],{"class":802},[312,2613,2614],{"class":318}," hmac.",[312,2616,2617],{"class":995},"New",[312,2619,2620],{"class":318},"(sha256.New, []",[312,2622,2462],{"class":802},[312,2624,2625],{"class":318},"(secret))\n",[312,2627,2628,2631,2634,2637,2639,2641,2643,2646],{"class":314,"line":661},[312,2629,2630],{"class":318},"    mac.",[312,2632,2633],{"class":995},"Write",[312,2635,2636],{"class":318},"([]",[312,2638,2462],{"class":802},[312,2640,1159],{"class":318},[312,2642,1832],{"class":802},[312,2644,2645],{"class":332}," \".\"",[312,2647,1603],{"class":318},[312,2649,2650,2652,2654],{"class":314,"line":674},[312,2651,2630],{"class":318},[312,2653,2633],{"class":995},[312,2655,2656],{"class":318},"(body)\n",[312,2658,2659,2661,2663,2666,2669,2671,2674],{"class":314,"line":685},[312,2660,1804],{"class":318},[312,2662,2488],{"class":802},[312,2664,2665],{"class":318}," mac.",[312,2667,2668],{"class":995},"Sum",[312,2670,1079],{"class":318},[312,2672,2673],{"class":325},"nil",[312,2675,1348],{"class":318},[312,2677,2678],{"class":314,"line":691},[312,2679,955],{"emptyLinePlaceholder":954},[312,2681,2682,2684,2687,2689,2692,2695,2698,2701,2703],{"class":314,"line":697},[312,2683,1863],{"class":802},[312,2685,2686],{"class":318}," _, part ",[312,2688,2488],{"class":802},[312,2690,2691],{"class":802}," range",[312,2693,2694],{"class":318}," strings.",[312,2696,2697],{"class":995},"Split",[312,2699,2700],{"class":318},"(signatureHeader, ",[312,2702,1883],{"class":332},[312,2704,2124],{"class":318},[312,2706,2708,2711,2713,2715,2718,2721,2724,2727,2729],{"class":314,"line":2707},30,[312,2709,2710],{"class":318},"        scheme, sig, ok ",[312,2712,2488],{"class":802},[312,2714,2694],{"class":318},[312,2716,2717],{"class":995},"Cut",[312,2719,2720],{"class":318},"(strings.",[312,2722,2723],{"class":995},"TrimSpace",[312,2725,2726],{"class":318},"(part), ",[312,2728,1898],{"class":332},[312,2730,1348],{"class":318},[312,2732,2734,2736,2738,2741,2743,2745,2747,2749],{"class":314,"line":2733},31,[312,2735,1905],{"class":802},[312,2737,1368],{"class":802},[312,2739,2740],{"class":318},"ok ",[312,2742,2580],{"class":802},[312,2744,1908],{"class":318},[312,2746,2517],{"class":802},[312,2748,1914],{"class":332},[312,2750,1307],{"class":318},[312,2752,2754],{"class":314,"line":2753},32,[312,2755,2756],{"class":802},"            continue\n",[312,2758,2760],{"class":314,"line":2759},33,[312,2761,2282],{"class":318},[312,2763,2765,2768,2770,2773,2776],{"class":314,"line":2764},34,[312,2766,2767],{"class":318},"        got, err ",[312,2769,2488],{"class":802},[312,2771,2772],{"class":318}," hex.",[312,2774,2775],{"class":995},"DecodeString",[312,2777,2778],{"class":318},"(sig)\n",[312,2780,2782,2784,2786,2788,2790,2792,2794,2797],{"class":314,"line":2781},35,[312,2783,1905],{"class":802},[312,2785,2514],{"class":318},[312,2787,1911],{"class":802},[312,2789,2520],{"class":325},[312,2791,2243],{"class":802},[312,2793,2614],{"class":318},[312,2795,2796],{"class":995},"Equal",[312,2798,2799],{"class":318},"(got, expected) {\n",[312,2801,2803,2805],{"class":314,"line":2802},36,[312,2804,1925],{"class":802},[312,2806,2807],{"class":325}," true\n",[312,2809,2811],{"class":314,"line":2810},37,[312,2812,2282],{"class":318},[312,2814,2816],{"class":314,"line":2815},38,[312,2817,688],{"class":318},[312,2819,2821,2823],{"class":314,"line":2820},39,[312,2822,1402],{"class":802},[312,2824,1174],{"class":325},[312,2826,2828],{"class":314,"line":2827},40,[312,2829,700],{"class":318},[10,2831,2832,2833,2836,2837,2840,2841,721],{},"En un ",[27,2834,2835],{},"http.Handler",", lee el cuerpo con ",[27,2838,2839],{},"io.ReadAll(r.Body)"," antes de decodificar el JSON y pasa ",[27,2842,2843],{},"time.Now()",[924,2845,2847],{"id":2846},"vector-de-prueba","Vector de prueba",[10,2849,2850],{},"Usa estos valores para probar tu implementación (el secreto es de ejemplo, no es de ninguna cuenta):",[52,2852,2853,2863],{},[55,2854,2855],{},[58,2856,2857,2860],{},[61,2858,2859],{},"Dato",[61,2861,2862],{},"Valor",[68,2864,2865,2875,2886,2896],{},[58,2866,2867,2870],{},[73,2868,2869],{},"Secreto",[73,2871,2872],{},[27,2873,2874],{},"whsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",[58,2876,2877,2881],{},[73,2878,2879],{},[27,2880,842],{},[73,2882,2883],{},[27,2884,2885],{},"1793509212",[58,2887,2888,2891],{},[73,2889,2890],{},"Cuerpo",[73,2892,2893],{},[27,2894,2895],{},"{\"id\":\"evt_1\",\"type\":\"ping\"}",[58,2897,2898,2903],{},[73,2899,2900,2902],{},[27,2901,852],{}," esperada",[73,2904,2905],{},[27,2906,2907],{},"v1=a6e678f685978f4335967817ea566984611995345a4b65a347ccd2bd51afe0ed",[10,2909,2910,2911,2914],{},"Con esos datos (y tu reloj fijado cerca del timestamp), tu función debe aceptar también la cabecera de rotación ",[27,2912,2913],{},"v1=ea3e29664abd3e52830afcc08e7d6caf9fc0162288737032eb959e5d3bfb443c,v1=a6e678f685978f4335967817ea566984611995345a4b65a347ccd2bd51afe0ed",", y rechazar el evento si cambia un solo byte del cuerpo o si el timestamp tiene más de 5 minutos.",[14,2916,2918],{"id":2917},"entrega-y-reintentos","Entrega y reintentos",[702,2920,2921,2931,2938,2941],{},[705,2922,2923,2924,2930],{},"Responde con cualquier ",[22,2925,2926,2929],{},[27,2927,2928],{},"2xx"," en menos de 10 segundos",". Guarda el evento (por ejemplo en una cola) y procésalo después: no hagas trabajo lento antes de responder.",[705,2932,2933,2934,2937],{},"Si respondes otro código, se vence el tiempo o falla la conexión, Paygumi reintenta a los ",[22,2935,2936],{},"1 min, 5 min, 30 min, 2 h, 6 h, 12 h y 24 h"," (≈ 45 h en total). Después la entrega queda agotada; puedes reenviarla desde el panel.",[705,2939,2940],{},"No se siguen redirects: registra la URL final.",[705,2942,2943],{},"Si un endpoint falla 100 veces seguidas durante 3 días, se deshabilita y se avisa por correo a los propietarios y administradores de tu cuenta.",[14,2945,2947],{"id":2946},"duplicados-y-orden","Duplicados y orden",[702,2949,2950,2963,2973],{},[705,2951,2952,2955,2956,2958,2959,2962],{},[22,2953,2954],{},"La entrega es al menos una vez",": el mismo evento puede llegar más de una vez (reintentos o reenvíos). Deduplica por ",[27,2957,832],{}," (igual a ",[27,2960,2961],{},"id","), que no cambia entre reintentos.",[705,2964,2965,2968,2969,2972],{},[22,2966,2967],{},"No hay orden garantizado",": un evento viejo puede llegar después de uno nuevo. Como cada evento trae el estado completo y ",[27,2970,2971],{},"created_at",", descarta los que sean más viejos que el último que aplicaste para esa suscripción.",[705,2974,2975,2976,721],{},"Si te perdiste eventos, reconcilia con ",[745,2977,2979],{"href":2978},"\u002Fdocs\u002Fsuscripciones-y-acceso#consultar-el-acceso",[27,2980,2981],{},"GET \u002Fv1\u002Fsubscriptions\u002F{id}\u002Faccess",[2983,2984,2985],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":308,"searchDepth":339,"depth":339,"links":2987},[2988,2989,2990,2991,2992,2999,3000],{"id":16,"depth":322,"text":17},{"id":49,"depth":322,"text":50},{"id":289,"depth":322,"text":290},{"id":789,"depth":322,"text":790},{"id":860,"depth":322,"text":861,"children":2993},[2994,2995,2996,2997,2998],{"id":926,"depth":339,"text":927},{"id":1630,"depth":339,"text":1631},{"id":1947,"depth":339,"text":1948},{"id":2310,"depth":339,"text":2311},{"id":2846,"depth":339,"text":2847},{"id":2917,"depth":322,"text":2918},{"id":2946,"depth":322,"text":2947},"Recibe en tu backend los eventos de pagos y suscripciones, verifica su firma HMAC-SHA256 y procésalos sin duplicados.","md",{},"\u002Fdocs\u002Fwebhooks",{"title":5,"description":3001},"docs\u002F4.webhooks","mtwamg-n_irYj6MCrpq0poSUS1xFu6zYuZ3Y4XWSEAA",[3009,3012],{"title":3010,"path":778,"stem":3011,"children":-1},"Suscripciones y estados de acceso","docs\u002F3.suscripciones-y-acceso",{"title":3013,"path":747,"stem":3014,"children":-1},"Portal del cliente","docs\u002F5.portal-del-cliente",[3016,3019,3022,3025,3026,3027,3028],{"title":3017,"path":3018},"Introducción","\u002Fdocs",{"title":3020,"path":3021},"Primeros pasos","\u002Fdocs\u002Fprimeros-pasos",{"title":3023,"path":3024},"Crear una sesión de checkout","\u002Fdocs\u002Fsesiones-de-checkout",{"title":3010,"path":778},{"title":5,"path":3004},{"title":3013,"path":747},{"title":3029,"path":3030},"Idempotencia y errores","\u002Fdocs\u002Fidempotencia-y-errores",1794279319988]