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

# Payout Reconciliation Sheet

> Track every storekit payout in a ledger keyed by svix-id, with a reconciled tick-box and CSV export, fed by the payments.payout.created webhook.

Every payout storekit sends to your bank becomes a row in your own ledger the moment it is created: amount, currency, which bank account, and when to expect it. Tick each row off as it appears on your bank statement, and download the month as CSV for your accountant.

<Tip>
  The dashboard lists payouts under **Analytics** → **Accounting** → **Payouts** (see [How payouts work](/docs/payouts/overview)). Build this recipe when you want your own reconciliation state (ticked off, notes, expected-vs-arrived), an export shaped for your bookkeeping software, or a Slack nudge when a payout is due.
</Tip>

## How It Works

1. storekit sends `payments.payout.created` to your app when a payout is created.
2. Your app adds a row per payout with the total by currency, the last four digits of the bank account, and the estimated arrival date.
3. You tick rows off against your bank statement; anything past its arrival date and still unticked is highlighted.

## What You Need

* Webhooks enabled on your storekit plan (**Settings** → **Developers**).
* A [Replit](https://replit.com) account — the ledger needs a database and a URL that stays up.
* Optional: a Slack incoming webhook URL for a weekly "still not reconciled" reminder.

## 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=JIAg7ghgdgLiEgM4FsIBs0gA4CcCWAbhDAKbgkBG8WWIMAFsSANYklaLwhokAmA5iRwgA9gDM69MlggBPEQFcYnRDBE4SzPHEQkovTmpDJZICtGbwAxlcWwANEhEhQV6CA22oVvD0klkeH4IPChVY1NzKEtVYgC9GAA6JDUNLThSDEMpaloICkU4Eggremw5QrNTMEp6ERFmZIAVHIgaYwVwqBICIW4RXS4ZeSVHbt7hT3VefyQIZDIRbrowPCsSR2gZ8b7UVhT1TW1wEKSAKDOAIQVfGePVhi5eYnyIQcRnBjIeAT7EBRwBEIJE4GliOGU8H0UPgaEgskQAFoltgFBQ0GsQAAJJpNAAKAGUQABVABKABlEhcAMIAeQAck0AKIADSaIAAgpdacT2QSmrTSUyANLAJoXUA4BRQLh1RBYbTobSRTqhEGcFGqQ7pEAACi1aW0iVsyAAlJsZUsMctpkJQvwoTNhgtYJwsGhiGJ1IEvZMQTAIADoMpHG4xABLzhbMwQHCIZIGo5wNwyqAiGB4MSmEyiMAytp5ApKST27JMRg0PQgUIRA4aKpIPS8e3kCh1Brxi5YkRgOtJ1vt5icMDqZgALjOiJATJKZR6CWrUZlOPxIDxtP54G0ZQQACkCQyzCJeKYjAgyeSXB4SPw8Ko+jWvn2dc9EPQCrHeFSpy0yAUT7C8KcIw2RkG+bRkAA3iAABE86wDBY6wQAPPBGSyFgJAAHwwY4MHPAGiEgNBiSkSAAC+FHfiApIkAAjgo-pRvWiB4Pw3QzA8ZQEkCAAeyRMhMpgaAx-ogG4OD4CCsxSBAvBCCofGIngvCOIgSkZgssTIHk0LqXgvGIqx7HEACJDJAAanaWb+IEXGzOIYhrHg6AgAABvpvFudweAUDgsamL6IAjMIHpQPwCgQIIjiquFXC6FYGhwKAqwYNgOAiEC8nVvmMp6ECGVQC6cBEPg+R+G4GB8CA-KCiKYoAPoAOpMpcWK0rSwoNQSTLUkKTTJLRABWJBWHA0CyAwLYMEwYghGgnATJmazEHgKL2SueIgAALAADLt1EALKmE2WAiKEcDIJ0cBgmd0L2QgABMvG8SkpnDtuNYAIwAKyNl4BjJASEC9LMaF0J8OQEa8gxOXGMCOLdSwzI9+2OF8Mq8JDZAjjglhiBlgRPogIPVbYWCyNRtLeGQoCMKDSP6NVqO7WpqT9qlmBpjoTaSEw4NRSEUBqdj4Cjh0XTpmY3wDDASGJiAxSlIrvSwFujwILEMCdHqmH6PajhY90jhzb4fCmo6ohQGg2axpYxxG2QEBiKQwhPrjlggdY2voDbSAKDYbDyV+IDU29EIKLpMwoggmlCI4CpWJYkdQpN9AtqoviYHrzZxeoICmz8ltJTgphGiA9LzsIWOzB7Vt+zWCzIOopjO6717ysj9rUb+cwLCrC4pqINhvGtUC+5EZDyRiEzVc39YzZaNPJMSgzE0pKkgLJ8nCG8szSngoksCQgX58TpMzGhSEIBomFMOIMIC2gGhyaYJO9HcnBsWmGiqeAjCkAmNWSEWtOjUQ5FAUwMhBCiGEKdc6atcDplGqQGYFBW7lEQIgXGMwrrhAoGBIQH9RBAM2kSK0sgADc15myeDgPnDQYhOjSA9DWTaR0lgnwLngEgaADB6jUAGNA6M8BYENrw4EpcABibBDZ3lsNKGATR0zoEcNoAI6U1gbGMMeZafRcBaItrGMgF0bwKRysYUI+cD7KCQo9H6+1jDFDCCAAAxfYxI+1qK2hwE0DCJiNQ2gkAAEQkUJRweI1jMEjo4YAUACTs0cNSAEFBWLySSXEfA4VQkzyELIDJrt7SRKTinfOlws7DGogGdETtoTT0kbIDkvBeBgk4EPAhIAoAKAwMkbm0l2lkC6WlfOAQsAwEphcJkvF5jukWDgHexoX6oPKLINAIg5ITjOJBM4IBYJoSIjBHxiziioNwjs2C0MiLbN2bsmCKkDnTAavkKwX1HoAGYzk3NgrYeSByORfUuI9T5NyYJvDaERGAUoNjnNuYI9ARF7FoxhbBDMWAEUOPsMi-CYS8kyJIAigAnASzFXz8LyLsEolRaAiKsyxT4vxmEDnFOiWiklILEonL4ByGABzUaPW2oiXaX1ES-SaF9XaY43kSv2gALWBbc+pQkmh4AWLy3a-LBXCtFV9L6Y59p6t2nKtlty+mIAOaSFsWN1AEIwPK7510RALBwFc5Fty4aqHpPMfFSEYK7hEPQKAdrbkeg9V6g5wSRD4uNSCgI80DlDX9VAAAAiQaZOkeDGkdUG2CWA6jdAOQAam2ttV5bzto-QAGwAHYAAcBLs0wVQHjEgGZwrUiWLoBCSFIWMWReRaNdzSDIDNUhAA2q64iE6TVhp9YdWM-ApD4ADGuPAAAvVdEBs23IMesIirykVfNJQxYMyo93RtJe6BQREhnCKnTmgYwBeDXu6bew9IKAy8VJHEZ9GBz0gubs2JyCkiKjoALoTvIsi0DA7VaMRdW+u5T6kKlu2n+mC49VWztMLRWIQYEJocQGgBQ-ADkmERGCAMeGeVobki09U8G32wVUBoFtX0DmlpAHOmsCSt1MchWwGAQKfW8Zgj4cZBzyTIyWCJs6qh22-J9U1L6HIQDKY5CJhRsBS4HOJMKDTjqZCQM9Zh2Cx0aL+kDP5NW5IYBPr-bc2w0xQhxBHcRWCHoMzawUyAH6X1EgOKrah9zSxbxee9SAQViRXm1oohBvtA7qk8AY6S+5PqYDogaqWhtGHwswSaOVMgP0G22AmK51DE6YKycfQc2TDV0toAakVmjL8rmwVS7BYxEBMu4Vgjlg5XGZRSLWeoHrlWH1Ic6S+2LXz+1YsVXkppdGsHJZBcxgTbGfXloraHCAlgOS9AbWtltQnYJDaYECtDYnZASak4GtDsmYDydy0yL6IAK0cnpMVil2mfW6a+058epBXPQRgh57QChvO+f879H6eE1nhXB95yLu0q2PUetNm5kHdmQcg2cXuTC0oYnCA-cG4zMJLidHINZckv5RjgPQGAMAOBjgAPQs8TOkTNyAWdYysIgHn841mYTjCzmobZ6hDlF7UCXiI0KIAuE0LETJpwWSZIyEAivgBEg5HiLaxIeoEiQs6BI8ZhiFGOXEXgEp2KHAcvMvoqaZl+HyJlEg1CppRnaBQ8SxioxFjgMb10iQzdKAt6gxwYB07K2G0OHy+wPebOuXs1WPKfWB+UMHiooeOWW+BfhF4K3QXN0Ua58db6QdWABBobw12fUAHFLh4lG0QIjuW3mPXFf53aMEKJ-or1XvQVha+wQb03vCLe4M+vFVKzx3eMe7Og1iqIzAOQ2ApSt257X8IUAJVWkgqMfoit2j9KwiJtqPTEGIRENbtoVt2oiXgu0K1WHsVYCgFaa1SobcAS4H2DkN9+ixEuF0z2miwrRQ0rVrQbRKE0xgHpAUGQAIWdR9X2iFXeW2gbT8mgFKCewOX2hQ2KwHxr3-0bxgniyxX9BVUtw5EkkIHQGCW-R9T5QFSFRFSrTIOxzOFx2AB0nUADDVkAzECEEH2kgJkdTtx3gHldAnCnEOk4VMEXQCQrlpHZEbisWEBsXjGnFnBAAnyUIQHkh8FQEwGYxbEbggATQ0KgG0EQB9Xb07wcR7wWGgE4DeXsA7wOgcWSDxFjEGGOD3gMNGioOEU6SrngCjALmpzgBGH8BlAymlBmEABQCEAZhJ4II4wnyLA0uWBPsYBcI+KSFFsaMf4OybcHKUgQQYQPWdYaieYClWnLgInJIGqBA2YPQt0PoSvSSQfKhIYLPOAToSKDABQveThcSIgofMwYsGubmcIkokxZogkOoXsJWMoLo6vSYuFTAFEJ8DKMAHuHIYYanGYL2NMFZSoTeaMM4hWHLFeNeHITyZSE44oSQveJ8A+I+VgU+N2HIPY6iZfVfWA8SJYAMUITgAnTAZfEAeSUEhaIGdmeuU8HIENOAL0AEcSRgfyMacxB+GAileAxAvoXUfOB+H-D7asCQPExRAkpAxcSxLBe0IxaEJ8AEtfRRRIFSZICNTpKWBWfOZseUD0JEsgCElwX-ekHIqktWLpQk3eSBSPYQtRbwIjXOB0NZfgTsKcSg1AVBGg-AFvBg0gOkgwuIPUAATQtLNMREOkOkRGCWCSZNwTkGlkmx6QuCaixA5HZAFBAGAOAHJGCQuD8zXA3HZCl3Fw7HZ3Zh1GSKWhsifB4gMiQG-lMnrHsifEcmclck8kyP8myOjFqiFFFCaGalanak6m6l6n6iCCFnCF2IgF7BEkYnwWPF6L2l2itgLnmjMiBlJlmD2OLhbQBBlBZmrGQAWGbDiBtmSGABt3rAmiQA3hmHQBfgAnfj4GSCxC2D8HT1N36LDz4GoXXMthKGYDTDAB+BgQXPTEXSkPQkwimLgBmJvJbHsiNDOEemSBU0vL6ESxxm3CQk8BIGBBmDjj1F5GpDkUFOdJrCaGAEOiZBlQZCZHNHgGL1dGwE6ImPyRjGiGsCBJRILkUBwEcChKlLgBUkcG1Mt3gFoJb2hLiERlGiWB8CLl1FkBBBZzTFQqmG8DNmjmFjeiHQXEYWEJpj1AJjYDoFTRgFQtNQkpY2kt4lkqpDeWSDryZDDJAGSKQVIGxLQVME2hjFYisHgCUDKHsg5GCUOmAHpAajxA5AJAJCakFGCS-LoAK1EAkBD0hG6BqHCHdTgAeh0VgDKCcjQHbl1Hkjmm6TlnGO6LVmblCtQrfB7GmhyFsCI2QBcRd16AEh0IHK9gQFoi8FYuqgzCTkRAKFemjD4EVBqSEviDVlEo2NqW2GQXBJ4T4TLB0D7K4kqAQGgTIA0GOMGh7GHDlDIGouWVjH1NcgIiUPnjIEXhAHRw9ktTkC-hlCfBkHCGjGMR5JumYr4qLgOvTgXQxAXRmtlK-IKFBifD-O7SpXKE3kqA9xCseGF3io2Jwq2OvFKv4vRmesKFiH1nCipG2i5J7GtnWRmGpAJAsiIp+JFN8FdjnhBPoEcDGIHK+p8tDBECyrCEAtGmAo-gagWtDGws2HQoRlwuYAahRKC2mr4CeTotUX+pYsBo5pOpZqWDZjiGKgahapELGA6q-JpqjDdFYUxnSPQA+keBgBHGwA9HWApx5O+p6KQFkEQIJqcCPAaFYHYAznEEVoOpVTOghFAkCCsB4GgGnJcEpPOOLC9iWr5iXjIHWJ6IjyXUWGWHhsRpxqwoSqHypB+mSFpDGTHlchqE0D9g0GQFCB3iQkzBqnJA5GpC6hajag6i6gvDpN0FpqrlMDkP0GdKYF2gJQNQsTgoQqQvpGVwe1gglJ8sMCpWtAdBZEOuChbW5rKr-gJvkgCsJhAGQgWpwixuEG5nTjihTq+HnPrDTG6CBi0FoASDwBGrLgkCfFKhcgarvBSLCBbSpArXUs0pABZ1kgirKBmiOu1hwBcVHOjBBwaAhShQoktinpbF4V0CpDOGCSZCkVsrFGAEPFpCkRAGCWQouCnFHRAFAw1xyEd3TWkCp1hvgDuo9qWWkmxp7CQGWJbFQ35U8I7NH3VjnEzh1OqlmroMwC+irRAF3HcGYOolgfgd7hJn7nBnm1-hWC0XEiwcCWGp7BYbgc5BjBmGMkBzMmvDvpcXbMHNsHmU4E-vBsnBAFYYrhfJnu2qhheHMF0ATiijmX6A1OBNgFrNmFFLJIlPznIsm1lO0sRNgkgJrR71UFWW+DeBgERDROEHaJEbYaiRbBKs5qLnXK-nHLqqnNbj0gBCBF6AiJGrQf2o7gDAhACYQbIH9tED1i2vilwBeLfAEzIbQvqPyJlKQM1PUdEZUx8pKfcGZuXLZswAQBjssCimcCuKll4r7rpPOvoEuoZw3PdM9O9NpBADr2AFV042Vw9LVxADNJ5E5CFAgagbOGDN7iwDRAxFMrF0HBJApD1EADICcMwcfnDnbQR02YH84QC8KkT8zJxWKAAqJYYqXQ2MXengVuxsOAXUQs+qEsrO8srqHqPqTSxwKymyuyhypyly0kYJRwGuxC5CrGyOpYCeVO9OzOssnOhqC8K5hUheZwAu2yTs9690T0b0VS5IdtKAOGHUsePmDIVodoWIBEFI2gflLeYiiI54CZIAA&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%20private%20web%20app%20that%20keeps%20a%20ledger%20of%20the%20payouts%20storekit%20sends%20to%20my%20bank%20account%2C%20so%20I%20can%20reconcile%20them%20against%20my%20bank%20statement.%20storekit%20tells%20the%20app%20about%20each%20payout%20by%20webhook.%20The%20app%20must%20never%20lose%20a%20payout%2C%20never%20record%20the%20same%20one%20twice%2C%20and%20never%20make%20storekit%20wait.%0A%0ABuild%20it%20with%20a%20database%20so%20the%20ledger%20survives%20restarts%20and%20an%20always-on%20public%20HTTPS%20URL.%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%20payments.payout.created%0A%0AIgnore%20the%20order%20example%20above%3B%20this%20app%20only%20cares%20about%20payments.payout.created%2C%20which%20looks%20like%20this%3A%0A%0A%7B%0A%20%20%22event%22%3A%20%22payments.payout.created%22%2C%0A%20%20%22data%22%3A%20%7B%0A%20%20%20%20%22amounts%22%3A%20%5B%0A%20%20%20%20%20%20%7B%20%22currency%22%3A%20%22GBP%22%2C%20%22value%22%3A%20%223210.50%22%20%7D%2C%0A%20%20%20%20%20%20%7B%20%22currency%22%3A%20%22GBP%22%2C%20%22value%22%3A%20%221030.00%22%20%7D%0A%20%20%20%20%5D%2C%0A%20%20%20%20%22bankAccount%22%3A%20%7B%0A%20%20%20%20%20%20%22id%22%3A%20%22db97e205-105c-42ff-8460-d06c25cb6830%22%2C%0A%20%20%20%20%20%20%22IBAN%22%3A%20%22GB15HBUK40127612345678%22%2C%0A%20%20%20%20%20%20%22accountNumber%22%3A%20%220001234%22%2C%0A%20%20%20%20%20%20%22branchCode%22%3A%20%22001234%22%2C%0A%20%20%20%20%20%20%22currency%22%3A%20%22GBP%22%0A%20%20%20%20%7D%2C%0A%20%20%20%20%22estimatedArrivalDate%22%3A%20%222024-01-17%22%0A%20%20%7D%0A%7D%0A%0AImportant%20differences%20from%20the%20order%20events%3A%0A-%20Money%20here%20is%20NOT%20in%20minor%20units.%20Each%20value%20is%20a%20decimal%20string%20in%20major%20units%3A%20%223210.50%22%20means%203%2C210.50.%20Parse%20it%20as%20a%20decimal%2C%20never%20as%20a%20float%20you%20then%20round%20%E2%80%94%20use%20a%20decimal%20library%20or%20store%20it%20as%20a%20string%20and%20sum%20with%20integer%20pence.%0A-%20amounts%20is%20a%20list.%20Sum%20the%20values%20per%20currency%3B%20a%20payout%20usually%20has%20one%20currency%20but%20do%20not%20assume%20it.%20Show%20each%20currency%20total%20on%20the%20row.%0A-%20The%20payload%20has%20no%20payout%20id%20and%20no%20store%20name.%20Use%20the%20svix-id%20header%20as%20the%20unique%20key%20for%20the%20row.%0A-%20bankAccount%20contains%20full%20bank%20details.%20Store%20only%20the%20last%20four%20characters%20of%20accountNumber%20(or%20of%20IBAN%20if%20accountNumber%20is%20missing)%20and%20the%20bankAccount.id.%20Do%20not%20store%20or%20display%20the%20full%20IBAN%20or%20account%20number%20anywhere%2C%20including%20logs.%0A-%20estimatedArrivalDate%20is%20a%20date%20(YYYY-MM-DD)%20and%20may%20be%20null.%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%20row%20and%20return%20200%20immediately.%20Ignore%20any%20svix-id%20already%20saved.%20Handle%20payments.payout.created%3B%20save%20and%20acknowledge%20any%20other%20event%20type%20but%20do%20nothing%20with%20it.%0A2.%20A%20ledger%20table%20with%3A%20received%20time%20(UTC%2C%20display%20in%20TIMEZONE)%2C%20amounts%20per%20currency%2C%20bank%20account%20last%20four%2C%20bank%20account%20id%2C%20estimated%20arrival%20date%2C%20reconciled%20(yes%2Fno)%2C%20reconciled%20on%2C%20statement%20reference%20(free%20text)%2C%20notes%20(free%20text).%0A3.%20GET%20%2F%20%E2%80%94%20protected%20by%20HTTP%20basic%20auth%20with%20ADMIN_PASSWORD.%20A%20table%20of%20payouts%20newest%20first%20with%20a%20month%20filter%20(default%3A%20current%20month)%2C%20showing%20the%20columns%20above.%20Each%20row%20has%20a%20Reconciled%20tick-box%20and%20editable%20statement%20reference%20and%20notes%20fields%20that%20save%20without%20a%20page%20reload.%20Rows%20whose%20estimated%20arrival%20date%20is%20more%20than%202%20working%20days%20in%20the%20past%20and%20are%20not%20reconciled%20are%20highlighted%20amber.%20Above%20the%20table%3A%20total%20paid%20out%20this%20month%20per%20currency%2C%20total%20reconciled%2C%20total%20outstanding.%0A4.%20Download%20CSV%20for%20the%20filtered%20month%2C%20one%20row%20per%20payout%2C%20columns%3A%20received_date%2C%20currency%2C%20amount%2C%20bank_last4%2C%20estimated_arrival%2C%20reconciled%2C%20reconciled_on%2C%20statement_reference%2C%20notes.%20Amounts%20as%20plain%20decimals%20with%20two%20places%20and%20no%20currency%20symbol%20so%20bookkeeping%20software%20imports%20them%20cleanly.%20If%20a%20payout%20has%20more%20than%20one%20currency%2C%20write%20one%20CSV%20row%20per%20currency.%0A5.%20Optional%20weekly%20reminder%3A%20if%20SLACK_WEBHOOK_URL%20is%20set%2C%20every%20Monday%20at%2009%3A00%20in%20TIMEZONE%20post%20%22N%20payouts%20totalling%20X%20not%20yet%20reconciled%2C%20oldest%20from%20%3Cdate%3E%22%2C%20or%20nothing%20if%20there%20are%20none.%20Skip%20entirely%20if%20the%20variable%20is%20unset.%0A6.%20GET%20%2Fhealth%20that%20returns%20200%20and%20%7B%20%22ok%22%3A%20true%20%7D%20and%20nothing%20else.%0A%0ADEFINITION%20OF%20DONE%0A%0A-%20%5B%20%5D%20The%20example%20payload%20above%20creates%20one%20row%20showing%204%2C240.50%20GBP%20with%20estimated%20arrival%2017%20Jan%202024.%0A-%20%5B%20%5D%20The%20same%20event%20delivered%20twice%20creates%20one%20row.%0A-%20%5B%20%5D%20A%20bad%20signature%20returns%20400%20and%20records%20nothing.%0A-%20%5B%20%5D%20Nothing%20in%20the%20database%2C%20page%20or%20logs%20contains%20the%20full%20IBAN%20or%20account%20number%20%E2%80%94%20only%20%225678%22%20style%20last-four%20values.%0A-%20%5B%20%5D%20Ticking%20Reconciled%20saves%20immediately%20and%20survives%20a%20reload%20and%20a%20restart.%0A-%20%5B%20%5D%20The%20CSV%20opens%20in%20a%20spreadsheet%20with%20amounts%20as%20numbers.%0A-%20%5B%20%5D%20A%20payout%20with%20an%20estimated%20arrival%20a%20week%20ago%20and%20not%20reconciled%20is%20highlighted.%0A%0AWHAT%20TO%20GIVE%20ME%20WHEN%20YOU%20ARE%20DONE%0A%0A1.%20The%20public%20webhook%20URL%20(%E2%80%A6%2Fwebhooks%2Fstorekit)%20and%20the%20ledger%20URL.%0A2.%20The%20environment%20variables%20to%20set%20(STOREKIT_WEBHOOK_SECRET%2C%20ADMIN_PASSWORD%2C%20TIMEZONE%2C%20optionally%20SLACK_WEBHOOK_URL)%20and%20where%20to%20set%20them%20on%20this%20platform.%0A3.%20Confirmation%20that%20the%20app%20stays%20up%2024%20hours%20a%20day.%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 private web app that keeps a ledger of the payouts storekit sends to my bank account, so I can reconcile them against my bank statement. storekit tells the app about each payout by webhook. The app must never lose a payout, never record the same one twice, and never make storekit wait.

  Build it with a database so the ledger survives restarts and an always-on public HTTPS URL.

  ```

  ```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: payments.payout.created

  Ignore the order example above; this app only cares about payments.payout.created, which looks like this:

  {
    "event": "payments.payout.created",
    "data": {
      "amounts": [
        { "currency": "GBP", "value": "3210.50" },
        { "currency": "GBP", "value": "1030.00" }
      ],
      "bankAccount": {
        "id": "db97e205-105c-42ff-8460-d06c25cb6830",
        "IBAN": "GB15HBUK40127612345678",
        "accountNumber": "0001234",
        "branchCode": "001234",
        "currency": "GBP"
      },
      "estimatedArrivalDate": "2024-01-17"
    }
  }

  Important differences from the order events:
  - Money here is NOT in minor units. Each value is a decimal string in major units: "3210.50" means 3,210.50. Parse it as a decimal, never as a float you then round — use a decimal library or store it as a string and sum with integer pence.
  - amounts is a list. Sum the values per currency; a payout usually has one currency but do not assume it. Show each currency total on the row.
  - The payload has no payout id and no store name. Use the svix-id header as the unique key for the row.
  - bankAccount contains full bank details. Store only the last four characters of accountNumber (or of IBAN if accountNumber is missing) and the bankAccount.id. Do not store or display the full IBAN or account number anywhere, including logs.
  - estimatedArrivalDate is a date (YYYY-MM-DD) and may be null.

  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 row and return 200 immediately. Ignore any svix-id already saved. Handle payments.payout.created; save and acknowledge any other event type but do nothing with it.
  2. A ledger table with: received time (UTC, display in TIMEZONE), amounts per currency, bank account last four, bank account id, estimated arrival date, reconciled (yes/no), reconciled on, statement reference (free text), notes (free text).
  3. GET / — protected by HTTP basic auth with ADMIN_PASSWORD. A table of payouts newest first with a month filter (default: current month), showing the columns above. Each row has a Reconciled tick-box and editable statement reference and notes fields that save without a page reload. Rows whose estimated arrival date is more than 2 working days in the past and are not reconciled are highlighted amber. Above the table: total paid out this month per currency, total reconciled, total outstanding.
  4. Download CSV for the filtered month, one row per payout, columns: received_date, currency, amount, bank_last4, estimated_arrival, reconciled, reconciled_on, statement_reference, notes. Amounts as plain decimals with two places and no currency symbol so bookkeeping software imports them cleanly. If a payout has more than one currency, write one CSV row per currency.
  5. Optional weekly reminder: if SLACK_WEBHOOK_URL is set, every Monday at 09:00 in TIMEZONE post "N payouts totalling X not yet reconciled, oldest from <date>", or nothing if there are none. Skip entirely if the variable is unset.
  6. GET /health that returns 200 and { "ok": true } and nothing else.

  DEFINITION OF DONE

  - [ ] The example payload above creates one row showing 4,240.50 GBP with estimated arrival 17 Jan 2024.
  - [ ] The same event delivered twice creates one row.
  - [ ] A bad signature returns 400 and records nothing.
  - [ ] Nothing in the database, page or logs contains the full IBAN or account number — only "5678" style last-four values.
  - [ ] Ticking Reconciled saves immediately and survives a reload and a restart.
  - [ ] The CSV opens in a spreadsheet with amounts as numbers.
  - [ ] A payout with an estimated arrival a week ago and not reconciled is highlighted.

  WHAT TO GIVE ME WHEN YOU ARE DONE

  1. The public webhook URL (…/webhooks/storekit) and the ledger URL.
  2. The environment variables to set (STOREKIT_WEBHOOK_SECRET, ADMIN_PASSWORD, TIMEZONE, optionally SLACK_WEBHOOK_URL) and where to set them on this platform.
  3. Confirmation that the app stays up 24 hours a day.
  ```
</Accordion>

## 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 `payments.payout.created` for this endpoint. Use the portal's test option with that event type to create your first row.

## Step 3: Reconcile

When the next real payout lands, compare the row against **Analytics** → **Accounting** → **Payouts** in the dashboard and against your bank statement, then tick it off. Payouts follow your [payout schedule](/docs/payouts/payout-schedule), so the first real row may take a few days.

## Ideas

* **Send to your accountant automatically**: ask the builder to email the month's CSV on the 1st via a provider such as Resend or Postmark.
* **Match by amount**: paste a bank-statement CSV in and let the app suggest matches by amount and date.
* **Multiple stores**: run one endpoint per storekit account and add a "store" label from an environment variable so the ledger can be merged.

## What This Does Not Do

* **It does not explain what is inside a payout.** The event has the totals, not the orders, fees or refunds behind them; use the [fee report](/docs/guides/reports/fee-report) and [transactions report](/docs/guides/reports/transactions-report) for that.
* **It starts from the day you connect it.** Earlier payouts are in the dashboard, not the event stream.
* **It is not proof of payment.** The event says a payout was created; whether and when it arrives is between the payment provider and your bank.
* **It is your service.** storekit support can confirm the event reached your URL and the response code; the ledger and export are yours.

## Related

<CardGroup cols={2}>
  <Card title="payments.payout.created payload" icon="code" href="/docs/developers/webhooks/webhook-events#payments-payout-created">
    The full payload, including the fields this recipe deliberately does not store.
  </Card>

  <Card title="How Payouts Work" icon="building-columns" href="/docs/payouts/overview">
    Timing, fees and where payouts appear in the dashboard.
  </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="Fee Report" icon="receipt" href="/docs/guides/reports/fee-report">
    What was deducted before the payout.
  </Card>
</CardGroup>


## Related topics

- [Build Your Own Tools on storekit](/docs/guides/build/overview.md)
- [Payout Schedule](/docs/payouts/payout-schedule.md)
- [How storekit payouts work: timing, fees, and verification](/docs/payouts/overview.md)
- [Sales and accounting reports in storekit](/docs/guides/reports/overview.md)
- [Refunds & Payouts](/docs/guides/payments/refunds-payouts.md)
