[{"data":1,"prerenderedAt":1729},["ShallowReactive",2],{"api-webhooks":3},{"article":4,"sections":1694,"prevSection":1725,"nextSection":7},{"id":5,"title":6,"avatar":7,"body":8,"description":1687,"docx":7,"extension":1688,"meta":1689,"navigation":930,"order":253,"path":1690,"pdf":7,"pdfLabel":7,"procurementPack":7,"seo":1691,"slug":14,"stem":1692,"__hash__":1693},"content\u002Fapi\u002Fwebhooks.md","Webhooks",null,{"type":9,"value":10,"toc":1661},"minimark",[11,15,19,24,33,37,108,112,115,326,331,490,494,497,500,571,574,577,645,651,659,662,665,743,746,749,807,810,813,893,897,900,904,910,914,1048,1052,1080,1084,1088,1091,1179,1183,1186,1251,1255,1258,1309,1313,1627,1631,1634,1654,1657],[12,13,6],"h1",{"id":14},"webhooks",[16,17,18],"p",{},"Webhooks allow you to receive real-time HTTP notifications when events occur in your organization. This is more efficient than polling the API for status updates.",[20,21,23],"h2",{"id":22},"configuration","Configuration",[16,25,26,27,32],{},"Configure your webhook URL in your ",[28,29,31],"a",{"href":30},"\u002Fapp\u002Fadmin\u002Fapi","organization settings",". You'll receive a webhook secret for verifying request authenticity.",[20,34,36],{"id":35},"webhook-events","Webhook Events",[38,39,40,53],"table",{},[41,42,43],"thead",{},[44,45,46,50],"tr",{},[47,48,49],"th",{},"Event",[47,51,52],{},"Description",[54,55,56,68,78,88,98],"tbody",{},[44,57,58,65],{},[59,60,61],"td",{},[62,63,64],"code",{},"model.created",[59,66,67],{},"A new model was created",[44,69,70,75],{},[59,71,72],{},[62,73,74],{},"model.status.updated",[59,76,77],{},"Model status changed (e.g., pending → active)",[44,79,80,85],{},[59,81,82],{},[62,83,84],{},"model.photos_ready",[59,86,87],{},"Headshots have been generated and are ready",[44,89,90,95],{},[59,91,92],{},[62,93,94],{},"model.favorite_selected",[59,96,97],{},"User selected their favorite photo",[44,99,100,105],{},[59,101,102],{},[62,103,104],{},"model.deleted",[59,106,107],{},"Model was deleted",[20,109,111],{"id":110},"payload-structure","Payload Structure",[16,113,114],{},"All webhooks follow the same payload structure:",[116,117,122],"pre",{"className":118,"code":119,"language":120,"meta":121,"style":121},"language-json shiki shiki-themes dark-plus","{\n  \"event\": \"model.status.updated\",\n  \"objectType\": \"model\",\n  \"object\": {\n    \"_id\": \"507f1f77bcf86cd799439011\",\n    \"uid\": \"anon_abc123\",\n    \"title\": \"John Doe\",\n    \"trigger\": \"male\",\n    \"status\": \"active\",\n    \"isDevelopment\": false,\n    \"finishedAt\": \"2024-01-15T11:45:00.000Z\",\n    \"deletedAt\": null,\n    \"inviteExpiresAt\": \"2024-02-14T10:30:00.000Z\",\n    \"createdAt\": \"2024-01-15T10:30:00.000Z\",\n    \"organization\": \"org_abc123def456\"\n  }\n}\n","json","",[62,123,124,133,150,163,172,185,198,211,224,237,251,264,277,290,303,314,320],{"__ignoreMap":121},[125,126,129],"span",{"class":127,"line":128},"line",1,[125,130,132],{"class":131},"sTNBD","{\n",[125,134,136,140,143,147],{"class":127,"line":135},2,[125,137,139],{"class":138},"s3F5K","  \"event\"",[125,141,142],{"class":131},": ",[125,144,146],{"class":145},"sKc5r","\"model.status.updated\"",[125,148,149],{"class":131},",\n",[125,151,153,156,158,161],{"class":127,"line":152},3,[125,154,155],{"class":138},"  \"objectType\"",[125,157,142],{"class":131},[125,159,160],{"class":145},"\"model\"",[125,162,149],{"class":131},[125,164,166,169],{"class":127,"line":165},4,[125,167,168],{"class":138},"  \"object\"",[125,170,171],{"class":131},": {\n",[125,173,175,178,180,183],{"class":127,"line":174},5,[125,176,177],{"class":138},"    \"_id\"",[125,179,142],{"class":131},[125,181,182],{"class":145},"\"507f1f77bcf86cd799439011\"",[125,184,149],{"class":131},[125,186,188,191,193,196],{"class":127,"line":187},6,[125,189,190],{"class":138},"    \"uid\"",[125,192,142],{"class":131},[125,194,195],{"class":145},"\"anon_abc123\"",[125,197,149],{"class":131},[125,199,201,204,206,209],{"class":127,"line":200},7,[125,202,203],{"class":138},"    \"title\"",[125,205,142],{"class":131},[125,207,208],{"class":145},"\"John Doe\"",[125,210,149],{"class":131},[125,212,214,217,219,222],{"class":127,"line":213},8,[125,215,216],{"class":138},"    \"trigger\"",[125,218,142],{"class":131},[125,220,221],{"class":145},"\"male\"",[125,223,149],{"class":131},[125,225,227,230,232,235],{"class":127,"line":226},9,[125,228,229],{"class":138},"    \"status\"",[125,231,142],{"class":131},[125,233,234],{"class":145},"\"active\"",[125,236,149],{"class":131},[125,238,240,243,245,249],{"class":127,"line":239},10,[125,241,242],{"class":138},"    \"isDevelopment\"",[125,244,142],{"class":131},[125,246,248],{"class":247},"scz_3","false",[125,250,149],{"class":131},[125,252,254,257,259,262],{"class":127,"line":253},11,[125,255,256],{"class":138},"    \"finishedAt\"",[125,258,142],{"class":131},[125,260,261],{"class":145},"\"2024-01-15T11:45:00.000Z\"",[125,263,149],{"class":131},[125,265,267,270,272,275],{"class":127,"line":266},12,[125,268,269],{"class":138},"    \"deletedAt\"",[125,271,142],{"class":131},[125,273,274],{"class":247},"null",[125,276,149],{"class":131},[125,278,280,283,285,288],{"class":127,"line":279},13,[125,281,282],{"class":138},"    \"inviteExpiresAt\"",[125,284,142],{"class":131},[125,286,287],{"class":145},"\"2024-02-14T10:30:00.000Z\"",[125,289,149],{"class":131},[125,291,293,296,298,301],{"class":127,"line":292},14,[125,294,295],{"class":138},"    \"createdAt\"",[125,297,142],{"class":131},[125,299,300],{"class":145},"\"2024-01-15T10:30:00.000Z\"",[125,302,149],{"class":131},[125,304,306,309,311],{"class":127,"line":305},15,[125,307,308],{"class":138},"    \"organization\"",[125,310,142],{"class":131},[125,312,313],{"class":145},"\"org_abc123def456\"\n",[125,315,317],{"class":127,"line":316},16,[125,318,319],{"class":131},"  }\n",[125,321,323],{"class":127,"line":322},17,[125,324,325],{"class":131},"}\n",[327,328,330],"h3",{"id":329},"object-fields","Object Fields",[38,332,333,345],{},[41,334,335],{},[44,336,337,340,343],{},[47,338,339],{},"Field",[47,341,342],{},"Type",[47,344,52],{},[54,346,347,360,372,384,404,416,429,442,454,466,478],{},[44,348,349,354,357],{},[59,350,351],{},[62,352,353],{},"_id",[59,355,356],{},"string",[59,358,359],{},"Model ID",[44,361,362,367,369],{},[59,363,364],{},[62,365,366],{},"uid",[59,368,356],{},[59,370,371],{},"User identifier",[44,373,374,379,381],{},[59,375,376],{},[62,377,378],{},"title",[59,380,356],{},[59,382,383],{},"Display name",[44,385,386,391,393],{},[59,387,388],{},[62,389,390],{},"trigger",[59,392,356],{},[59,394,395,396,399,400,403],{},"Selected gender (",[62,397,398],{},"male",", ",[62,401,402],{},"female",")",[44,405,406,411,413],{},[59,407,408],{},[62,409,410],{},"status",[59,412,356],{},[59,414,415],{},"Current model status",[44,417,418,423,426],{},[59,419,420],{},[62,421,422],{},"isDevelopment",[59,424,425],{},"boolean",[59,427,428],{},"Whether in test mode",[44,430,431,436,439],{},[59,432,433],{},[62,434,435],{},"finishedAt",[59,437,438],{},"datetime",[59,440,441],{},"When processing completed",[44,443,444,449,451],{},[59,445,446],{},[62,447,448],{},"deletedAt",[59,450,438],{},[59,452,453],{},"When model was deleted",[44,455,456,461,463],{},[59,457,458],{},[62,459,460],{},"inviteExpiresAt",[59,462,438],{},[59,464,465],{},"When invite expires",[44,467,468,473,475],{},[59,469,470],{},[62,471,472],{},"createdAt",[59,474,438],{},[59,476,477],{},"When model was created",[44,479,480,485,487],{},[59,481,482],{},[62,483,484],{},"organization",[59,486,356],{},[59,488,489],{},"Organization ID",[20,491,493],{"id":492},"event-details","Event Details",[327,495,64],{"id":496},"modelcreated",[16,498,499],{},"Triggered when a new model is created via API or invite signup.",[116,501,503],{"className":118,"code":502,"language":120,"meta":121,"style":121},"{\n  \"event\": \"model.created\",\n  \"objectType\": \"model\",\n  \"object\": {\n    \"_id\": \"507f1f77bcf86cd799439011\",\n    \"status\": \"onboarding\",\n    ...\n  }\n}\n",[62,504,505,509,520,530,536,546,557,563,567],{"__ignoreMap":121},[125,506,507],{"class":127,"line":128},[125,508,132],{"class":131},[125,510,511,513,515,518],{"class":127,"line":135},[125,512,139],{"class":138},[125,514,142],{"class":131},[125,516,517],{"class":145},"\"model.created\"",[125,519,149],{"class":131},[125,521,522,524,526,528],{"class":127,"line":152},[125,523,155],{"class":138},[125,525,142],{"class":131},[125,527,160],{"class":145},[125,529,149],{"class":131},[125,531,532,534],{"class":127,"line":165},[125,533,168],{"class":138},[125,535,171],{"class":131},[125,537,538,540,542,544],{"class":127,"line":174},[125,539,177],{"class":138},[125,541,142],{"class":131},[125,543,182],{"class":145},[125,545,149],{"class":131},[125,547,548,550,552,555],{"class":127,"line":187},[125,549,229],{"class":138},[125,551,142],{"class":131},[125,553,554],{"class":145},"\"onboarding\"",[125,556,149],{"class":131},[125,558,559],{"class":127,"line":200},[125,560,562],{"class":561},"sHkYM","    ...\n",[125,564,565],{"class":127,"line":213},[125,566,319],{"class":131},[125,568,569],{"class":127,"line":226},[125,570,325],{"class":131},[327,572,74],{"id":573},"modelstatusupdated",[16,575,576],{},"Triggered when a model's status changes. Use this to track progress through the pipeline.",[116,578,580],{"className":118,"code":579,"language":120,"meta":121,"style":121},"{\n  \"event\": \"model.status.updated\",\n  \"objectType\": \"model\",\n  \"object\": {\n    \"_id\": \"507f1f77bcf86cd799439011\",\n    \"status\": \"generatingHeadshots\",\n    ...\n  }\n}\n",[62,581,582,586,596,606,612,622,633,637,641],{"__ignoreMap":121},[125,583,584],{"class":127,"line":128},[125,585,132],{"class":131},[125,587,588,590,592,594],{"class":127,"line":135},[125,589,139],{"class":138},[125,591,142],{"class":131},[125,593,146],{"class":145},[125,595,149],{"class":131},[125,597,598,600,602,604],{"class":127,"line":152},[125,599,155],{"class":138},[125,601,142],{"class":131},[125,603,160],{"class":145},[125,605,149],{"class":131},[125,607,608,610],{"class":127,"line":165},[125,609,168],{"class":138},[125,611,171],{"class":131},[125,613,614,616,618,620],{"class":127,"line":174},[125,615,177],{"class":138},[125,617,142],{"class":131},[125,619,182],{"class":145},[125,621,149],{"class":131},[125,623,624,626,628,631],{"class":127,"line":187},[125,625,229],{"class":138},[125,627,142],{"class":131},[125,629,630],{"class":145},"\"generatingHeadshots\"",[125,632,149],{"class":131},[125,634,635],{"class":127,"line":200},[125,636,562],{"class":561},[125,638,639],{"class":127,"line":213},[125,640,319],{"class":131},[125,642,643],{"class":127,"line":226},[125,644,325],{"class":131},[16,646,647],{},[648,649,650],"strong",{},"Status Flow:",[116,652,657],{"className":653,"code":655,"language":656},[654],"language-text","onboarding → waiting → pending → generatingHeadshots → active\n","text",[62,658,655],{"__ignoreMap":121},[327,660,84],{"id":661},"modelphotos_ready",[16,663,664],{},"Triggered when headshots are generated and available for download. This is typically the event you want to listen for to fetch the final photos.",[116,666,668],{"className":118,"code":667,"language":120,"meta":121,"style":121},"{\n  \"event\": \"model.photos_ready\",\n  \"objectType\": \"model\",\n  \"object\": {\n    \"_id\": \"507f1f77bcf86cd799439011\",\n    \"status\": \"active\",\n    \"finishedAt\": \"2024-01-15T11:45:00.000Z\",\n    ...\n  }\n}\n",[62,669,670,674,685,695,701,711,721,731,735,739],{"__ignoreMap":121},[125,671,672],{"class":127,"line":128},[125,673,132],{"class":131},[125,675,676,678,680,683],{"class":127,"line":135},[125,677,139],{"class":138},[125,679,142],{"class":131},[125,681,682],{"class":145},"\"model.photos_ready\"",[125,684,149],{"class":131},[125,686,687,689,691,693],{"class":127,"line":152},[125,688,155],{"class":138},[125,690,142],{"class":131},[125,692,160],{"class":145},[125,694,149],{"class":131},[125,696,697,699],{"class":127,"line":165},[125,698,168],{"class":138},[125,700,171],{"class":131},[125,702,703,705,707,709],{"class":127,"line":174},[125,704,177],{"class":138},[125,706,142],{"class":131},[125,708,182],{"class":145},[125,710,149],{"class":131},[125,712,713,715,717,719],{"class":127,"line":187},[125,714,229],{"class":138},[125,716,142],{"class":131},[125,718,234],{"class":145},[125,720,149],{"class":131},[125,722,723,725,727,729],{"class":127,"line":200},[125,724,256],{"class":138},[125,726,142],{"class":131},[125,728,261],{"class":145},[125,730,149],{"class":131},[125,732,733],{"class":127,"line":213},[125,734,562],{"class":561},[125,736,737],{"class":127,"line":226},[125,738,319],{"class":131},[125,740,741],{"class":127,"line":239},[125,742,325],{"class":131},[327,744,94],{"id":745},"modelfavorite_selected",[16,747,748],{},"Triggered when a user selects their favorite photo.",[116,750,752],{"className":118,"code":751,"language":120,"meta":121,"style":121},"{\n  \"event\": \"model.favorite_selected\",\n  \"objectType\": \"model\",\n  \"object\": {\n    \"_id\": \"507f1f77bcf86cd799439011\",\n    ...\n  }\n}\n",[62,753,754,758,769,779,785,795,799,803],{"__ignoreMap":121},[125,755,756],{"class":127,"line":128},[125,757,132],{"class":131},[125,759,760,762,764,767],{"class":127,"line":135},[125,761,139],{"class":138},[125,763,142],{"class":131},[125,765,766],{"class":145},"\"model.favorite_selected\"",[125,768,149],{"class":131},[125,770,771,773,775,777],{"class":127,"line":152},[125,772,155],{"class":138},[125,774,142],{"class":131},[125,776,160],{"class":145},[125,778,149],{"class":131},[125,780,781,783],{"class":127,"line":165},[125,782,168],{"class":138},[125,784,171],{"class":131},[125,786,787,789,791,793],{"class":127,"line":174},[125,788,177],{"class":138},[125,790,142],{"class":131},[125,792,182],{"class":145},[125,794,149],{"class":131},[125,796,797],{"class":127,"line":187},[125,798,562],{"class":561},[125,800,801],{"class":127,"line":200},[125,802,319],{"class":131},[125,804,805],{"class":127,"line":213},[125,806,325],{"class":131},[327,808,104],{"id":809},"modeldeleted",[16,811,812],{},"Triggered when a model is deleted via API or admin action.",[116,814,816],{"className":118,"code":815,"language":120,"meta":121,"style":121},"{\n  \"event\": \"model.deleted\",\n  \"objectType\": \"model\",\n  \"object\": {\n    \"_id\": \"507f1f77bcf86cd799439011\",\n    \"status\": \"deleted\",\n    \"deletedAt\": \"2024-01-16T09:00:00.000Z\",\n    ...\n  }\n}\n",[62,817,818,822,833,843,849,859,870,881,885,889],{"__ignoreMap":121},[125,819,820],{"class":127,"line":128},[125,821,132],{"class":131},[125,823,824,826,828,831],{"class":127,"line":135},[125,825,139],{"class":138},[125,827,142],{"class":131},[125,829,830],{"class":145},"\"model.deleted\"",[125,832,149],{"class":131},[125,834,835,837,839,841],{"class":127,"line":152},[125,836,155],{"class":138},[125,838,142],{"class":131},[125,840,160],{"class":145},[125,842,149],{"class":131},[125,844,845,847],{"class":127,"line":165},[125,846,168],{"class":138},[125,848,171],{"class":131},[125,850,851,853,855,857],{"class":127,"line":174},[125,852,177],{"class":138},[125,854,142],{"class":131},[125,856,182],{"class":145},[125,858,149],{"class":131},[125,860,861,863,865,868],{"class":127,"line":187},[125,862,229],{"class":138},[125,864,142],{"class":131},[125,866,867],{"class":145},"\"deleted\"",[125,869,149],{"class":131},[125,871,872,874,876,879],{"class":127,"line":200},[125,873,269],{"class":138},[125,875,142],{"class":131},[125,877,878],{"class":145},"\"2024-01-16T09:00:00.000Z\"",[125,880,149],{"class":131},[125,882,883],{"class":127,"line":213},[125,884,562],{"class":561},[125,886,887],{"class":127,"line":226},[125,888,319],{"class":131},[125,890,891],{"class":127,"line":239},[125,892,325],{"class":131},[20,894,896],{"id":895},"verifying-webhooks","Verifying Webhooks",[16,898,899],{},"Webhooks are signed using HMAC-SHA256. Verify the signature to ensure requests are from HeadshotPro.",[327,901,903],{"id":902},"signature-header","Signature Header",[116,905,908],{"className":906,"code":907,"language":656},[654],"X-HeadshotPro-Signature: sha256=abc123...\n",[62,909,907],{"__ignoreMap":121},[327,911,913],{"id":912},"verification-example","Verification Example",[116,915,919],{"className":916,"code":917,"language":918,"meta":121,"style":121},"language-javascript shiki shiki-themes dark-plus","const crypto = require('crypto');\n\nfunction verifyWebhook(payload, signature, secret) {\n  const expectedSignature = crypto\n    .createHmac('sha256', secret)\n    .update(JSON.stringify(payload))\n    .digest('hex');\n\n  return `sha256=${expectedSignature}` === signature;\n}\n\n\u002F\u002F Express middleware\napp.post('\u002Fwebhooks\u002Fheadshotpro', express.json(), (req, res) => {\n  const signature = req.headers['x-headshotpro-signature'];\n\n  if (!verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET)) {\n    return res.status(401).send('Invalid signature');\n  }\n\n  \u002F\u002F Process webhook\n  const { event, object } = req.body;\n  console.log(`Received ${event} for model ${object._id}`);\n\n  res.status(200).send('OK');\n});\n","javascript",[62,920,921,926,932,937,942,947,952,957,961,966,970,974,979,984,989,993,998,1003,1008,1013,1019,1025,1031,1036,1042],{"__ignoreMap":121},[125,922,923],{"class":127,"line":128},[125,924,925],{},"const crypto = require('crypto');\n",[125,927,928],{"class":127,"line":135},[125,929,931],{"emptyLinePlaceholder":930},true,"\n",[125,933,934],{"class":127,"line":152},[125,935,936],{},"function verifyWebhook(payload, signature, secret) {\n",[125,938,939],{"class":127,"line":165},[125,940,941],{},"  const expectedSignature = crypto\n",[125,943,944],{"class":127,"line":174},[125,945,946],{},"    .createHmac('sha256', secret)\n",[125,948,949],{"class":127,"line":187},[125,950,951],{},"    .update(JSON.stringify(payload))\n",[125,953,954],{"class":127,"line":200},[125,955,956],{},"    .digest('hex');\n",[125,958,959],{"class":127,"line":213},[125,960,931],{"emptyLinePlaceholder":930},[125,962,963],{"class":127,"line":226},[125,964,965],{},"  return `sha256=${expectedSignature}` === signature;\n",[125,967,968],{"class":127,"line":239},[125,969,325],{},[125,971,972],{"class":127,"line":253},[125,973,931],{"emptyLinePlaceholder":930},[125,975,976],{"class":127,"line":266},[125,977,978],{},"\u002F\u002F Express middleware\n",[125,980,981],{"class":127,"line":279},[125,982,983],{},"app.post('\u002Fwebhooks\u002Fheadshotpro', express.json(), (req, res) => {\n",[125,985,986],{"class":127,"line":292},[125,987,988],{},"  const signature = req.headers['x-headshotpro-signature'];\n",[125,990,991],{"class":127,"line":305},[125,992,931],{"emptyLinePlaceholder":930},[125,994,995],{"class":127,"line":316},[125,996,997],{},"  if (!verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET)) {\n",[125,999,1000],{"class":127,"line":322},[125,1001,1002],{},"    return res.status(401).send('Invalid signature');\n",[125,1004,1006],{"class":127,"line":1005},18,[125,1007,319],{},[125,1009,1011],{"class":127,"line":1010},19,[125,1012,931],{"emptyLinePlaceholder":930},[125,1014,1016],{"class":127,"line":1015},20,[125,1017,1018],{},"  \u002F\u002F Process webhook\n",[125,1020,1022],{"class":127,"line":1021},21,[125,1023,1024],{},"  const { event, object } = req.body;\n",[125,1026,1028],{"class":127,"line":1027},22,[125,1029,1030],{},"  console.log(`Received ${event} for model ${object._id}`);\n",[125,1032,1034],{"class":127,"line":1033},23,[125,1035,931],{"emptyLinePlaceholder":930},[125,1037,1039],{"class":127,"line":1038},24,[125,1040,1041],{},"  res.status(200).send('OK');\n",[125,1043,1045],{"class":127,"line":1044},25,[125,1046,1047],{},"});\n",[20,1049,1051],{"id":1050},"delivery-behavior","Delivery Behavior",[1053,1054,1055,1062,1068,1074],"ul",{},[1056,1057,1058,1061],"li",{},[648,1059,1060],{},"Delay:"," Webhooks are sent with a 5-second delay",[1056,1063,1064,1067],{},[648,1065,1066],{},"Retries:"," Failed deliveries are retried up to 3 times",[1056,1069,1070,1073],{},[648,1071,1072],{},"Timeout:"," Your endpoint must respond within 10 seconds",[1056,1075,1076,1079],{},[648,1077,1078],{},"Expected Response:"," HTTP 200 status code",[20,1081,1083],{"id":1082},"handling-webhooks","Handling Webhooks",[327,1085,1087],{"id":1086},"_1-respond-quickly","1. Respond Quickly",[16,1089,1090],{},"Process webhooks asynchronously and return 200 immediately:",[116,1092,1094],{"className":916,"code":1093,"language":918,"meta":121,"style":121},"app.post('\u002Fwebhooks\u002Fheadshotpro', async (req, res) => {\n  \u002F\u002F Respond immediately\n  res.status(200).send('OK');\n\n  \u002F\u002F Process asynchronously\n  processWebhook(req.body).catch(console.error);\n});\n\nasync function processWebhook({ event, object }) {\n  switch (event) {\n    case 'model.photos_ready':\n      await handlePhotosReady(object);\n      break;\n    case 'model.favorite_selected':\n      await handleFavoriteSelected(object);\n      break;\n  }\n}\n",[62,1095,1096,1101,1106,1110,1114,1119,1124,1128,1132,1137,1142,1147,1152,1157,1162,1167,1171,1175],{"__ignoreMap":121},[125,1097,1098],{"class":127,"line":128},[125,1099,1100],{},"app.post('\u002Fwebhooks\u002Fheadshotpro', async (req, res) => {\n",[125,1102,1103],{"class":127,"line":135},[125,1104,1105],{},"  \u002F\u002F Respond immediately\n",[125,1107,1108],{"class":127,"line":152},[125,1109,1041],{},[125,1111,1112],{"class":127,"line":165},[125,1113,931],{"emptyLinePlaceholder":930},[125,1115,1116],{"class":127,"line":174},[125,1117,1118],{},"  \u002F\u002F Process asynchronously\n",[125,1120,1121],{"class":127,"line":187},[125,1122,1123],{},"  processWebhook(req.body).catch(console.error);\n",[125,1125,1126],{"class":127,"line":200},[125,1127,1047],{},[125,1129,1130],{"class":127,"line":213},[125,1131,931],{"emptyLinePlaceholder":930},[125,1133,1134],{"class":127,"line":226},[125,1135,1136],{},"async function processWebhook({ event, object }) {\n",[125,1138,1139],{"class":127,"line":239},[125,1140,1141],{},"  switch (event) {\n",[125,1143,1144],{"class":127,"line":253},[125,1145,1146],{},"    case 'model.photos_ready':\n",[125,1148,1149],{"class":127,"line":266},[125,1150,1151],{},"      await handlePhotosReady(object);\n",[125,1153,1154],{"class":127,"line":279},[125,1155,1156],{},"      break;\n",[125,1158,1159],{"class":127,"line":292},[125,1160,1161],{},"    case 'model.favorite_selected':\n",[125,1163,1164],{"class":127,"line":305},[125,1165,1166],{},"      await handleFavoriteSelected(object);\n",[125,1168,1169],{"class":127,"line":316},[125,1170,1156],{},[125,1172,1173],{"class":127,"line":322},[125,1174,319],{},[125,1176,1177],{"class":127,"line":1005},[125,1178,325],{},[327,1180,1182],{"id":1181},"_2-handle-duplicate-events","2. Handle Duplicate Events",[16,1184,1185],{},"Webhooks may be delivered more than once. Make your handlers idempotent:",[116,1187,1189],{"className":916,"code":1188,"language":918,"meta":121,"style":121},"const processedEvents = new Set();\n\nasync function handleWebhook(payload) {\n  const eventKey = `${payload.event}:${payload.object._id}:${payload.object.status}`;\n\n  if (processedEvents.has(eventKey)) {\n    console.log('Duplicate event, skipping');\n    return;\n  }\n\n  processedEvents.add(eventKey);\n  \u002F\u002F Process event...\n}\n",[62,1190,1191,1196,1200,1205,1210,1214,1219,1224,1229,1233,1237,1242,1247],{"__ignoreMap":121},[125,1192,1193],{"class":127,"line":128},[125,1194,1195],{},"const processedEvents = new Set();\n",[125,1197,1198],{"class":127,"line":135},[125,1199,931],{"emptyLinePlaceholder":930},[125,1201,1202],{"class":127,"line":152},[125,1203,1204],{},"async function handleWebhook(payload) {\n",[125,1206,1207],{"class":127,"line":165},[125,1208,1209],{},"  const eventKey = `${payload.event}:${payload.object._id}:${payload.object.status}`;\n",[125,1211,1212],{"class":127,"line":174},[125,1213,931],{"emptyLinePlaceholder":930},[125,1215,1216],{"class":127,"line":187},[125,1217,1218],{},"  if (processedEvents.has(eventKey)) {\n",[125,1220,1221],{"class":127,"line":200},[125,1222,1223],{},"    console.log('Duplicate event, skipping');\n",[125,1225,1226],{"class":127,"line":213},[125,1227,1228],{},"    return;\n",[125,1230,1231],{"class":127,"line":226},[125,1232,319],{},[125,1234,1235],{"class":127,"line":239},[125,1236,931],{"emptyLinePlaceholder":930},[125,1238,1239],{"class":127,"line":253},[125,1240,1241],{},"  processedEvents.add(eventKey);\n",[125,1243,1244],{"class":127,"line":266},[125,1245,1246],{},"  \u002F\u002F Process event...\n",[125,1248,1249],{"class":127,"line":279},[125,1250,325],{},[327,1252,1254],{"id":1253},"_3-log-events","3. Log Events",[16,1256,1257],{},"Maintain a log of received webhooks for debugging:",[116,1259,1261],{"className":916,"code":1260,"language":918,"meta":121,"style":121},"app.post('\u002Fwebhooks\u002Fheadshotpro', async (req, res) => {\n  await db.webhookLogs.insert({\n    receivedAt: new Date(),\n    event: req.body.event,\n    objectId: req.body.object._id,\n    payload: req.body\n  });\n\n  res.status(200).send('OK');\n});\n",[62,1262,1263,1267,1272,1277,1282,1287,1292,1297,1301,1305],{"__ignoreMap":121},[125,1264,1265],{"class":127,"line":128},[125,1266,1100],{},[125,1268,1269],{"class":127,"line":135},[125,1270,1271],{},"  await db.webhookLogs.insert({\n",[125,1273,1274],{"class":127,"line":152},[125,1275,1276],{},"    receivedAt: new Date(),\n",[125,1278,1279],{"class":127,"line":165},[125,1280,1281],{},"    event: req.body.event,\n",[125,1283,1284],{"class":127,"line":174},[125,1285,1286],{},"    objectId: req.body.object._id,\n",[125,1288,1289],{"class":127,"line":187},[125,1290,1291],{},"    payload: req.body\n",[125,1293,1294],{"class":127,"line":200},[125,1295,1296],{},"  });\n",[125,1298,1299],{"class":127,"line":213},[125,1300,931],{"emptyLinePlaceholder":930},[125,1302,1303],{"class":127,"line":226},[125,1304,1041],{},[125,1306,1307],{"class":127,"line":239},[125,1308,1047],{},[20,1310,1312],{"id":1311},"complete-integration-example","Complete Integration Example",[116,1314,1316],{"className":916,"code":1315,"language":918,"meta":121,"style":121},"const express = require('express');\nconst crypto = require('crypto');\nconst app = express();\n\napp.post('\u002Fwebhooks\u002Fheadshotpro', express.json(), async (req, res) => {\n  \u002F\u002F 1. Verify signature\n  const signature = req.headers['x-headshotpro-signature'];\n  const expected = `sha256=${crypto\n    .createHmac('sha256', process.env.WEBHOOK_SECRET)\n    .update(JSON.stringify(req.body))\n    .digest('hex')}`;\n\n  if (signature !== expected) {\n    console.error('Invalid webhook signature');\n    return res.status(401).send('Invalid signature');\n  }\n\n  \u002F\u002F 2. Respond quickly\n  res.status(200).send('OK');\n\n  \u002F\u002F 3. Process asynchronously\n  const { event, object } = req.body;\n\n  try {\n    switch (event) {\n      case 'model.created':\n        console.log(`New model created: ${object._id}`);\n        break;\n\n      case 'model.photos_ready':\n        console.log(`Photos ready for: ${object._id}`);\n        \u002F\u002F Fetch favorite photo and update user profile\n        const photo = await fetchFavoritePhoto(object._id);\n        if (photo) {\n          await updateUserProfilePhoto(object.uid, photo.urls.main);\n        }\n        break;\n\n      case 'model.favorite_selected':\n        console.log(`Favorite selected for: ${object._id}`);\n        \u002F\u002F Sync favorite photo\n        const favorite = await fetchFavoritePhoto(object._id);\n        await syncToExternalSystem(object.uid, favorite);\n        break;\n\n      case 'model.deleted':\n        console.log(`Model deleted: ${object._id}`);\n        \u002F\u002F Clean up local references\n        await removeUserProfilePhoto(object.uid);\n        break;\n\n      default:\n        console.log(`Unhandled event: ${event}`);\n    }\n  } catch (error) {\n    console.error('Webhook processing error:', error);\n    \u002F\u002F Don't throw - we already responded 200\n  }\n});\n",[62,1317,1318,1323,1327,1332,1336,1341,1346,1350,1355,1360,1365,1370,1374,1379,1384,1388,1392,1396,1401,1405,1409,1414,1418,1422,1427,1432,1438,1444,1450,1455,1461,1467,1473,1479,1485,1491,1497,1502,1507,1513,1519,1525,1531,1537,1542,1547,1553,1559,1565,1571,1576,1581,1587,1593,1599,1605,1611,1617,1622],{"__ignoreMap":121},[125,1319,1320],{"class":127,"line":128},[125,1321,1322],{},"const express = require('express');\n",[125,1324,1325],{"class":127,"line":135},[125,1326,925],{},[125,1328,1329],{"class":127,"line":152},[125,1330,1331],{},"const app = express();\n",[125,1333,1334],{"class":127,"line":165},[125,1335,931],{"emptyLinePlaceholder":930},[125,1337,1338],{"class":127,"line":174},[125,1339,1340],{},"app.post('\u002Fwebhooks\u002Fheadshotpro', express.json(), async (req, res) => {\n",[125,1342,1343],{"class":127,"line":187},[125,1344,1345],{},"  \u002F\u002F 1. Verify signature\n",[125,1347,1348],{"class":127,"line":200},[125,1349,988],{},[125,1351,1352],{"class":127,"line":213},[125,1353,1354],{},"  const expected = `sha256=${crypto\n",[125,1356,1357],{"class":127,"line":226},[125,1358,1359],{},"    .createHmac('sha256', process.env.WEBHOOK_SECRET)\n",[125,1361,1362],{"class":127,"line":239},[125,1363,1364],{},"    .update(JSON.stringify(req.body))\n",[125,1366,1367],{"class":127,"line":253},[125,1368,1369],{},"    .digest('hex')}`;\n",[125,1371,1372],{"class":127,"line":266},[125,1373,931],{"emptyLinePlaceholder":930},[125,1375,1376],{"class":127,"line":279},[125,1377,1378],{},"  if (signature !== expected) {\n",[125,1380,1381],{"class":127,"line":292},[125,1382,1383],{},"    console.error('Invalid webhook signature');\n",[125,1385,1386],{"class":127,"line":305},[125,1387,1002],{},[125,1389,1390],{"class":127,"line":316},[125,1391,319],{},[125,1393,1394],{"class":127,"line":322},[125,1395,931],{"emptyLinePlaceholder":930},[125,1397,1398],{"class":127,"line":1005},[125,1399,1400],{},"  \u002F\u002F 2. Respond quickly\n",[125,1402,1403],{"class":127,"line":1010},[125,1404,1041],{},[125,1406,1407],{"class":127,"line":1015},[125,1408,931],{"emptyLinePlaceholder":930},[125,1410,1411],{"class":127,"line":1021},[125,1412,1413],{},"  \u002F\u002F 3. Process asynchronously\n",[125,1415,1416],{"class":127,"line":1027},[125,1417,1024],{},[125,1419,1420],{"class":127,"line":1033},[125,1421,931],{"emptyLinePlaceholder":930},[125,1423,1424],{"class":127,"line":1038},[125,1425,1426],{},"  try {\n",[125,1428,1429],{"class":127,"line":1044},[125,1430,1431],{},"    switch (event) {\n",[125,1433,1435],{"class":127,"line":1434},26,[125,1436,1437],{},"      case 'model.created':\n",[125,1439,1441],{"class":127,"line":1440},27,[125,1442,1443],{},"        console.log(`New model created: ${object._id}`);\n",[125,1445,1447],{"class":127,"line":1446},28,[125,1448,1449],{},"        break;\n",[125,1451,1453],{"class":127,"line":1452},29,[125,1454,931],{"emptyLinePlaceholder":930},[125,1456,1458],{"class":127,"line":1457},30,[125,1459,1460],{},"      case 'model.photos_ready':\n",[125,1462,1464],{"class":127,"line":1463},31,[125,1465,1466],{},"        console.log(`Photos ready for: ${object._id}`);\n",[125,1468,1470],{"class":127,"line":1469},32,[125,1471,1472],{},"        \u002F\u002F Fetch favorite photo and update user profile\n",[125,1474,1476],{"class":127,"line":1475},33,[125,1477,1478],{},"        const photo = await fetchFavoritePhoto(object._id);\n",[125,1480,1482],{"class":127,"line":1481},34,[125,1483,1484],{},"        if (photo) {\n",[125,1486,1488],{"class":127,"line":1487},35,[125,1489,1490],{},"          await updateUserProfilePhoto(object.uid, photo.urls.main);\n",[125,1492,1494],{"class":127,"line":1493},36,[125,1495,1496],{},"        }\n",[125,1498,1500],{"class":127,"line":1499},37,[125,1501,1449],{},[125,1503,1505],{"class":127,"line":1504},38,[125,1506,931],{"emptyLinePlaceholder":930},[125,1508,1510],{"class":127,"line":1509},39,[125,1511,1512],{},"      case 'model.favorite_selected':\n",[125,1514,1516],{"class":127,"line":1515},40,[125,1517,1518],{},"        console.log(`Favorite selected for: ${object._id}`);\n",[125,1520,1522],{"class":127,"line":1521},41,[125,1523,1524],{},"        \u002F\u002F Sync favorite photo\n",[125,1526,1528],{"class":127,"line":1527},42,[125,1529,1530],{},"        const favorite = await fetchFavoritePhoto(object._id);\n",[125,1532,1534],{"class":127,"line":1533},43,[125,1535,1536],{},"        await syncToExternalSystem(object.uid, favorite);\n",[125,1538,1540],{"class":127,"line":1539},44,[125,1541,1449],{},[125,1543,1545],{"class":127,"line":1544},45,[125,1546,931],{"emptyLinePlaceholder":930},[125,1548,1550],{"class":127,"line":1549},46,[125,1551,1552],{},"      case 'model.deleted':\n",[125,1554,1556],{"class":127,"line":1555},47,[125,1557,1558],{},"        console.log(`Model deleted: ${object._id}`);\n",[125,1560,1562],{"class":127,"line":1561},48,[125,1563,1564],{},"        \u002F\u002F Clean up local references\n",[125,1566,1568],{"class":127,"line":1567},49,[125,1569,1570],{},"        await removeUserProfilePhoto(object.uid);\n",[125,1572,1574],{"class":127,"line":1573},50,[125,1575,1449],{},[125,1577,1579],{"class":127,"line":1578},51,[125,1580,931],{"emptyLinePlaceholder":930},[125,1582,1584],{"class":127,"line":1583},52,[125,1585,1586],{},"      default:\n",[125,1588,1590],{"class":127,"line":1589},53,[125,1591,1592],{},"        console.log(`Unhandled event: ${event}`);\n",[125,1594,1596],{"class":127,"line":1595},54,[125,1597,1598],{},"    }\n",[125,1600,1602],{"class":127,"line":1601},55,[125,1603,1604],{},"  } catch (error) {\n",[125,1606,1608],{"class":127,"line":1607},56,[125,1609,1610],{},"    console.error('Webhook processing error:', error);\n",[125,1612,1614],{"class":127,"line":1613},57,[125,1615,1616],{},"    \u002F\u002F Don't throw - we already responded 200\n",[125,1618,1620],{"class":127,"line":1619},58,[125,1621,319],{},[125,1623,1625],{"class":127,"line":1624},59,[125,1626,1047],{},[20,1628,1630],{"id":1629},"testing-webhooks","Testing Webhooks",[16,1632,1633],{},"During development, use tools like ngrok to expose your local server:",[116,1635,1639],{"className":1636,"code":1637,"language":1638,"meta":121,"style":121},"language-bash shiki shiki-themes dark-plus","ngrok http 3000\n","bash",[62,1640,1641],{"__ignoreMap":121},[125,1642,1643,1647,1650],{"class":127,"line":128},[125,1644,1646],{"class":1645},"sCudf","ngrok",[125,1648,1649],{"class":145}," http",[125,1651,1653],{"class":1652},"sDGX8"," 3000\n",[16,1655,1656],{},"Then configure the ngrok URL as your webhook endpoint in HeadshotPro settings.",[1658,1659,1660],"style",{},"html pre.shiki code .sTNBD, html code.shiki .sTNBD{--shiki-default:#D4D4D4}html pre.shiki code .s3F5K, html code.shiki .s3F5K{--shiki-default:#9CDCFE}html pre.shiki code .sKc5r, html code.shiki .sKc5r{--shiki-default:#CE9178}html pre.shiki code .scz_3, html code.shiki .scz_3{--shiki-default:#569CD6}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 pre.shiki code .sHkYM, html code.shiki .sHkYM{--shiki-default:#F44747}html pre.shiki code .sCudf, html code.shiki .sCudf{--shiki-default:#DCDCAA}html pre.shiki code .sDGX8, html code.shiki .sDGX8{--shiki-default:#B5CEA8}",{"title":121,"searchDepth":135,"depth":135,"links":1662},[1663,1664,1665,1668,1675,1679,1680,1685,1686],{"id":22,"depth":135,"text":23},{"id":35,"depth":135,"text":36},{"id":110,"depth":135,"text":111,"children":1666},[1667],{"id":329,"depth":152,"text":330},{"id":492,"depth":135,"text":493,"children":1669},[1670,1671,1672,1673,1674],{"id":496,"depth":152,"text":64},{"id":573,"depth":152,"text":74},{"id":661,"depth":152,"text":84},{"id":745,"depth":152,"text":94},{"id":809,"depth":152,"text":104},{"id":895,"depth":135,"text":896,"children":1676},[1677,1678],{"id":902,"depth":152,"text":903},{"id":912,"depth":152,"text":913},{"id":1050,"depth":135,"text":1051},{"id":1082,"depth":135,"text":1083,"children":1681},[1682,1683,1684],{"id":1086,"depth":152,"text":1087},{"id":1181,"depth":152,"text":1182},{"id":1253,"depth":152,"text":1254},{"id":1311,"depth":135,"text":1312},{"id":1629,"depth":135,"text":1630},"Receive real-time notifications for model events","md",{},"\u002Fapi\u002Fwebhooks",{"title":6,"description":1687},"api\u002Fwebhooks","CjMcEn6ggO8A3g_jRlYDQxE1knU7X19ix0RhoGpGcTo",[1695,1699,1702,1704,1707,1710,1713,1716,1719,1722,1725,1728],{"title":1696,"slug":1697,"order":1698},"API Overview","index",0,{"title":1700,"slug":1701,"order":128},"Authentication","authentication",{"title":1703,"slug":484,"order":135},"Organization",{"title":1705,"slug":1706,"order":152},"Invites","invites",{"title":1708,"slug":1709,"order":165},"Teams","teams",{"title":1711,"slug":1712,"order":174},"Team Members","team-members",{"title":1714,"slug":1715,"order":187},"Models","models",{"title":1717,"slug":1718,"order":200},"Photos","photos",{"title":1720,"slug":1721,"order":213},"Whitelabel","whitelabel",{"title":1723,"slug":1724,"order":226},"Errors","errors",{"title":1726,"slug":1727,"order":239},"Rate Limits","rate-limits",{"title":6,"slug":14,"order":253},1792465015441]