> ## Documentation Index
> Fetch the complete documentation index at: https://storekit.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Out-of-Stock Log and Morning Digest

> Log every item that sells out on storekit with reason and source from item.out_of_stock, rank the last 30 days and send a Slack digest each morning.

Every time a dish goes out of stock — snoozed by the team, sold out through inventory, or switched off by your till — storekit sends an event. This recipe stores them, shows you patterns (which dishes, which days, what time) and posts a digest to Slack every morning so the buyer and the head chef start the day knowing what ran out.

<Tip>
  The dashboard already has a snooze report under **Reports** (see [Snooze Reporting](/docs/guides/menu/availability-snoozing#snooze-reporting)). Build this recipe when you also want inventory sell-outs and till-driven changes in the same list, or want the digest pushed to Slack rather than looked up.
</Tip>

## How It Works

1. storekit sends `item.out_of_stock` to your app whenever an item becomes unavailable.
2. Your app records the item, the time, the reason and what caused it.
3. A scheduled job runs at a time you choose each morning and posts yesterday's list, plus a rolling "most sold out this month" table, to Slack.

## What You Need

* Webhooks enabled on your storekit plan (**Settings** → **Developers**).
* A [Replit](https://replit.com) account. This recipe needs a public URL that stays up, a database, and a scheduler for the morning job, which Replit provides in one place.
* A Slack incoming webhook URL for the channel that should receive the digest.

## Step 1: Build the App

Paste the whole block into the builder in a single message.

<div className="sk-build-btns">
  <a className="sk-build-btn sk-build-btn--replit" href="https://replit.com/?stack=Build&prompt=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-Ddz+uleYsRCr2HN2CJUUwTwY3K2DDa-+iDmXIXTspQazeghBk8ZIflllsnhN3Jba2kAApDgyl4BWw78m7kzYOoikjKm6J3PUw4TTLXBg6c-JNoioo4YgFbdKIU9X2WWdTdZ0H-myOzM+SFy6yXJtRZi4jBGLEmDxDCyl16T122g1Bqt75KHrkoZQ5cSevDKrom6JobGdF+wFkcO005LgpCuDgCQGAMBaA7gAPS88gm6QNvOaY6EQCLvoTB+ymV57Uac65Zyy7XA0MoNFEDXCaJiUUh4-KihFCATXwACTSlxNeQkC0ggUTnQkIoI0OAjTtHobUzBZa7zpnyrgxbtnwEKLwVhu1XAdD9kIHSBl2zqCkDIZANv1B2-YA70oFBbBgDLtSAYG5EpHAD+c3DJFMNjtnVHmPMA48J90BQJFrEPj7eIph8ScOhsUSIyxCbdyXPUfc9Kvi6G50N8K5FjtnaW-ka3XwHdwZ91pOPaelir6hWkkJAqxV83WsEYW4gEac6RoPUezJjJdyghqkX29aUxIADiS+RPd8pYAxAmnU2IFlowQ9DI-VETvxYTYLHoBxzf0GMwJ0Dej+64L+ooDgZqFqiqyqCMqqnaDqDquqhOpO1wB4t+10bOPocS7ApsT+L+jg0iKyngnUvcZcQyA4hCEI7A7AXog4+eg8I0QY2yRyzojAjwbAEepA1UTGJ4cQdBUQ5mHiNBPgiw3U+BpC0g1I-U4UHogkH+fAfM54mBbA2BBw4g0ULBIgoi9Ufw0EJAsEgYNBHujoCAOgAaQaAYl8fCjgfwSaGgvQQh1KeCtg6IO6Hi-YDItgh6y8ci4YgwDuAgvMDsXQNIjMga9AdYlU7IDizAlgdY10MhnS-+x0aApYwBz+WAYBoCTCVUKgYwh07IaBtouB-UYYYyCyEGtgIg0A5cxRTwwsEUsQMg-sgkr6Bci2DcjgiAGS2YIw5RUynSSEUkihssxExY4Q4EjuFep4zQdMfYbCMwgYm4rQIwu08Qm4eRRQrClY0AMyIAFAssS4KeA4OQIe08RYYQjQ1wY0mI0oUoyoIANwhIwApI8S1w0WN4d4UoSu8uG4AueSDogAKAQgBnRBR-CKR2TSQu7eRSShR-DhSRTRTuSJTJSQhDyFhKgqjqiajjSTTTSzTzSLTLTwDMCqzdB-CWBLgmD3yUKfgsKPS+LoF1w+RJBBCvxYz569KlQWAjCixSDIAiA+DlLNEAQu4pAlxICnyUgAIXzAJsxJCYg7A9LW62726TGsKszDCUh6AHH1xoLbRsA5BTB07RJFgR40z6xlyFShQXCXCgxJBGZMCsC5Y9LyQUScJYCbzWEmq5DOh5JEhNB8gaJGihBCDgRNDABvSijarCiig0F14TLTwGJR49CJmR5GJWQFyMY2QZK2CFHqyf6mJpGgEOCrQbQRLsljAADk2Q3pV8HIecEcyu+gkIekcplwoKSQZ+ooXxIAQJViuCm0-UhCDC5xnkOg8A6gJ4oU0o8Sb0wAQoI0uI0oQQQQY0Ko8SMxQmnYaxPoShqCWAe43yEASQREAAmqYPwO8GidYZ+JGBxAgNThoJwJMRUOoGZHEHzCzGYF4MoSAAIBeYGJGPVOGZGdGUKKKLYL0LUN0OGuMO4IuHtN6UmbILmebHfrCG5BlAociKEOBGQlIkREELUaOFDFQTfLAA8IRnAS9PNkEKwb7BwSQFwTurQWMIPPAGHP0NWHyrRS8JIb7GRewEzCANqt4URAkd8jgCeW9F9j8GwUUM4V8HAJqpyJGE1nOmCNAFQAQkIOIMgIbFMJoC+ZUMno+ByAIb0n8APNpIGgPCMB4BYL+VeZHr2IQKIHON8joEkCog8KBHQCmPGU6PWPzGQJjDvLkHvJgTQYUb0ggGxLkLRnlLFSAOmi2CziAHyEEH5KaVzugYrJ7t9FCFAuUvsIoRFGgKBP1LCXTDMIxsgAEqEXIFgnMHaZcA9PaapQ8EIFyP4Npv1D+CEBccCVFMEKSNKHyHNBNFNDNHNH+AYClcAF2UqCNCBdro6JiJiDuG9G9MBRGVGTGUokdHSHALjhqk9DIRlddDILAAYiMINeEMgNrPYC2IedcgAFR0XyUR7-klCXlAVAmUSBUTYMS2CUTxUMRvXYo8xGxWhVxgRR63XEC1l8qppwwPQ7jkzXKj7j57oIBT4nq9lyUMVwCOi8FsWCAehEToJERwzUVIzBDmwPFfB0BAlFlDljhmBkXaKUWgy2o0WdlcFECEKBXBWGkxLRBu7cW9BXDfJIT6hohaWJT+hhhERvUyXdB35fW0Rlx0B2WkBvVU1sgYCLjs31mMBEARSYwaVWwngCEeXXLACcAzKWkAj0XsFwA-WgRXltHdBERCi1gu1E3u1-kAVXkJBU2s0RGYDabqz8z7GLgchzGKGmi8Ay3XK4iyU6G+D+DXR8zS6qWfSYZSCcB-BzHsZQhullmBi4zlK-k2FKWcjZ0a15FhhdEKLF0uXRzoCAKoSGwDijr5FTD5XxVXAIwdWEXOCN3chwAHFU0Wi5X7wcj7mD0ZAVzOxQIbAnjabXw9UwXsie1-UaQkmBj0gkK-lHGGkuWQCuDSlALywwBXCtqdndkgC85GQVUnj5Vcl8ANW8lhiET3IV7jqYp8S9LO2TiZBDC8iEjfCrVgXa4piTDdVAWFB6jvBRBhjCECL-lekiBJDpr6wgDMBaSuDrxHUVVW6cBwMxkYF6hDDExYC8LHyBAQg-lhiOn0A6D0iQh+IkK5S+TXDxKigqLzmajADvgCgqIpUxkoEgCLogAIYyiQmyRpnR4qnx6TGWx9jmymTaF0wcPyTgRuBlmFhhh-BfQ-RzDf0qOiyCTyOKOTyLDLDYybaRD9XrB8wSoGRaHVRgB2MKNKPFidFeR2xSTWN0DPRPS9KOl0DgPjydL2MG50xFpbI9LM6MDGQ+41Q4SLiuB0AUbbq7omgHrT6E1s06WkWcDc2YC00OpET+MON0y71wDb3zXxKLWijLWrVgNywqzgTD2Ri3VcPhaTjDxcVsKsMP0JMBMT2FjNM1hgBU2tMDWhDjH8nsJCkdIHiJOTyZXZU0xpUZO+yOzOJFRWIXzuByY21KG+Nw1aMKw1WsFiD1V0CPmpzTOKOubfmDpOmzEdDGwGRxOJBXE3F3ECggBn7AC67iba7XF64gCnnygyiqjSPgVvEzH8Kojojjly4zhEhkhOiABkBN8TOOLoLjALYg8-wvw-i6SFcHaUkzPE1BwtdG1MCZ1DVtkCwwOk6BiUNNiVNXiXNAtEtN2bYDOXOQuUuSuWucSPErYEEGNRNTidNfiX+LYNQ+BbYO00taNKtZS3JLuhFdy9rUYnlTreGPGImNHu2UkMOlAITKgHESsT2KXR0BCAiDfEQHKgaBlKQ85WY663TIgPxXSP1MyHgLqA1UcSMLLChEINHH8MvebaIFcEAA&referrer=docs.storekit.com" target="_blank" rel="noopener noreferrer" aria-label="Open in Replit">
    <img src="https://replit.com/badge?caption=Open%20in%20Replit" alt="Open in Replit" height="36" noZoom />
  </a>

  <a className="sk-build-btn sk-build-btn--lovable" href="https://lovable.dev/#prompt=I%20want%20a%20small%20web%20app%20that%20keeps%20a%20log%20of%20every%20menu%20item%20that%20goes%20out%20of%20stock%20at%20my%20restaurant%2C%20and%20sends%20a%20short%20digest%20to%20Slack%20every%20morning.%20storekit%20tells%20the%20app%20about%20stock-outs%20by%20webhook.%20The%20app%20must%20never%20lose%20an%20event%2C%20never%20count%20the%20same%20event%20twice%2C%20and%20never%20make%20storekit%20wait.%0A%0ABuild%20it%20with%20a%20database%20so%20the%20log%20survives%20restarts%2C%20an%20always-on%20public%20HTTPS%20URL%2C%20and%20a%20scheduled%20job%20for%20the%20digest.%0A%0ACONTEXT%20ABOUT%20STOREKIT%0A%0AI%20run%20a%20hospitality%20business%20on%20storekit%20(storekit.com)%2C%20an%20online%20ordering%20and%20payments%20platform%20for%20restaurants%2C%20caf%C3%A9s%20and%20bars.%20storekit%20can%20notify%20my%20own%20app%20about%20things%20that%20happen%20in%20my%20store%20by%20sending%20webhooks.%0A%0AHow%20storekit%20webhooks%20work%3A%0A-%20Each%20event%20is%20an%20HTTP%20POST%20with%20a%20JSON%20body%20to%20a%20URL%20I%20register%20in%20the%20storekit%20dashboard.%0A-%20The%20body%20always%20has%20the%20shape%20%7B%20%22event%22%3A%20%22%3Cevent%20type%3E%22%2C%20%22data%22%3A%20%7B%20...%20%7D%20%7D.%0A-%20Requests%20are%20signed%20with%20Svix.%20Every%20request%20carries%20the%20headers%20svix-id%2C%20svix-timestamp%20and%20svix-signature.%20Verify%20them%20with%20the%20official%20%60svix%60%20library%20for%20your%20language%2C%20using%20a%20secret%20I%20will%20provide%20in%20an%20environment%20variable%20called%20STOREKIT_WEBHOOK_SECRET.%20Reject%20anything%20that%20fails%20verification%20with%20HTTP%20400.%0A-%20My%20endpoint%20must%20respond%20with%20a%202xx%20status%20within%2015%20seconds.%20Save%20the%20event%20to%20the%20database%20first%2C%20respond%20200%2C%20then%20do%20the%20work%20from%20the%20saved%20copy.%0A-%20Once%20I%20have%20responded%20200%2C%20storekit%20will%20not%20send%20that%20event%20again%2C%20so%20the%20work%20must%20not%20be%20lost%3A%20store%20each%20event%20with%20a%20status%20(pending%2C%20done%2C%20failed)%20and%20only%20mark%20it%20done%20after%20the%20work%20has%20actually%20succeeded.%20On%20startup%20and%20on%20a%20timer%2C%20pick%20up%20anything%20still%20pending%20or%20failed%20and%20retry%20it.%20Never%20do%20the%20work%20only%20in%20memory%20after%20responding.%0A-%20The%20same%20event%20can%20occasionally%20be%20delivered%20more%20than%20once.%20Use%20the%20svix-id%20header%20as%20the%20unique%20key%20for%20the%20saved%20event%3A%20a%20repeat%20of%20an%20event%20already%20saved%20is%20ignored%2C%20whatever%20its%20status.%0A-%20Any%20page%20or%20endpoint%20protected%20by%20a%20password%20must%20be%20served%20over%20HTTPS%20only%3B%20redirect%20or%20refuse%20plain%20HTTP.%0A-%20Money%20fields%20(total%2C%20tip%2C%20deliveryFee%2C%20discountTotal%2C%20item%20price%2C%20modifier%20price)%20are%20integers%20in%20minor%20units%3A%202500%20means%20%C2%A325.00.%0A-%20orderType%20is%20one%20of%20Delivery%2C%20Pickup%2C%20InStore%2C%20Curbside%2C%20CateringDelivery%2C%20CateringPickup%20or%20Billpay.%0A-%20table%20and%20deliveryAddress%20can%20be%20null.%20notes%20can%20be%20null%20or%20empty.%0A%0AExample%20order.created%20payload%3A%0A%0A%7B%0A%20%20%22event%22%3A%20%22order.created%22%2C%0A%20%20%22data%22%3A%20%7B%0A%20%20%20%20%22id%22%3A%20%22ord_abc123%22%2C%0A%20%20%20%20%22code%22%3A%20%22A1B2%22%2C%0A%20%20%20%20%22asap%22%3A%20true%2C%0A%20%20%20%20%22total%22%3A%202500%2C%0A%20%20%20%20%22tip%22%3A%20250%2C%0A%20%20%20%20%22deliveryFee%22%3A%20299%2C%0A%20%20%20%20%22discountTotal%22%3A%200%2C%0A%20%20%20%20%22orderType%22%3A%20%22Pickup%22%2C%0A%20%20%20%20%22createdAt%22%3A%20%222024-01-15T10%3A30%3A00Z%22%2C%0A%20%20%20%20%22deliveryTime%22%3A%20%222024-01-15T11%3A00%3A00Z%22%2C%0A%20%20%20%20%22notes%22%3A%20%22Ring%20doorbell%22%2C%0A%20%20%20%20%22customer%22%3A%20%7B%0A%20%20%20%20%20%20%22firstName%22%3A%20%22John%22%2C%0A%20%20%20%20%20%20%22lastName%22%3A%20%22Doe%22%2C%0A%20%20%20%20%20%20%22email%22%3A%20%22john%40example.com%22%2C%0A%20%20%20%20%20%20%22phone%22%3A%20%22%2B44123456789%22%2C%0A%20%20%20%20%20%20%22marketingConsent%22%3A%20true%0A%20%20%20%20%7D%2C%0A%20%20%20%20%22items%22%3A%20%5B%0A%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%22name%22%3A%20%22Margherita%20Pizza%22%2C%0A%20%20%20%20%20%20%20%20%22price%22%3A%201200%2C%0A%20%20%20%20%20%20%20%20%22quantity%22%3A%201%2C%0A%20%20%20%20%20%20%20%20%22plu%22%3A%20null%2C%0A%20%20%20%20%20%20%20%20%22posId%22%3A%20null%2C%0A%20%20%20%20%20%20%20%20%22taxRate%22%3A%20null%2C%0A%20%20%20%20%20%20%20%20%22modifiers%22%3A%20%5B%5D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%5D%2C%0A%20%20%20%20%22venue%22%3A%20%7B%0A%20%20%20%20%20%20%22id%22%3A%201234%2C%0A%20%20%20%20%20%20%22name%22%3A%20%22My%20Restaurant%22%2C%0A%20%20%20%20%20%20%22slug%22%3A%20%22my-restaurant%22%2C%0A%20%20%20%20%20%20%22address%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%22street1%22%3A%20%22123%20Main%20St%22%2C%0A%20%20%20%20%20%20%20%20%22street2%22%3A%20%22%22%2C%0A%20%20%20%20%20%20%20%20%22city%22%3A%20%22London%22%2C%0A%20%20%20%20%20%20%20%20%22postCode%22%3A%20%22W1A%201AA%22%2C%0A%20%20%20%20%20%20%20%20%22country%22%3A%20%22UK%22%2C%0A%20%20%20%20%20%20%20%20%22companyName%22%3A%20%22My%20Restaurant%20Ltd%22%2C%0A%20%20%20%20%20%20%20%20%22coordinates%22%3A%20%7B%20%22latitude%22%3A%2051.5074%2C%20%22longitude%22%3A%20-0.1278%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%2C%0A%20%20%20%20%22table%22%3A%20%7B%0A%20%20%20%20%20%20%22id%22%3A%20%22tbl_123%22%2C%0A%20%20%20%20%20%20%22name%22%3A%20%22Table%205%22%2C%0A%20%20%20%20%20%20%22covers%22%3A%204%2C%0A%20%20%20%20%20%20%22posId%22%3A%20%22pos_tbl_5%22%2C%0A%20%20%20%20%20%20%22area%22%3A%20%7B%20%22id%22%3A%20%22area_1%22%2C%20%22name%22%3A%20%22Main%20Floor%22%2C%20%22posId%22%3A%20null%20%7D%0A%20%20%20%20%7D%2C%0A%20%20%20%20%22deliveryAddress%22%3A%20%7B%0A%20%20%20%20%20%20%22street1%22%3A%20%22456%20Oak%20Ave%22%2C%0A%20%20%20%20%20%20%22street2%22%3A%20%22Flat%202%22%2C%0A%20%20%20%20%20%20%22city%22%3A%20%22London%22%2C%0A%20%20%20%20%20%20%22postCode%22%3A%20%22E1%206AN%22%2C%0A%20%20%20%20%20%20%22country%22%3A%20%22UK%22%2C%0A%20%20%20%20%20%20%22coordinates%22%3A%20%7B%20%22latitude%22%3A%2051.5155%2C%20%22longitude%22%3A%20-0.0722%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A%0AThe%20full%20list%20of%20event%20types%20and%20payloads%20is%20at%20https%3A%2F%2Fstorekit.com%2Fdocs%2Fdevelopers%2Fwebhooks%2Fwebhook-events%0A%0ATHE%20EVENT%20THIS%20APP%20USES%3A%20item.out_of_stock%0A%0AIgnore%20the%20order%20example%20above%3B%20this%20app%20only%20cares%20about%20item.out_of_stock%2C%20which%20looks%20like%20this%3A%0A%0A%7B%0A%20%20%22event%22%3A%20%22item.out_of_stock%22%2C%0A%20%20%22data%22%3A%20%7B%0A%20%20%20%20%22venue%22%3A%20%7B%20%22id%22%3A%201234%2C%20%22name%22%3A%20%22My%20Restaurant%22%20%7D%2C%0A%20%20%20%20%22item%22%3A%20%7B%20%22id%22%3A%205678%2C%20%22name%22%3A%20%22Margherita%20Pizza%22%2C%20%22plu%22%3A%20%22PLU-001%22%2C%20%22posId%22%3A%20%22pos_item_42%22%2C%20%22sku%22%3A%20%22SKU-MARG-001%22%20%7D%2C%0A%20%20%20%20%22reason%22%3A%20%22snoozed%22%2C%0A%20%20%20%20%22source%22%3A%20%22manual%22%2C%0A%20%20%20%20%22details%22%3A%20%7B%20%22snoozeEnd%22%3A%20%222024-01-15T18%3A00%3A00Z%22%20%7D%0A%20%20%7D%0A%7D%0A%0A-%20reason%20is%20one%20of%3A%20snoozed%20(temporarily%20hidden%20by%20staff)%2C%20inventory_depleted%20(sold%20out%20through%20inventory%20counts)%2C%20disabled%20(switched%20off).%0A-%20source%20is%20one%20of%3A%20manual%20(someone%20in%20the%20dashboard)%2C%20order%20(a%20customer%20order%20used%20the%20last%20one)%2C%20deliverect%2C%20lightspeed%2C%20zonal%2C%20pos_sync%20(pushed%20from%20a%20till%20or%20integration).%0A-%20details.snoozeEnd%20is%20only%20present%20for%20reason%20snoozed%20and%20can%20be%20null%2C%20meaning%20snoozed%20indefinitely.%0A-%20plu%2C%20posId%20and%20sku%20can%20be%20null.%0A-%20There%20is%20no%20%22back%20in%20stock%22%20event.%20The%20app%20records%20when%20things%20went%20out%3B%20it%20cannot%20know%20when%20they%20came%20back.%0A%0AWHAT%20TO%20BUILD%0A%0A1.%20POST%20%2Fwebhooks%2Fstorekit%20%E2%80%94%20verify%20the%20Svix%20signature%20with%20the%20official%20svix%20library%20and%20STOREKIT_WEBHOOK_SECRET%20against%20the%20raw%20request%20body%3B%20400%20on%20failure.%20Save%20the%20event%20and%20return%20200%20immediately.%20Ignore%20any%20svix-id%20already%20saved.%20Handle%20item.out_of_stock%3B%20save%20and%20acknowledge%20any%20other%20event%20type%20but%20do%20nothing%20with%20it.%0A2.%20A%20log%20table%20with%3A%20received%20time%20(store%20UTC%2C%20display%20in%20TIMEZONE)%2C%20venue%20name%2C%20item%20name%2C%20item%20id%2C%20plu%2C%20sku%2C%20reason%2C%20source%2C%20snoozeEnd.%20Record%20the%20event's%20time%20as%20the%20time%20the%20webhook%20arrived.%0A3.%20GET%20%2F%20%E2%80%94%20protected%20by%20HTTP%20basic%20auth%20with%20ADMIN_PASSWORD.%20Three%20things%20on%20one%20page%3A%0A%20%20%20a.%20%22Yesterday%20and%20today%22%3A%20a%20list%20of%20stock-outs%20since%20the%20start%20of%20yesterday%20in%20TIMEZONE%2C%20newest%20first%2C%20showing%20time%2C%20item%2C%20reason%20and%20source%20in%20plain%20words%20(%22Snoozed%20by%20staff%20until%2018%3A00%22%2C%20%22Sold%20out%20through%20inventory%20after%20an%20order%22%2C%20%22Switched%20off%20from%20Zonal%22).%0A%20%20%20b.%20%22Most%20sold%20out%2C%20last%2030%20days%22%3A%20items%20ranked%20by%20number%20of%20stock-outs%2C%20with%20the%20count%20and%20the%20most%20common%20hour%20of%20day%20it%20happens.%0A%20%20%20c.%20Filters%20for%20venue%20(if%20I%20have%20more%20than%20one)%2C%20reason%20and%20a%20date%20range%2C%20and%20a%20Download%20CSV%20button%20that%20exports%20whatever%20is%20filtered%20with%20the%20columns%20from%20point%202.%0A4.%20A%20daily%20digest%20posted%20to%20Slack%20via%20SLACK_WEBHOOK_URL%20at%20DIGEST_TIME%20(HH%3AMM%20in%20TIMEZONE%2C%20default%2007%3A30).%20Content%2C%20in%20Slack%20mrkdwn%3A%0A%20%20%20*Sold%20out%20yesterday%20%E2%80%94%20%3Cvenue%20name%3E%2C%20%3Cdate%3E*%0A%20%20%20One%20line%20per%20item%2C%20in%20time%20order%3A%20%2214%3A20%20%20Margherita%20Pizza%20%E2%80%94%20sold%20out%20(inventory)%22%20or%20%2218%3A05%20%20Sea%20Bass%20%E2%80%94%20snoozed%20by%20staff%20until%2021%3A00%22.%20Group%20by%20venue%20if%20there%20is%20more%20than%20one.%0A%20%20%20Then%20a%20blank%20line%20and%20%22*Most%20sold%20out%20this%20month*%22%20followed%20by%20the%20top%20five%20items%20with%20counts.%0A%20%20%20If%20nothing%20sold%20out%20yesterday%2C%20post%20%22Nothing%20sold%20out%20yesterday.%22%20%E2%80%94%20still%20post%2C%20so%20I%20know%20the%20app%20is%20alive.%0A%20%20%20Post%20the%20digest%20once%20per%20day%20even%20if%20the%20app%20restarts%3A%20record%20the%20date%20of%20the%20last%20digest%20sent%20and%20skip%20if%20it%20has%20already%20been%20sent%20for%20that%20date.%0A5.%20A%20%22Send%20digest%20now%22%20button%20on%20the%20page%20for%20testing%2C%20which%20posts%20the%20digest%20for%20yesterday%20regardless%20of%20whether%20it%20was%20already%20sent.%0A6.%20GET%20%2Fhealth%20that%20returns%20200%20and%20%7B%20%22ok%22%3A%20true%20%7D%20and%20nothing%20else.%0A%0AUse%20TIMEZONE%20for%20every%20day%20boundary%20and%20displayed%20time.%20Do%20not%20guess%20a%20default%3A%20if%20TIMEZONE%20is%20unset%2C%20refuse%20to%20start%20and%20log%20a%20clear%20message.%0A%0ADEFINITION%20OF%20DONE%0A%0A-%20%5B%20%5D%20A%20signed%20item.out_of_stock%20event%20appears%20in%20the%20log%20within%20a%20second%20and%20the%20endpoint%20returned%20200.%0A-%20%5B%20%5D%20The%20same%20event%20delivered%20twice%20creates%20one%20row.%0A-%20%5B%20%5D%20A%20bad%20signature%20returns%20400%20and%20logs%20nothing.%0A-%20%5B%20%5D%20The%20example%20payload%20above%20shows%20as%20%22Margherita%20Pizza%20%E2%80%94%20snoozed%20by%20staff%20until%2018%3A00%22.%0A-%20%5B%20%5D%20The%20digest%20posts%20at%20DIGEST_TIME%20and%20not%20again%20that%20day%2C%20including%20after%20a%20restart.%0A-%20%5B%20%5D%20%22Send%20digest%20now%22%20posts%20to%20Slack%20immediately.%0A-%20%5B%20%5D%20The%20CSV%20download%20opens%20in%20a%20spreadsheet%20with%20one%20row%20per%20event%20and%20the%20columns%20listed.%0A-%20%5B%20%5D%20Restarting%20the%20app%20loses%20nothing.%0A%0AWHAT%20TO%20GIVE%20ME%20WHEN%20YOU%20ARE%20DONE%0A%0A1.%20The%20public%20webhook%20URL%20(%E2%80%A6%2Fwebhooks%2Fstorekit)%20and%20the%20page%20URL.%0A2.%20The%20environment%20variables%20to%20set%20(STOREKIT_WEBHOOK_SECRET%2C%20ADMIN_PASSWORD%2C%20SLACK_WEBHOOK_URL%2C%20TIMEZONE%2C%20DIGEST_TIME)%20and%20where%20to%20set%20them%20on%20this%20platform.%0A3.%20Confirmation%20that%20the%20app%20stays%20up%2024%20hours%20a%20day%20and%20that%20the%20scheduled%20job%20runs%20when%20nobody%20has%20the%20page%20open.%0A" target="_blank" rel="noopener noreferrer" aria-label="Build with Lovable"><img src="https://mintcdn.com/storekit/IGeB_84apyho3Z6U/images/logos/lovable.svg?fit=max&auto=format&n=IGeB_84apyho3Z6U&q=85&s=cc21d11769d5e6b686fed56390a88f5e" alt="" width="20" height="20" noZoom data-path="images/logos/lovable.svg" /> Build with Lovable</a>
</div>

<p className="sk-build-btns__note">Either button opens the builder with the whole prompt below already filled in. Nothing starts until you press send, so read it through and edit the parts marked as yours first.</p>

<Accordion title="Full prompt (click to expand)">
  ```text theme={null}
  I want a small web app that keeps a log of every menu item that goes out of stock at my restaurant, and sends a short digest to Slack every morning. storekit tells the app about stock-outs by webhook. The app must never lose an event, never count the same event twice, and never make storekit wait.

  Build it with a database so the log survives restarts, an always-on public HTTPS URL, and a scheduled job for the digest.

  ```

  ```text theme={null}
  CONTEXT ABOUT STOREKIT

  I run a hospitality business on storekit (storekit.com), an online ordering and payments platform for restaurants, cafés and bars. storekit can notify my own app about things that happen in my store by sending webhooks.

  How storekit webhooks work:
  - Each event is an HTTP POST with a JSON body to a URL I register in the storekit dashboard.
  - The body always has the shape { "event": "<event type>", "data": { ... } }.
  - Requests are signed with Svix. Every request carries the headers svix-id, svix-timestamp and svix-signature. Verify them with the official `svix` library for your language, using a secret I will provide in an environment variable called STOREKIT_WEBHOOK_SECRET. Reject anything that fails verification with HTTP 400.
  - My endpoint must respond with a 2xx status within 15 seconds. Save the event to the database first, respond 200, then do the work from the saved copy.
  - Once I have responded 200, storekit will not send that event again, so the work must not be lost: store each event with a status (pending, done, failed) and only mark it done after the work has actually succeeded. On startup and on a timer, pick up anything still pending or failed and retry it. Never do the work only in memory after responding.
  - The same event can occasionally be delivered more than once. Use the svix-id header as the unique key for the saved event: a repeat of an event already saved is ignored, whatever its status.
  - Any page or endpoint protected by a password must be served over HTTPS only; redirect or refuse plain HTTP.
  - Money fields (total, tip, deliveryFee, discountTotal, item price, modifier price) are integers in minor units: 2500 means £25.00.
  - orderType is one of Delivery, Pickup, InStore, Curbside, CateringDelivery, CateringPickup or Billpay.
  - table and deliveryAddress can be null. notes can be null or empty.

  Example order.created payload:

  {
    "event": "order.created",
    "data": {
      "id": "ord_abc123",
      "code": "A1B2",
      "asap": true,
      "total": 2500,
      "tip": 250,
      "deliveryFee": 299,
      "discountTotal": 0,
      "orderType": "Pickup",
      "createdAt": "2024-01-15T10:30:00Z",
      "deliveryTime": "2024-01-15T11:00:00Z",
      "notes": "Ring doorbell",
      "customer": {
        "firstName": "John",
        "lastName": "Doe",
        "email": "john@example.com",
        "phone": "+44123456789",
        "marketingConsent": true
      },
      "items": [
        {
          "name": "Margherita Pizza",
          "price": 1200,
          "quantity": 1,
          "plu": null,
          "posId": null,
          "taxRate": null,
          "modifiers": []
        }
      ],
      "venue": {
        "id": 1234,
        "name": "My Restaurant",
        "slug": "my-restaurant",
        "address": {
          "street1": "123 Main St",
          "street2": "",
          "city": "London",
          "postCode": "W1A 1AA",
          "country": "UK",
          "companyName": "My Restaurant Ltd",
          "coordinates": { "latitude": 51.5074, "longitude": -0.1278 }
        }
      },
      "table": {
        "id": "tbl_123",
        "name": "Table 5",
        "covers": 4,
        "posId": "pos_tbl_5",
        "area": { "id": "area_1", "name": "Main Floor", "posId": null }
      },
      "deliveryAddress": {
        "street1": "456 Oak Ave",
        "street2": "Flat 2",
        "city": "London",
        "postCode": "E1 6AN",
        "country": "UK",
        "coordinates": { "latitude": 51.5155, "longitude": -0.0722 }
      }
    }
  }

  The full list of event types and payloads is at https://storekit.com/docs/developers/webhooks/webhook-events
  ```

  ```text theme={null}
  THE EVENT THIS APP USES: item.out_of_stock

  Ignore the order example above; this app only cares about item.out_of_stock, which looks like this:

  {
    "event": "item.out_of_stock",
    "data": {
      "venue": { "id": 1234, "name": "My Restaurant" },
      "item": { "id": 5678, "name": "Margherita Pizza", "plu": "PLU-001", "posId": "pos_item_42", "sku": "SKU-MARG-001" },
      "reason": "snoozed",
      "source": "manual",
      "details": { "snoozeEnd": "2024-01-15T18:00:00Z" }
    }
  }

  - reason is one of: snoozed (temporarily hidden by staff), inventory_depleted (sold out through inventory counts), disabled (switched off).
  - source is one of: manual (someone in the dashboard), order (a customer order used the last one), deliverect, lightspeed, zonal, pos_sync (pushed from a till or integration).
  - details.snoozeEnd is only present for reason snoozed and can be null, meaning snoozed indefinitely.
  - plu, posId and sku can be null.
  - There is no "back in stock" event. The app records when things went out; it cannot know when they came back.

  WHAT TO BUILD

  1. POST /webhooks/storekit — verify the Svix signature with the official svix library and STOREKIT_WEBHOOK_SECRET against the raw request body; 400 on failure. Save the event and return 200 immediately. Ignore any svix-id already saved. Handle item.out_of_stock; save and acknowledge any other event type but do nothing with it.
  2. A log table with: received time (store UTC, display in TIMEZONE), venue name, item name, item id, plu, sku, reason, source, snoozeEnd. Record the event's time as the time the webhook arrived.
  3. GET / — protected by HTTP basic auth with ADMIN_PASSWORD. Three things on one page:
     a. "Yesterday and today": a list of stock-outs since the start of yesterday in TIMEZONE, newest first, showing time, item, reason and source in plain words ("Snoozed by staff until 18:00", "Sold out through inventory after an order", "Switched off from Zonal").
     b. "Most sold out, last 30 days": items ranked by number of stock-outs, with the count and the most common hour of day it happens.
     c. Filters for venue (if I have more than one), reason and a date range, and a Download CSV button that exports whatever is filtered with the columns from point 2.
  4. A daily digest posted to Slack via SLACK_WEBHOOK_URL at DIGEST_TIME (HH:MM in TIMEZONE, default 07:30). Content, in Slack mrkdwn:
     *Sold out yesterday — <venue name>, <date>*
     One line per item, in time order: "14:20  Margherita Pizza — sold out (inventory)" or "18:05  Sea Bass — snoozed by staff until 21:00". Group by venue if there is more than one.
     Then a blank line and "*Most sold out this month*" followed by the top five items with counts.
     If nothing sold out yesterday, post "Nothing sold out yesterday." — still post, so I know the app is alive.
     Post the digest once per day even if the app restarts: record the date of the last digest sent and skip if it has already been sent for that date.
  5. A "Send digest now" button on the page for testing, which posts the digest for yesterday regardless of whether it was already sent.
  6. GET /health that returns 200 and { "ok": true } and nothing else.

  Use TIMEZONE for every day boundary and displayed time. Do not guess a default: if TIMEZONE is unset, refuse to start and log a clear message.

  DEFINITION OF DONE

  - [ ] A signed item.out_of_stock event appears in the log within a second and the endpoint returned 200.
  - [ ] The same event delivered twice creates one row.
  - [ ] A bad signature returns 400 and logs nothing.
  - [ ] The example payload above shows as "Margherita Pizza — snoozed by staff until 18:00".
  - [ ] The digest posts at DIGEST_TIME and not again that day, including after a restart.
  - [ ] "Send digest now" posts to Slack immediately.
  - [ ] The CSV download opens in a spreadsheet with one row per event and the columns listed.
  - [ ] Restarting the app loses nothing.

  WHAT TO GIVE ME WHEN YOU ARE DONE

  1. The public webhook URL (…/webhooks/storekit) and the page URL.
  2. The environment variables to set (STOREKIT_WEBHOOK_SECRET, ADMIN_PASSWORD, SLACK_WEBHOOK_URL, TIMEZONE, DIGEST_TIME) and where to set them on this platform.
  3. Confirmation that the app stays up 24 hours a day and that the scheduled job runs when nobody has the page open.
  ```
</Accordion>

Open the app, press "Send digest now", and check Slack shows "Nothing sold out yesterday." before connecting storekit.

## Step 2: Connect storekit

<Steps>
  <Step title="Open the webhooks portal">
    In the dashboard sidebar, go to **Settings** → **Developers** and click **Manage Webhooks** (it reads **Enable Webhooks** the first time). Webhooks are a subscription feature — if you see **Upgrade to enable webhooks**, [contact support](/docs/getting-started/contact-support) first.
  </Step>

  <Step title="Add your app as an endpoint">
    In the embedded portal, add a new endpoint, paste the webhook URL your app builder gave you, and tick only the event types the recipe needs.
  </Step>

  <Step title="Copy the signing secret into your app">
    Open the endpoint you just created and copy its signing secret. In your app builder, save it as the environment variable `STOREKIT_WEBHOOK_SECRET` (builders call this "secrets" or "environment variables"). Redeploy if the builder asks you to.
  </Step>

  <Step title="Send a test event">
    Use the portal's test option on the endpoint to send a sample event. The delivery log shows the response code from your app. A `2xx` means your app accepted it; anything else, read the response body in the log — it is usually a missing secret.
  </Step>
</Steps>

Tick only `item.out_of_stock` for this endpoint.

## Step 3: Test With a Real Stock-Out

Snooze an item in the menu builder (see [Snoozing Products](/docs/guides/menu/availability-snoozing#snoozing-products)). Within a few seconds a row appears in the app with reason "snoozed" and source "manual". Unsnooze the item afterwards — the app does not record this, which is expected.

## Ideas

* **Weekly buyer email**: ask the builder to add a Monday email (via a provider such as Resend or Postmark) with the 30-day table attached as CSV.
* **Prep list**: mark items that sell out before 8 pm three times in a week as "under-prepped" and list them separately.
* **Cross-check with the till**: filter by `source` to see how often your POS is hiding items you thought were on sale.

## What This Does Not Do

* **It cannot tell you when an item came back**, because there is no "back in stock" event. Read the log as "went out at", not "was out for".
* **It does not change availability.** Un-snoozing and restocking happen in the dashboard or your till as before.
* **Events start from the moment you connect the endpoint.** There is no history before that; the 30-day table fills up over the first month.
* **It is your service.** storekit support can confirm an `item.out_of_stock` event reached your URL and the response code; the log and digest are yours.

## Related

<CardGroup cols={2}>
  <Card title="item.out_of_stock payload" icon="code" href="/docs/developers/webhooks/webhook-events#item-out-of-stock">
    Every reason and source value the event can carry.
  </Card>

  <Card title="Writing a Prompt That Works" icon="wand-magic-sparkles" href="/docs/guides/build/prompting-guide">
    The structure behind this prompt, and what to say when the builder gets it wrong.
  </Card>

  <Card title="Order Alerts to Slack or Your Phone" icon="bell" href="/docs/guides/build/recipes/order-alerts">
    The same pattern for orders instead of stock.
  </Card>

  <Card title="Availability and Snoozing" icon="utensils" href="/docs/guides/menu/availability-snoozing">
    Snoozing, inventory and the built-in snooze report.
  </Card>
</CardGroup>


## Related topics

- [Build Your Own Tools on storekit](/docs/guides/build/overview.md)
- [Syrve Stock & Availability](/docs/guides/integrations/pos/syrve/stock-and-availability.md)
- [Toast Menu Sync](/docs/guides/integrations/pos/toast/menu-sync.md)
- [Availability & Snoozing](/docs/guides/menu/availability-snoozing.md)
- [Toast POS](/docs/guides/integrations/pos/toast/overview.md)
