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

# Ticket de cuisine personnalisé sur une imprimante cloud Star

> Ticket cuisine avec bandeau allergènes, code de commande en grand et sections par poste sur imprimante Star CloudPRNT via webhook order.created.v2.

storekit imprime un ticket de cuisine figé. Cette recette vous fournit un second ticket, mis en page comme vous le souhaitez, sur une imprimante Star pilotée par votre propre application. L'impression et le tableau de bord de storekit continuent exactement comme avant.

> [!WARNING]
>
> Il n'y a pas d'éditeur de modèle de ticket personnalisé dans le tableau de bord storekit, et la mise en page des tickets propres à storekit ne peut pas être modifiée. Cette recette contourne cette limitation avec une imprimante que storekit ne connaît pas. Lisez [Ce que cela ne fait pas](#what-this-does-not-do) avant d'acheter du matériel.

## Fonctionnement

1. Un client passe une commande. storekit envoie `order.created.v2` à votre application.
2. Votre application transforme la commande en ticket selon votre mise en page et le place dans une file d'impression.
3. Une imprimante Star CloudPRNT, configurée pour pointer vers votre application plutôt que vers storekit, interroge la file toutes les quelques secondes, imprime le ticket et confirme.

Tout ce qui se passe aux étapes 2 et 3 tourne dans l'application que le builder IA fabrique pour vous. Le protocole CloudPRNT de Star est public et simple — l'imprimante demande « quelque chose à imprimer ? » en HTTP, votre application répond — de sorte que les builders le gèrent bien à condition de leur fournir les règles ci-dessous.

## Ce dont vous avez besoin

* Les webhooks activés sur votre forfait storekit (**Settings** → **Developers**).
* Une imprimante Star Micronics CloudPRNT qui n'est **pas** connectée à storekit : un modèle TSP100IV, mC-Print2, mC-Print3 ou SP742 avec le firmware CloudPRNT. Si vous en avez déjà une connectée sous **Direct Printing**, il vous en faut une seconde — une imprimante ne peut interroger qu'un seul serveur.
* La page de configuration web de l'imprimante, accessible via son adresse IP sur votre réseau (consultez le manuel de l'imprimante pour savoir comment imprimer ses paramètres réseau).
* Un compte [Replit](https://replit.com). Cette recette a besoin d'une URL publique disponible 24 heures sur 24, ainsi que d'une petite base de données, ce que Replit fournit. Lovable ou Bolt fonctionnent si vous y attachez un backend ; voir [Choisir un builder](/docs/fr/guides/build/prompting-guide#choosing-a-builder).

## Étape 1 : construire l'application

Collez l'intégralité du bloc ci-dessous dans le builder en un seul message. Modifiez d'abord la section marquée **MY TICKET LAYOUT** — c'est votre mise en page, et l'exemple n'est qu'un point de départ.

[![Open in Replit](https://replit.com/badge?caption=Open%20in%20Replit)](https://replit.com/?stack=Build\&prompt=JIAg7ghgdgLiEgM4FsIBs3gKYCMlYCcA3ASwGMsQYALCOABwJNkXhDIFdEYB7ZEANYkYZalihVyArHABmPAiCxFCATxAKAJoRCgCWCiRUaJ3BViEwANCbb7D9Bk1g6adXRrBQAdLrgEOKFZAuhhxbU0QZjssbggOAmg4S1FxG0QeKLhaVl4QHEp9NBIIHDRKaEicBWYAcwAuLJBkLjgoZR1GZjhoDQJtRRgwciwbdpVFNB5ECoktHRJZJv04ghhEG0qQcZ1UaSRefUtwCGFvAChzgBUwTJo63PQBKjvqElYZ4hHGsyPhEAAFDMoJo6s1KPMCKwcOowLhqDweAIAJTwEHNdRdFyKAH0HgYMHyRQAKx4OEQyJsWGEYkUoPsMDQ6kWTXebAAyjAIIoAMJTDiaAAKACUAHJXEBYsIEGwKKjUAg8Di1ahsFDoTBS+C1cRwHgSBBkPj0DjSqISZDqFRQDiUIlo9Q8GidZzS3yisk8TTqLBoGYgLixLKbejcuSK-jISipMhCKC1NHwTTIaKhnUaMhkCCIEj6jWqC7nABCHBIaEi-2GNDYmjopWzlAy8soAEdbba0ZFnSA3r91IgEqRjCsuWtWFt9Np6FNVIhfMA2lgsJpxwa0JBZwBafWSjhlcggAASVyugvZIAAqsKADKFnkAeXFAFEABoSgCCRfvF4l7Ku9+FJ8AGlgCuS49ECNgEUQehhHQYR1BwLhmFiVgd1+Cx-iBQ5MJgbwjWQSlE31Yp2j6AYwS2UNVCjFhJTQOgiX4e0R3iRIWBsLNZAAS9XKpuTnA5zGOLMJCgJ1FnUS1PANeh6HgapTXlB55XcWg5PEc0MSE-R8n7cIwThHAESROdLkPHgwB03DsGMxEBFYW4CAEepzk3EAnwgUQlGtOA2V6Y9TxAQV7z-cAaTYAApdlH3yL11DyBAr2vDx9Fqd4zWibsMOOWtEGoapuU0C53KuMQ4u9Hts2bJB1MoABvEAACIOlgJrGiagAeVq4BgVR6CwAA+JqbCa2suXakBGu8GaQAAX3m3wbkyLAAA8KEcXMgkabtylqLzHX6Qh8P0Ohlx83VAU0TJxLgQMsgAbgDf1IROrAzuKogACZUWBFcapwbkISOxQyWJAxrE7GrXv0WRAmKshTrCSIAHpyOO2H4e8AdMyXCIQDRmGsDhkFvFkU5ykiHrWBNPUQc41o+EIGxrQ7Cdifh7Y1t6lbfPNbh3siHglmYHNtBAcaIBKkBhSwNtYnWeBdJzWp2kiKtVXZUhVt8J8JnUfR5e4dhuSYINuzECABg+bXNxITR0ltmASCjOJkHkrZEFtlWoDoBIsF8AA1QhJObfgNeh2RZHIEpMAAAy9khVrjkBihwRICHUe1VCVSZoFqDgIB1GxkPjNUDH0OBQGGDBJUVUhxeiXpxFIRUoFouAiG5EoymjDVzr-ADgNAgB9AB1J8i0Pe97yAkf2SfHlAKuXxZfBsgeigVR7jLtw5Ap1gJkWcg6C28Lq0CwUQAAFgABlv6WAFkfRBPFumaVoQBWPF0QjhAvtWqtA4ftHI0miAARgAKz4CNCCQS7IIDGG7D1F4NVJaA39NHKEkNv76kiF9e+NhnQSGujVJyzxZARhqogRB50jT0ALG5EA94oAUA8LQYcsQf74wIbfdIOFjg10wLdfA6I94XVgNqU4UB0ivEoOQj+xsREFFTtMGAPwcJKC8qqFBf9gEwC4ICAaII6g2Guu0Gw5MyzLlRFsEiUluTPH+OYiosgzTdgUTkeAG9C4YH7BwXGy5ly+BYcAtYHAPboh3AgZ2UYZSSikAGSJ283hl24GWTUBky5yisZTKGldM5ZHdB0OkcjwAKGePYrSUZkAKHUBANxOhcEmPjNLMqjYIBRgkXAUSGYsw5jzH4-IlBtDFAmOdWpuk3BzFYQHS8-psq23tj2QWOhqrdkCCQeWggsBZzlNlWhVNfKNAQPoAa7hhaJhQegU6lUaEqArKwEgqtzAO3ALQMIEwsgfC5AYsy7l3xb0lEXYGShX48HfowJ0ENzowjYKGRAiAnKRBaMbFRnwHkaC+Zfc89inqThIAyPoX8Ob+mnNIo8J5BRP31LskA0dfT-QBLwLkaAiEkHoGY30Rg1AADElxmPeEaQIMArhOnQDYYQWB+BdAoDYWpoIGWKFlVgWxuluhYB1FCapzA5SbPWI0L6kD77gmgKwAAxUa7w99paQiuP1SgbJaUaCWAAEW5frGwgopARJsMAKAnJzA2B5Akck9tRggB5GdZwtR3VjLUMG6NdRvWxgicSosGTqLSy5L3KIaEoBMjrrES69pmCbgwujKET1Rk8szu+TQmgVh8QSbGC89ArguwhAWvZdIPVqErYJMeYgJDOnUKQkREA5KFu7NIZkaEUwwGRrIqgSM2ApkRWCWd1DOkVFYDaDAvhbpBi2FK5AiAADaABdbwR6TYSBUfuzAcppWOALOwRmcT8LGmgKoUUO6oacDMJ+ruMBRQcGQAURQk7zmg27e8zSA4p0kGCZcJ8q1OnTmBgMN6H1vDfWBaoKYVtXLnHqucEAzUeqTSaq9RG71kZ4a+iNcjzVJaTTIxRijTV7bUa0CPUoZBwFfQAMzMc481I02hqPvnAUWJjVgWNcezJOyaMAAijEU81Fl6BJpGsIZppqzt6C6eNQp8TY0+2Z35VgXTABOWzZnONjSFUqWAYrWWTT4QZu1DrqNxtraoMTTnYKtvbZ26jBCvrX03LfcBm4oFXHAbfeo19IH1HvgALSC1x0Q0B2hoGoxPIs2Xmp4jAxBwgk1H2OZy0jZc74YARdvlFmLcWEtJfqMJ5LmWSsWfjZnDtUYmstdi-FyBiXwHpe67fLLNXmpHsQNR4UYJroKAKBgXrgHeBxPY5prjWDuB-qGx1SKPBqBQBK1xhih2d1+Z4DZubXHpUU2o6Sc7AABNaGHyhfuQJd0rCJ2jUYANTX2vkJ4TqWABsAB2AAHLZ-7TVUDORkHUHk+pgSNZ2upzTc05vcbCGeya569tTTJ1x32x3mqP25CqEOXJgokAAF7M4gP9rjUojs2Y6rTgg9OmATUe8FpgFBJpCf0+J8zbYkgIXF8LznaAOBVY4BgBXpXpjAE0CrtXFOtMQFWsKM6umvNS6c0enXbK9fI69MfQgi3Gik7N+JxqTUqc8+amhtThoxDFpGqV103PqM8l9zMf3TUVXi9M81GXsA5eNHAaNacyvGjVY14gLXlvRpckN8bw1t95p68vWTuamnL0E9Zh7jjUvuPa4TyJ6+wu3e3d5+oWWcQEhJCR4gJXtRqOWk3KxTvbUm9W0bahXbzvmrcH0DIcB1GIcgFp9ETkHPp9qaXDAJjHU19NTIPH5q148H6l33ibgGOpMdTHuA98IAb-vl38K2AmdqMXiAo-79W8g+t5lgrNiSQQBrwYBtd1c99ER+hmAzoHcppmoGJnYDFL8QBIFwFvBjUYdG9YD9R0oECPcYtvAhN4dC8zdS9xN8cDMa19Z61x9EVJ8a8Z9N958OpodmEIBnh3wVBu8N8ZBt9mpeU4CQB5Myc98D8moj8QQT8m8z8YAL8PcmonxwEQAod3xRQkcn81NAsOo38kcFss8hCjQtBICwhoDXc4DhABQPdkDUCoFIFRoph4wzDEC8Db4YcvovoiDOMSDC9S9rhyo4Za5ihjZLkUE+oBpm1qIiN-p-JshF16BEB6gUYUYcozgCIUZroyBEBUiOgpgBooQUYjITIHI8j4R7JNxqZLgrhDwnxgphRgBnxhRI1HxRRF4rhgBHxLhb8RRairgnxhQR4rgABNQUKoluQlfUDuEALuJgUocoJACAWcHsSybSKUHQI0DACGXIKQGQOI9fbkJqelOUBATkbkSNfkIUMUCUZYnEHgTaHcBQ9bSyVEOUJqEFNqfYqDIFJ0WkOuDVeJGgRUZUVUBALUF4uAAEa452HcNw+4sAZEXwEsMsKoT4jQG4oIGwQGWMWFBKcqGhLpeWW0NEpSKpM1OEWDaGMiTouovowYqomYcoDeRAJ6bsT4wgAAchpjoGoFYErgSAkDvmvjhKRPugpO6N6P-CAifFFE2FUTLnYmun4BmDo3DD4CuSgFbjGMukmJ7nKElRHTEEJUvBvELGFJ6OYUFBaNigUIBGNNFOpIAF5VhkRGg-x3x6ieRrxvxXURRxRqj7x-wHxrxLhn5vjsQ80ORRwl9yA25yAaZXQdAI4+QlQzjxR5xwpa4QjKB7gPhCBSAKA2SDSUpugylLi8zYFo4C5EgISJA0wI0thuxLjUzMBiFJR8RMAzhdB3ZyhxjspsydAxYIQlgjiCA8yEyBQvSJRL464nQeBVj4BepypQxqwUYyBTjGBYAUZOprS+jZ4JTBobAwBaRKBNyxSJTQyEA7CExZTlSFTK56UqFm5VTRj24NTu5pi5kO0MTFZYEuRogtsmZe1vy-Rl0f0SUjYeYCZlzEzVy4B9zpgMykQENNVxjrogwRFUARBVRUU4AdQ4A+SoZbpUkExfQZg4TfRFjq5BdKAlyVyCBJF7R8oFAYAnp0ANwnlWEldxZuxeBpBTBELdRCxSp5zYzFAQo-xxwkBUBa5opYo4g-kNAcB14wKqLIKaK4ASks4sArIFS8FBJ29uFz5VQpLRRfAh1NJnR1U91PiwQ8TzpmlIhXdSQcBZYrYND6V0B-Q5pjLh1mxzK2AHKSUYI8EYCmoHKnLvRVN1NRooxQQIB7VQiSdDNuYUZyVmAmpy9moHKxVuLqNOpllLluwHLhpRpRkZAsBH4ZAEQ69mpXUnxrwnxui9iFpAAUAhMELRKSqiiSUnyrJFnJqkoX1D1CWG7GsraUEp+ObAkAAHE6rWAlKBQoKAB+dM20sIVaGAJKhiZgAAMi4vEFtM6gyvgqgEGkBH+FQHqT9EyD+maC8mJVLEiHlkKVDESCjGlAZKiBeSmTEEIlXi4UCojk4s2LgGqEqmqmSpHW5mDX6t1E3FiozMSrBt8CmolEwuGRJXORzXKEaG5Jou3S6T8qLgpRZGzGkDeXHSdBJVqWMH+D6v4CGvbADiYXfEaWVVdFMRqnrL+lYGqtqu6PAuotgEWsOr2oOu4sGk2skywD2pWFVxgEGiWnKgltPPzNKNYS9BstiBlo6l4WYXf0BAOPYDVrCWdjLgjh+lNSCCaGWMiGapR2eC6rwCtvyWlR4CpqVNpvKmGpAEBUdDMoNvFijDNXZqEoNtV0iBES1H+BtscRqj8qalyWXD2P+o+T9ojX0EpsdTdpqmsqQCLODrZBEW4ACUqQkGEE2HRHKFoVZF1IqGTFTBBRzo8F6TOVdCKRAG5rqqqJRpUXDXdmhVgBOTRvozeN8u6puUFnUFLMJSitDOjgsX8r0t4WllFDaviG21PizCGX0E3GzoQD8rrODq8VkBkFSCqCUhEUnoICip2lGpDLOqqmMEdv+DsTgymGNm7AvrQq2jLsiC3ussMiVHLGDL8l6mGAoF8CAiXHkn+CbgD26DqD2JksoHhgWEVhqHSl9kbMOvnBFmyGqgKE0iymTsQeHsmQzNoAkDcJTBtCMPSARCsiftYCakCA-oTq0m7CtioeBXTD0Vli1CQkXX1G-tThkHBAlgMHDWlggawHkg8QRBmLxFrjIFUGXKQZBDWRgTwUZJvrNAUb9ANqCC5FgCZGlkHN3H3HynNkEqnJnLUhiLiISNWE3GQG8GJHoCSsVE0ACXWESJHilESM0AEBRkHJHKTKuBRlQBtHQBRnEHcesfxEH2JkIHEAoBRmYG0B1moBgGQEwGavkAwEWMrAPKaEVSPu1Rpu0gHGQBR0YXOGtNNPNNFAEMBCPLtJBMdK9tFH6OYQqJNOtJsAqOFG-AmsPC9uqK6K9qmvFEDMxDztYr7O0krwAzy3JpUVOhPtvOVO7B+PaDwlGeBJ1FgClPVFrihVqGepABzg4HgAuvAAopIdTsCHzUTGYrmMQG3AkAIlpgWGromPEA7ABDHjScsg2GurIFCmJQQCNxgkg0KW9SImJtyF7HwC+DYXtAcrQmxSpXPColdERelXnD1Dgz2CDCVBgFqAhRlLlltG4BBabHwrBB3Gkh2YUXaGXDQmMTBAiULHaWRZzMbHDU2DgBRhBPXKPO3NFGOoBAQDADbgTB2okGwtYFwq2HpbLiIprNYHtFms0CgsdKYSRoJhFY3JqMpOPIlZRnaFWs8tMqKfzssrLmsp-t+vRHsrJFCpcvJj9EoA8pABMurp8t3u6tsqCpCvHvCvxPSrJEyvEGytysGvKgKvDxWux2ak6gBo-IqlUGGnmhAGaqqTavIaFk6oTdHrnLtDbgGqzvpsRrqsUSBsKGkfo1fKxpkB5NxsoHxv2miCJsQBJrMRunJrTpdozo2fdtbGrd-yNH6BqliX7IukKUNmpZ6DxaAdZNYGuwYBbOlhEolGFYObWuNa6J6K3PFPNZcX0pAEMpDajcOuoxmm8HDyRHDe9fgGZqAbZrlFdZwGjYuw6nvcfYEEmk9ZmFGkIEVAIGytdhoR1GzYWhZH+GFUAfDtdF8CRBXQ7ADotqtsaFtpju6sdvZnTqaHKbpqwFtFQ4oTcsoEw9YFw-tuanju1wveQQIHA5sCHddtHarbI8bDKRBNDILoMVjFsFLqhgrs48IZrs4erIbtACbukZbrbIXDrdRp7rxDCH7piHRuHsDbwDHucv0awWnrZFntTudfwRtSYR5ZxMKFV13WHvBLPgUOg2KFiFTxXtND4HXvzBJW3vppHrwHYf3aqk1ePrEFPraHJpYeKhACkagbgAj1ZvjD2OQcUGEDQiYHQfQBeG4toYKeXeamYf1CM9YYaTNEoeYFNFiCEfKDgC6W0H320G5e0e1EunS99FkFcnchYUPJSR3GE3pWsWegZbg2ykMdrGnbTgzlUBDDDC44YWGCgGEzcPtClBG99aBbAEEmU9uSeUVhmEXRUnKZGLbnGM1NfIy7VAkswEnoTGjhmMtbAuECdJ6MDmAB5CfBHmSkBG7EADICPd3UUVk1kUk9nc-M2xdEYUkeR+Oq6eV1bd4H6H2H+8V1ebGQchPYkz-W+wakRwVd0kpl9HipRoa4zSBAK4HkK+WBdoDeM+PITc6eMKHcPENYEAWzJLAvK04H49kKYUCUYd1j8NPboiK69N6QXqbmADV+uZKHmHiolH6fWcInTHzV-W+s0WVlPJHcd+40KrggEni0LycFrYa8SroBWCAaGdyXvINAeSTcSIQ9yk0Ud8GH467cL+CAMAJ6HcQF8Qrb0RCsAryF98MeEAPezITc53mHi9xb5bqUaWPWftAbrSuBJoDevRi17ma1g0MP7qtkW5ZkJwboPbohLy9Pma89iONDtTDsKpfUNhfesa-ZcqUnissERAJX6VbxDaZGNBOsWUUGSjr15j8qMDhQaWZTlG5D9+XTqgUBrAH4Su+2n9vbkL1drsTIBAY5zAe7ygR71BUTpsUdJAQcHlGIVYSGE9JYTP1a9q-6dE4uudmgWCwEAoLMe6BAVI51dPk4ddtRIiH-l-zIgE1ogGsUlu+1aS6Algn-c9j-1yQgtpA0jElOoTBD-ACgRIXdHGATA385ABxfCjoAcrLpnUr9HpMVynqnwdwOwRQHI3+hpo4CrgQGv8hADL0hgFSJQKxwUDNpeggQNZrQFzT1kp+bVMwPF0aBTA5Wo-DgfEkgCid1CUiZgIWGvC1EqiY8YAK6gqKXALwZKDauDVv7oslorBMhpQFIjyJ7Y1YcpggH9IXhH4ooc8Cd3VKSJzuvcRoNfDhw6cJAcOAvFU3x42BluOnJAq4K8GXEB+N8FbvrVMbshBQ6BNwi7FDAbxvBbDbRmuzEbkwZa+xWALrH1hfw7OwyKYFZHEQ0J5iLg82qwEsHWD2QvgTQV2kLTvh2QPIYAHJ1oCJAN49uXWOiDUqSAM2EcHMEAjKDQBngxgj4EWUnQ6Aj6bLVyooHEBKgVQqCMIMcWFiyBCwY8Q8O+AlD-gQARYC8MAGvCupLgKBYKKFF3b5F7IGRJInAGapjdhI-wcIG-HSEgBg4TAWQFiUoBawk4SAZ5L7AMS6RS44g-stHH3zZdE4QCKbtyHqRiJyog8QCCBCuDjxJ408WePPEXjLwwUapJ8g4JfK9xNgXbAxjVESBWRF2CsTNj9U+G8kTULII+H8IoESB4BRItttrRdhRUSgYQQtPX0MHdJQy9yZcGiWJjmASUajGNMSjW5lwf0O8WoPOA+qzB+wSySIJc3voVA0ABfWYg8l8CHhKgMxZBHzHTKVocMDGb6E9ArRbAvIAgcSGAEpjphgKzJCYRqIdT5AlIZNEURezODnAvovgW-PoD5GuBk6xI1cFqLoy4Z8M4RHgFbHNCJQOh4vYbmXEfhdMWiPIcUhKGvDvh+i34CUNCSEZj4V2zsDNnkEb6SJhq5wYTPLUoD1lqeEMM+NoEQCIwSABQSIKUGHZolDRmJbjh2Ek4Sw6wGCBfjVG1ZQUwUOrClorH3KaQWmQxdkaOFL6UV+O1w3sc2mzE9Bguu-eDCXT8jjh92P1OGAskuqjg-ISwQcVUTtaEsn+0EDMrcAuD8kQABrYVrXQkDNUt+dQeRiChsBQowgzQqoOoAnIYIDwq9VUN8K9qupH4tREeIKBqHsgx4AEV1DYACLG0fhqcbMHAGNShitiF7SECnXZRRh+EICQEI6wJgQDsBAYVWlABK6ox1+mExjiP0LFCUSyatCHtWN-x8NTQvAXUu4GsorsbOcEmcdIhdFlsXgnKWqIsX7E-N6yYJQYC1zaZQTjYujc6AaJABnMlwUAFGJOHNCggswhwFthWBwZr9w6LZc6M2I3aKEC8KfFcC6JuYIBBQLdIwr1EBo2iBG9EuAIxLYAZiwxEccmPsEliyc707AMQMJ12hzFwBYApSKcnehPoQYFwSBDW13aWw0A1YcRNjQtra0tgruJ9jjg7ALQVW9rQil6yeiICZGHtfzuUHjDVgtg2k3RpIC6Q7hzxnDexIWEjEgBoxsYwAgmKTGXBZYfI+dglHMlfYN4haYoPsEzK+BTe7QccLpFKE2C3J3ILyK9XCjiwAQRQ5sV9g7KxBYSkaXUFMkl79jlYdDHwPMkKDAsXUIAW0lDHci5YmhY06qDMCep0BOB1rXSORSlSXhBQQxV0jUKqLQkPAmHIBlXSSQ5ETYxFS4O5F6mUBJsbAPwpgEVBWRLktpJhL9IELX0sMKxQ2gCElhfptAqICgM-k5GESdw6XGSKnBQhO10a50bXuVH37GCG6-wOIKn3AEAhpIRY5ZnAFIQMR+cdofqqxXogUpk2y6M5GjkrKhlkE7sHIO8FhLgzsZwmKGZWioAOpOIi01GagA0hUx0M7U9QJ1LIbvBGg-mTIZuGOpNRuawAQOD0X6L+4o00oOoCrP7RqzmoPIFYT0VqITU26NVLWTrP9wpoBAaaE2U1EFDvcgIF4QUHrKTTxgHZTs9WWbJFKWzgobsj2XrNDTzNnZPIK8EWHZCqCnw-uf1IGl0gmyMaAcd3FpAiQfT+klAAEAHFFHNQrgn4WqkgSaiPFFATUWoiAAhHxyogg1V8gJ1Vxsp1hmaOYiAGdlFgthKUACf0Rh7ih-cwogikoGH4pzyouIiYugCbESBM5KxBsL4F5RygjZhSLYPrJDjxhF5M3caq9J3B5NchYIAYWgksz1IG0TaK6IfKoJNpsYXBGAIngOCz4t8tc7YOTRfR9R7xaiGQpRPfRAZjo9AQHAzR+nYzr4HUe8CDHOidRDwh4eoI-EfjZtymvo5GA1gSGUBK8eZKYBvSKlzJgA0AmhFA01ZUdNgDaXggBBABgKIFUCmBVQgoJqBBsXadtljLnqZdMmm4eCKrGCRQD4A2CrmRFSTCRAmoNQ98J7P5izDioAssiGlgBmNyv4ixS5JuCs5eUtmROQSPPImHaIsg0qRoLHngIbyYIXkCNKem2D-pog08xQNnKpDeB85TUNwkvhdLDMLZhcpqBUL5HtAOSVIFRQqjtxpc1GLgZ8YN2MDaKKAoCPKcIythggmo7kPYnnN8B7FxM7kL3IkHyDZgyw9ithb0FPR5lb0DKQBl4mTZaRROEdPyJ4t76pdvkzQW3EqhjLEwk450COE1AABUNS5qCEjIi9CoA-Q7GQUCGDSTVFZ6EamT3pQSLgZ20mRe5EwUtiuQN6aFJd2AqpKLKYQfRt+QtoeItAzUfuPzkCzEpniaxfnOQD2JSsgU2coiFqD+JLg6FbnOwFIqWAABCUcRICagXKLlFGd8NeFqrCgJqXTdyAvCqKihfST4c8CADuV7FkZG+N5E2VOTnK-lBYqsi3XhkLYtIgM+cUWixx7lEg0s1BEUIOmjSWhbC6FRMtDKMBi0kifho-PtEdwto44TZelGq6vSmoHoIwvUD2I7zLIDYjxMioGhh0Jl2DbyhUF0jiQRZC2e8S3VVapT-QB5HpTnzhUDLpFsim1tGA-RJCDsbQf9PlOgnmhhAscFquoABBNRIo0ASgPD1LmjiulIdSRJqoG6npFsH8ziuKh34KA0KvfcplQz1RQAMZSdGVax2SYT1Da5gyNFeEAiigeQXTOwaiM7joiZimq9zNlytQmoJqRYT2fNNdQDssK1LMStoBSHhTGgLIKOcKF9X+riULRGHhlkfA7jggQQGQOx1JRwUjaYnHgAmENAV1FAUHEFO6BWioAywYwTIHCoHA0Ud00sa8MquMED1xVYK20tcreE9CGILSk5WZHODVVeUtRUCK0Uab3heUbdQtd9JADnoQAl6UZj7DxmBS4FwSfDCghsltTGQjoMiHZNLVO0vRAhUkVPKanOo9J0sDdVuus7-oUEWOOfiMBAA2TnUF6yGCIlYFPrN1ozfEcbD0SAxIgPsP2LpCilKsTUWwGyYKqA1brOxKlQEA6WJQA81yjvEHma0Gg4DAQwk2Dbn0crj1XKw-XiY2MdSsBn5oIyIIWzQiEkyI+NHoOgvhXQMZlLo25lgTGamtxWkk+CXySEZNhkKK7BcqqDyot8USvG3DdSQljLIC6voCGMhtGbt1eaeicSFAHLQBJ-FrARWthG5Bly2AQAkwdWDQ7Acc5wk22iuz8qMc21RfLxUIwQ69BVmCnDVJEHKalT5BTCZ9Rt2rDbjWuhzIOjP2C5Sh0xBIv9bYEplCUDVfleiv7ytqpjVExsEzRLHuwzLXpT9DRmIk7Qqbb8tmsLrCjPptVou7ItaewpwnRcnNGWgjliJwnlBEUjdYoMJ14YocfNwG9vBuLBDdkUWIydLUSuS3jseOQsagRlBqBoK-1TA3zV5tMB0MNiH5EFhzV+SGIDR+Tf3s3WL5QwYkEWwGipp5YzTMMBGCIogrmWS83R1sC9ggE1nazhQXTImQyrhDeKmozBe8KwS9ocFRofBdwPJk9wKElCKhKGHWV-nbBwMkGEdU1AUJWKXllRGooXODkZYMsD+C9lEs4wxLVqcS1IH7hwkDBgwzUGlacuWxlxVsBAdbAVlGinZzsbdB9lDCajhrMAkagvNGs9lCMeVKy2oIhDyzHR2tW62-AGKDEwV-QMK25M1HZAlI9+vk8lWsvO324aobOjnVAHaAEBFhyw1YfeFPG2yl8ygyoo00TEXgvagEFdU0V2EFizGzW-MttJ618shGyCWWXAGSisBq4GSYFALGFmnDbIBRc3QCH+5HDTIiRARMIHhZtCaQHo0iWNW+72hDi4Zfif9wgpzUVKQPI9qKXFaDQjN3YBeMKDe4fcvuN4YetOMC2gl-uRrMVqexT0XBnRVU0fg+VO7PkpivcB3SpwO6Ahq5UImEVPBnhzwF4S8OqjYHfA-i-xAE9kEBJAlepueNpIYiPsT2g9JSDRa8FYJsHBofVEpf1f02AD5rC1ue8qMgr-4TbO0qYrePaPVYXMlQ3CmxFDBWkVrG9zofgPjLZDkoYATEC4PmKPCLE8gH9VkFyUCBOqhR64V5u83s1IByg0jMEO0rhCaRQN6wY8VxtpSbhTpZzSdDolWjkoPhZ8S5HQxmEEirhYiFaOiHQE8i5O+oGnhxNOiaghKQUjlQFv8j7thZ+zEtENzyDXQvAERSVPtxkAQSQWOvKprWUyABAFxtgX3gwZBaoAwW2LdEJC2zD0AYW6gb1EIzQN5BiWTQC-npG+Qdd4VHBr5ooAv6sAAQVwYmlXJPp2dSS-B4FmBPiCsJqAQsQ3sIfSCd9kAFhwAmb35lAA\&referrer=docs.storekit.com)

[![](https://mintcdn.com/storekit/IGeB_84apyho3Z6U/images/logos/lovable.svg?fit=max\&auto=format\&n=IGeB_84apyho3Z6U\&q=85\&s=cc21d11769d5e6b686fed56390a88f5e) Build with Lovable](https://lovable.dev/#prompt=I%20want%20a%20small%20web%20service%20that%20prints%20a%20custom%20kitchen%20ticket%20for%20every%20order%20I%20receive%20on%20storekit%2C%20on%20a%20receipt%20printer%20that%20I%20own.%20It%20runs%20unattended%20in%20a%20restaurant%20kitchen%2C%20so%20it%20has%20to%20be%20reliable%20and%20boring%3A%20it%20must%20never%20print%20an%20order%20twice%2C%20never%20lose%20an%20order%20if%20it%20restarts%2C%20and%20never%20make%20storekit%20wait.%0A%0ATwo%20things%20talk%20to%20this%20service%3A%20storekit%20\(sending%20me%20orders%20by%20webhook\)%20and%20my%20printer%20\(polling%20for%20jobs\)%2C%20either%20directly%20if%20it%20is%20a%20Star%20CloudPRNT%20printer%2C%20or%20through%20a%20small%20print%20agent%20on%20a%20computer%20in%20my%20venue%20for%20any%20other%20printer.%20Nobody%20else%20uses%20it%2C%20apart%20from%20me%20checking%20an%20admin%20page%20occasionally.%0A%0ABuild%20it%20with%20a%20database%20so%20the%20queue%20and%20the%20history%20survive%20restarts%20and%20redeploys.%20It%20needs%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%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.%20Two%20exceptions%3A%20the%20legacy%20order.created%20event%20\(do%20not%20use%20it%3B%20use%20order.created.v2\)%20sends%20the%20bare%20order%20object%2C%20and%20the%20order.refund.created%20%2F%20order.refund.succeeded%20%2F%20order.refund.failed%20events%20put%20order%2C%20customer%2C%20venue%20and%20refund%20next%20to%20event%20instead%20of%20inside%20data.%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%20is%20only%20present%20for%20in-store%20orders%3B%20deliveryAddress%20and%20pickUpTime%20only%20for%20delivery%20orders.%20When%20they%20do%20not%20apply%20the%20key%20is%20omitted%2C%20so%20treat%20a%20missing%20key%20the%20same%20as%20null.%20notes%20and%20items%5B%5D.note%20can%20be%20null%20or%20empty.%20customer.companyName%20and%20customer.vatNumber%20appear%20only%20when%20supplied.%0A%0AExample%20order.created.v2%20payload%3A%0A%0A%7B%0A%20%20%22event%22%3A%20%22order.created.v2%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%22Delivery%22%2C%0A%20%20%20%20%22pickUpTime%22%3A%20%222024-01-15T10%3A45%3A00Z%22%2C%0A%20%20%20%20%22channel%22%3A%20%22WEB%22%2C%0A%20%20%20%20%22poNumber%22%3A%20null%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%22printName%22%3A%20%22Margherita%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%2020%2C%0A%20%20%20%20%20%20%20%20%22note%22%3A%20null%2C%0A%20%20%20%20%20%20%20%20%22modifiers%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%7B%20%22name%22%3A%20%22Extra%20cheese%22%2C%20%22printName%22%3A%20%22Cheese%22%2C%20%22price%22%3A%20150%2C%20%22quantity%22%3A%201%2C%20%22plu%22%3A%20null%2C%20%22posId%22%3A%20null%2C%20%22taxRate%22%3A%2020%20%7D%0A%20%20%20%20%20%20%20%20%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%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%22notes%22%3A%20null%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%20PRINTER%20CONNECTION%0A%0AA%20PRINTER_TYPE%20environment%20variable%20says%20how%20my%20printer%20collects%20tickets%3A%20%22star%22%20for%20a%20Star%20CloudPRNT%20printer%20\(option%201%20below\)%20or%20%22agent%22%20for%20any%20other%20printer%2C%20through%20a%20print%20agent%20\(option%202%20below\).%20Build%20both%20options%2C%20backed%20by%20the%20same%20queue%2C%20but%20only%20answer%20on%20the%20one%20PRINTER_TYPE%20selects%3B%20the%20other's%20paths%20return%20404.%20Both%20use%20PRINTER_TOKEN%2C%20a%20long%20random%20secret%20from%20an%20environment%20variable%2C%20in%20their%20URL.%0A%0APRINTER%20OPTION%201%20\(PRINTER_TYPE%3Dstar\)%3A%20STAR%20CLOUDPRNT%20PROTOCOL%0A%0AMy%20printer%20is%20a%20Star%20Micronics%20printer%20with%20CloudPRNT.%20I%20will%20type%20this%20service's%20URL%20into%20the%20printer's%20configuration%20page%2C%20and%20the%20printer%20will%20then%20poll%20it.%20Implement%20the%20server%20side%20of%20Star's%20CloudPRNT%20HTTP%20protocol%20at%20the%20path%20%2Fcloudprnt%2F%3CPRINTER_TOKEN%3E%2C%20where%20PRINTER_TOKEN%20is%20a%20long%20random%20secret%20from%20an%20environment%20variable.%20Tickets%20contain%20customer%20details%2C%20so%20any%20request%20to%20%2Fcloudprnt%20whose%20token%20segment%20does%20not%20match%20must%20get%20404%20and%20nothing%20else.%20Below%20I%20write%20%2Fcloudprnt%20for%20short%3B%20always%20include%20the%20token%20segment.%0A%0A-%20The%20printer%20POSTs%20a%20small%20JSON%20status%20object%20to%20%2Fcloudprnt%20every%20few%20seconds.%20Respond%20with%20JSON.%20When%20there%20is%20nothing%20queued%20respond%20%7B%20%22jobReady%22%3A%20false%20%7D.%20When%20there%20is%20a%20job%20respond%20%7B%20%22jobReady%22%3A%20true%2C%20%22mediaTypes%22%3A%20%5B%22text%2Fplain%22%5D%2C%20%22jobToken%22%3A%20%22%3Cid%20of%20the%20job%3E%22%2C%20%22deleteMethod%22%3A%20%22DELETE%22%20%7D%20%E2%80%94%20only%20ever%20hand%20out%20the%20job%20at%20the%20front%20of%20the%20queue.%0A-%20The%20printer%20then%20GETs%20%2Fcloudprnt%3Ftype%3Dtext%2Fplain%26token%3D%3CjobToken%3E%20\(it%20may%20also%20send%20mac%20or%20uid%20query%20parameters%3B%20ignore%20them\).%20Respond%20with%20the%20ticket%20body%20as%20plain%20text%2C%20Content-Type%20text%2Fplain.%20GET%20must%20be%20repeatable%3A%20return%20the%20same%20job%20again%20if%20asked%2C%20do%20not%20remove%20it%20from%20the%20queue.%0A-%20After%20printing%2C%20the%20printer%20sends%20DELETE%20%2Fcloudprnt%3Ftoken%3D%3CjobToken%3E%26code%3D%3Cresult%3E.%20The%20code%20is%20a%20URL-encoded%20result%3A%20%22200%20OK%22%20\(or%20a%20code%20starting%20with%202\)%20means%20it%20printed%20%E2%80%94%20mark%20the%20job%20printed%20and%20remove%20it%20from%20the%20queue.%20Any%20other%20code%20means%20the%20printer%20could%20not%20print%20it%20%E2%80%94%20mark%20the%20job%20%22failed%22%20with%20that%20code%2C%20remove%20it%20from%20the%20queue%20so%20the%20printer%20is%20not%20stuck%20on%20it%2C%20and%20leave%20it%20in%20the%20admin%20page%20so%20I%20can%20reprint%20it.%20DELETE%20must%20be%20idempotent%3A%20a%20repeat%20for%20a%20job%20already%20confirmed%20is%20fine%2C%20respond%20200.%0A-%20Never%20automatically%20re-queue%20a%20job%20the%20printer%20has%20fetched%20but%20not%20confirmed%3A%20the%20printer%20may%20have%20printed%20it%20and%20only%20lost%20the%20confirmation%2C%20and%20re-queueing%20would%20print%20it%20twice.%20Keep%20it%20in%20a%20%22printing%22%20state%20under%20its%20original%20token.%20If%20it%20has%20been%20in%20that%20state%20for%20more%20than%202%20minutes%2C%20show%20it%20as%20%22unconfirmed%22%20in%20the%20admin%20page%20with%20a%20Reprint%20button%2C%20and%20let%20me%20decide.%0A-%20Keep%20the%20whole%20poll%20cycle%20under%20a%20second%3B%20the%20printer%20polls%20constantly.%0A-%20Star%20publishes%20the%20protocol%20at%20https%3A%2F%2Fstar-m.jp%2Fproducts%2Fs_print%2Fsdk%2FStarCloudPRNT%2Fmanual%2Fen%2Fprotocol-reference%2Findex.html%20%E2%80%94%20follow%20it%20where%20it%20differs%20from%20my%20summary.%0A%0APRINTER%20OPTION%202%20\(PRINTER_TYPE%3Dagent\)%3A%20ANY%20OTHER%20PRINTER%2C%20THROUGH%20A%20PRINT%20AGENT%0A%0AMy%20printer%20is%20inside%20my%20venue%20and%20cannot%20be%20reached%20from%20the%20internet.%20A%20print%20agent%2C%20a%20small%20program%20you%20also%20write%20for%20me%2C%20runs%20on%20an%20always-on%20computer%20in%20the%20venue%20\(Windows%2C%20macOS%20or%20a%20Raspberry%20Pi\)%2C%20asks%20this%20service%20for%20jobs%20over%20HTTPS%20and%20prints%20them.%20It%20only%20makes%20outgoing%20requests%2C%20so%20nothing%20on%20my%20network%20needs%20opening%20up.%0A%0AThe%20service%20side%2C%20at%20%2Fagent%2F%3CPRINTER_TOKEN%3E%20\(a%20wrong%20token%20gets%20404%20and%20nothing%20else%2C%20as%20for%20%2Fcloudprnt\)%3A%0A-%20GET%20%2Fagent%2F%3CPRINTER_TOKEN%3E%2Fnext.%20When%20there%20is%20nothing%20queued%20respond%20%7B%20%22jobReady%22%3A%20false%20%7D.%20When%20there%20is%20a%20job%20respond%20%7B%20%22jobReady%22%3A%20true%2C%20%22jobToken%22%3A%20%22%3Cid%20of%20the%20job%3E%22%2C%20%22text%22%3A%20%22%3Cthe%20ticket%20body%3E%22%20%7D%20%E2%80%94%20only%20ever%20hand%20out%20the%20job%20at%20the%20front%20of%20the%20queue.%20GET%20must%20be%20repeatable%3A%20return%20the%20same%20job%20again%20if%20asked%2C%20do%20not%20remove%20it%20from%20the%20queue.%20Record%20the%20time%20of%20every%20request%20as%20the%20printer's%20last%20poll.%0A-%20POST%20%2Fagent%2F%3CPRINTER_TOKEN%3E%2Fdone%20with%20JSON%20%7B%20%22jobToken%22%3A%20%22...%22%2C%20%22ok%22%3A%20true%20%7D%20after%20printing%2C%20or%20%7B%20%22jobToken%22%3A%20%22...%22%2C%20%22ok%22%3A%20false%2C%20%22error%22%3A%20%22%3Cmessage%3E%22%20%7D%20if%20it%20could%20not%20print.%20ok%20true%20means%20printed%3A%20mark%20the%20job%20printed%20and%20remove%20it%20from%20the%20queue.%20ok%20false%20means%20mark%20the%20job%20%22failed%22%20with%20the%20error%2C%20remove%20it%20from%20the%20queue%20so%20the%20agent%20is%20not%20stuck%20on%20it%2C%20and%20leave%20it%20in%20the%20admin%20page%20so%20I%20can%20reprint%20it.%20It%20must%20be%20idempotent%3A%20a%20repeat%20for%20a%20job%20already%20confirmed%20is%20fine%2C%20respond%20200.%0A-%20The%20same%20rule%20as%20for%20option%201%20applies%3A%20never%20automatically%20re-queue%20a%20job%20the%20agent%20has%20fetched%20but%20not%20confirmed.%20Keep%20it%20%22printing%22%20under%20its%20original%20token%2C%20show%20it%20as%20%22unconfirmed%22%20after%202%20minutes%2C%20and%20let%20me%20decide.%0A%0AThe%20print%20agent%20itself%3A%0A-%20One%20Python%203%20file%20using%20only%20the%20standard%20library%2C%20apart%20from%20pywin32%20for%20printing%20on%20Windows.%20It%20reads%20its%20settings%20from%20environment%20variables%20or%20a%20small%20config%20file%20next%20to%20it%3A%20SERVICE_URL%20\(the%20%E2%80%A6%2Fagent%2F%3CPRINTER_TOKEN%3E%20URL\)%20and%20PRINT_METHOD.%0A-%20PRINT_METHOD%20%22network%22%20is%20for%20a%20receipt%20printer%20on%20my%20network%3A%20open%20a%20TCP%20connection%20to%20PRINTER_HOST%20on%20port%209100%20\(PRINTER_PORT%20overrides%20it\)%2C%20send%20the%20ticket%20text%20and%20close.%20PRINT_METHOD%20%22system%22%20is%20for%20a%20printer%20installed%20on%20the%20computer%3A%20on%20macOS%20and%20Linux%20pipe%20the%20text%20to%20lp%20-d%20%3CPRINTER_NAME%3E%20-o%20raw%3B%20on%20Windows%20send%20it%20as%20a%20RAW%20job%20to%20PRINTER_NAME%20with%20win32print.%0A-%20Every%203%20seconds%20it%20calls%20%2Fnext.%20When%20a%20job%20is%20ready%20it%20prints%20it%2C%20then%20calls%20%2Fdone%20with%20ok%20true%20only%20once%20the%20printer%20or%20the%20operating%20system%20accepted%20the%20data%2C%20or%20ok%20false%20with%20the%20error.%0A-%20It%20must%20not%20print%20a%20job%20twice%3A%20save%20the%20jobTokens%20it%20has%20printed%20to%20a%20small%20file%20next%20to%20it%2C%20so%20they%20survive%20a%20restart%2C%20and%20if%20%2Fnext%20hands%20back%20one%20of%20those%20\(because%20a%20%2Fdone%20call%20was%20lost\)%2C%20call%20%2Fdone%20again%20without%20printing.%20If%20a%20%2Fdone%20call%20fails%2C%20keep%20retrying%20it%20before%20asking%20%2Fnext%20for%20another%20job%2C%20so%20one%20lost%20confirmation%20never%20holds%20up%20later%20tickets.%0A-%20Network%20errors%20and%20an%20unreachable%20printer%20must%20never%20stop%20it%3A%20log%20the%20error%2C%20wait%2C%20try%20again.%0A%0ALINE%20WIDTH%0A%0AUse%20plain%20text%20jobs.%20Take%20the%20line%20width%20from%20a%20COLUMNS%20environment%20variable%3A%2048%20for%20an%2080%20mm%20printer%2C%2032%20for%20a%2058%20mm%20printer%2C%20or%2042%20for%20a%20Star%20SP742%20impact%20printer%2C%20in%20the%20printer's%20default%20font.%20Every%20rule%20below%20that%20says%2048%20means%20COLUMNS.%20Use%20only%20ASCII%20characters.%20End%20every%20ticket%20with%20six%20blank%20lines%20so%20the%20paper%20feeds%20far%20enough%20to%20tear%20off.%0A%0AWHAT%20TO%20BUILD%0A%0A1.%20POST%20%2Fwebhooks%2Fstorekit%20%E2%80%94%20the%20storekit%20endpoint.%20Verify%20the%20Svix%20signature%20using%20the%20official%20svix%20library%20and%20the%20STOREKIT_WEBHOOK_SECRET%20environment%20variable%2C%20against%20the%20raw%20request%20body.%20Return%20400%20if%20verification%20fails.%20Return%20200%20immediately%20once%20the%20event%20is%20saved%2C%20before%20rendering%20or%20printing%20anything.%20Ignore%20any%20svix-id%20you%20have%20already%20saved.%20Handle%20the%20event%20type%20order.created.v2%3B%20store%20and%20acknowledge%20any%20other%20event%20type%20but%20do%20nothing%20with%20it.%0A2.%20A%20renderer%20that%20turns%20an%20order.created.v2%20payload%20into%20a%20ticket%20using%20MY%20TICKET%20LAYOUT%20below%2C%20and%20adds%20the%20ticket%20to%20the%20print%20queue.%0A3.%20The%20printer%20connection%20described%20above%2C%20backed%20by%20the%20queue%20in%20the%20database%3A%20the%20%2Fcloudprnt%20endpoints%20when%20PRINTER_TYPE%20is%20star%2C%20the%20%2Fagent%20endpoints%20and%20the%20print%20agent%20file%20when%20it%20is%20agent.%20Refuse%20to%20start%20if%20PRINTER_TYPE%20is%20not%20one%20of%20those%20two.%0A4.%20GET%20%2Fadmin%20%E2%80%94%20a%20single%20page%2C%20protected%20by%20HTTP%20basic%20auth%20using%20ADMIN_PASSWORD%2C%20listing%20the%20last%2050%20tickets%20with%20order%20code%2C%20time%2C%20status%20\(queued%20%2F%20printing%20%2F%20unconfirmed%20%2F%20printed%20%2F%20failed%20with%20the%20printer's%20code\)%20and%20a%20Reprint%20button%20that%20queues%20the%20same%20ticket%20again.%20At%20the%20top%2C%20show%20when%20the%20printer%20\(or%20the%20print%20agent\)%20last%20polled%20and%20a%20green%2Fred%20indicator%3A%20red%20if%20it%20has%20not%20polled%20in%20the%20last%2060%20seconds.%20Also%20a%20Print%20test%20ticket%20button%20that%20queues%20a%20ticket%20with%20fake%20data%20so%20I%20can%20check%20the%20layout%20without%20a%20real%20order.%0A5.%20GET%20%2Fhealth%20that%20returns%20200%20and%20%7B%20%22ok%22%3A%20true%20%7D%20and%20nothing%20else%3B%20keep%20the%20queue%20length%20and%20last%20poll%20time%20on%20%2Fadmin%20only.%0A%0AMY%20TICKET%20LAYOUT%0A%0ARender%20every%20ticket%20exactly%20like%20this.%20Lines%20are%20COLUMNS%20characters%20wide%20\(48%20in%20the%20examples\).%20Centre%20text%20where%20shown.%20Use%20rows%20of%20%3D%20and%20-%20characters%20as%20separators.%20Where%20I%20write%20UPPER%20CASE%20below%20I%20mean%20print%20it%20in%20upper%20case.%0A%0A-%20Line%201%3A%20a%20full%20row%20of%20%3D%0A-%20Line%202%3A%20the%20order%20code%20\(data.code\)%20centred%2C%20printed%20on%20its%20own%20line%20and%20repeated%20on%20the%20next%20line%20so%20it%20stands%20out%20\(my%20printer%20cannot%20do%20large%20fonts%20in%20plain%20text%2C%20so%20repetition%20is%20the%20emphasis\).%0A-%20Line%203%3A%20the%20order%20type%2C%20centred%2C%20mapped%20exactly%20like%20this%3A%20Delivery%20-%3E%20%22DELIVERY%22%2C%20CateringDelivery%20-%3E%20%22CATERING%20DELIVERY%22%2C%20Pickup%20-%3E%20%22PICKUP%22%2C%20CateringPickup%20-%3E%20%22CATERING%20PICKUP%22%2C%20Curbside%20-%3E%20%22CURBSIDE%22%2C%20InStore%20-%3E%20table.name%20in%20upper%20case%20\(e.g.%20%22TABLE%205%22\)%20or%20%22IN%20STORE%22%20if%20table%20is%20null%2C%20Billpay%20-%3E%20%22BILL%20PAYMENT%22%2C%20anything%20else%20-%3E%20the%20raw%20value%20in%20upper%20case.%20For%20Delivery%20and%20CateringDelivery%2C%20then%20print%20on%20following%20lines%20the%20delivery%20address%20\(deliveryAddress.street1%2C%20street2%20if%20not%20empty%2C%20postCode\)%20and%20customer.phone.%0A-%20Line%204%3A%20%22Ordered%20%3CHH%3AMM%3E%22%20from%20createdAt%20in%20the%20venue's%20local%20time.%20If%20asap%20is%20false%2C%20add%20%22FOR%20%3CHH%3AMM%3E%22%20from%20deliveryTime%20on%20the%20same%20line%2C%20right-aligned.%20If%20asap%20is%20true%2C%20add%20%22ASAP%22%20instead.%0A-%20Line%205%3A%20a%20full%20row%20of%20-%0A-%20Then%20the%20items.%20For%20each%20item%3A%20quantity%2C%20space%2C%20item%20name%20in%20upper%20case%2C%20e.g.%20%222%20%20MARGHERITA%22.%20Underneath%2C%20each%20modifier%20indented%20by%20five%20spaces%20with%20a%20leading%20%22-%20%22%20e.g.%20%22%20%20%20%20%20-%20Extra%20basil%22.%20If%20an%20item's%20note%20field%20has%20text%20in%20it%2C%20print%20it%20indented%20under%20its%20modifiers%20prefixed%20with%20%22**%20%22.%20One%20blank%20line%20between%20items.%0A-%20Then%20a%20full%20row%20of%20-%0A-%20If%20data.notes%20or%20any%20item's%20note%20contains%20the%20word%20%22allergy%22%20or%20%22allergic%22%20\(any%20case\)%2C%20print%20three%20lines%3A%20a%20row%20of%20!%2C%20then%20%22!!%20%20ALLERGY%20-%20SEE%20NOTES%20%20!!%22%20centred%2C%20then%20a%20row%20of%20!.%20Then%20print%20data.notes%20in%20full%20when%20present%2C%20wrapped%20to%2048%20characters.%20If%20data.notes%20is%20present%20but%20nothing%20mentions%20allergies%2C%20print%20%22Notes%3A%22%20followed%20by%20the%20wrapped%20notes.%20If%20there%20are%20no%20order%20notes%2C%20print%20nothing%20else%20here.%0A-%20Then%20a%20full%20row%20of%20-%0A-%20Then%20the%20customer's%20first%20name%20and%20last%20initial%20only%20\(%22Jane%20D.%22\)%2C%20the%20item%20count%20\(%223%20items%22\)%20and%20the%20total%20formatted%20from%20minor%20units%20with%20the%20currency%20code%20from%20a%20CURRENCY%20environment%20variable%20\(%22Total%2025.00%20GBP%22\).%20Do%20not%20guess%20a%20default%3A%20if%20CURRENCY%20or%20TIMEZONE%20is%20unset%2C%20refuse%20to%20start%20and%20log%20a%20clear%20message.%20No%20email%2C%20no%20full%20surname.%0A-%20Last%20line%3A%20a%20full%20row%20of%20%3D%2C%20then%20six%20blank%20lines.%0A%0ADEFINITION%20OF%20DONE%0A%0A-%20%5B%20%5D%20A%20signed%20order.created.v2%20event%20queues%20exactly%20one%20ticket%2C%20and%20returns%20200%20in%20under%20one%20second.%0A-%20%5B%20%5D%20The%20same%20event%20sent%20twice%20queues%20one%20ticket%2C%20not%20two.%0A-%20%5B%20%5D%20A%20request%20with%20a%20bad%20signature%20returns%20400%20and%20queues%20nothing.%0A-%20%5B%20%5D%20%2Fcloudprnt%20\(star\)%20or%20%2Fagent%2F%3CPRINTER_TOKEN%3E%2Fnext%20\(agent\)%20returns%20jobReady%20false%20when%20the%20queue%20is%20empty%20and%20hands%20out%20one%20job%20at%20a%20time%20when%20it%20is%20not.%20A%20wrong%20PRINTER_TOKEN%20gets%20404%2C%20and%20so%20does%20the%20path%20of%20the%20option%20PRINTER_TYPE%20did%20not%20select.%0A-%20%5B%20%5D%20A%20DELETE%20with%20a%20non-success%20code%20\(star\)%20or%20a%20%2Fdone%20with%20ok%20false%20\(agent\)%20marks%20the%20job%20failed%2C%20not%20printed%2C%20and%20it%20can%20be%20reprinted%20from%20%2Fadmin.%0A-%20%5B%20%5D%20With%20PRINTER_TYPE%20agent%2C%20the%20print%20agent%20prints%20the%20test%20ticket%20on%20my%20printer%2C%20the%20job%20shows%20printed%2C%20and%20a%20lost%20%2Fdone%20does%20not%20print%20it%20a%20second%20time.%0A-%20%5B%20%5D%20A%20job%20fetched%20but%20never%20confirmed%20is%20shown%20as%20unconfirmed%2C%20and%20is%20not%20printed%20again%20unless%20I%20click%20Reprint.%0A-%20%5B%20%5D%20Restarting%20the%20service%20does%20not%20lose%20queued%20or%20historical%20tickets.%0A-%20%5B%20%5D%20%2Fadmin%20shows%20tickets%2C%20printer%20status%20and%20allows%20reprints%20and%20a%20test%20ticket.%0A-%20%5B%20%5D%20The%20example%20payload%20in%20the%20context%20renders%20with%20a%20DELIVERY%20line%20followed%20by%20%22456%20Oak%20Ave%22%2C%20%22Flat%202%22%2C%20%22E1%206AN%22%20and%20the%20phone%20number%2C%20then%20%221%20%20MARGHERITA%20PIZZA%22%20with%20%22%20%20%20%20%20-%20Extra%20cheese%22%20under%20it%2C%20%22Notes%3A%20Ring%20doorbell%22%2C%20%22John%20D.%22%20and%20%22Total%2025.00%20GBP%22%2C%20and%20no%20allergy%20banner.%0A-%20%5B%20%5D%20A%20payload%20whose%20notes%20read%20%22Severe%20nut%20allergy%22%20renders%20the%20allergy%20banner.%0A%0AWHAT%20TO%20GIVE%20ME%20WHEN%20YOU%20ARE%20DONE%0A%0A1.%20The%20public%20URL%20of%20the%20service%2C%20and%20the%20exact%20URLs%20I%20will%20paste%3A%20the%20storekit%20webhook%20URL%20\(%E2%80%A6%2Fwebhooks%2Fstorekit\)%2C%20and%20either%20the%20printer%20URL%20for%20a%20Star%20printer%20\(%E2%80%A6%2Fcloudprnt%2F%3CPRINTER_TOKEN%3E\)%20or%20the%20SERVICE_URL%20for%20the%20print%20agent%20\(%E2%80%A6%2Fagent%2F%3CPRINTER_TOKEN%3E\).%0A2.%20The%20environment%20variables%20I%20must%20set%20\(STOREKIT_WEBHOOK_SECRET%2C%20ADMIN_PASSWORD%2C%20PRINTER_TYPE%2C%20PRINTER_TOKEN%2C%20COLUMNS%2C%20CURRENCY%2C%20TIMEZONE%20for%20the%20venue's%20local%20time%2C%20and%20anything%20else%20you%20added\)%20and%20where%20to%20set%20them%20on%20this%20platform.%0A3.%20How%20to%20confirm%20it%20is%20running%20always-on%2C%20not%20sleeping%20between%20requests.%0A4.%20A%20one-paragraph%20explanation%20of%20how%20to%20test%20it%20end%20to%20end%20before%20I%20connect%20the%20real%20printer.%0A5.%20If%20PRINTER_TYPE%20is%20agent%3A%20the%20print%20agent%20file%20to%20download%2C%20its%20settings%2C%20the%20command%20to%20run%20it%20on%20Windows%2C%20macOS%20and%20a%20Raspberry%20Pi%2C%20and%20how%20to%20make%20it%20start%20by%20itself%20when%20the%20computer%20starts%20\(Task%20Scheduler%20on%20Windows%2C%20launchd%20on%20macOS%2C%20systemd%20on%20Linux\).%0A)

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.

> **Prompt complet (cliquez pour développer)**
>
> ```text theme={null}
> I want a small web service that prints a custom kitchen ticket for every order I receive on storekit, on a Star CloudPRNT receipt printer that I own. It runs unattended in a restaurant kitchen, so it has to be reliable and boring: it must never print an order twice, never lose an order if it restarts, and never make storekit wait.
>
> Two things talk to this service: storekit (sending me orders by webhook) and my Star printer (polling for jobs). Nobody else uses it, apart from me checking an admin page occasionally.
>
> Build it with a database so the queue and the history survive restarts and redeploys. It needs 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 has the shape { "event": "<event type>", "data": { ... } }. Two exceptions: the legacy order.created event (do not use it; use order.created.v2) sends the bare order object, and the order.refund.created / order.refund.succeeded / order.refund.failed events put order, customer, venue and refund next to event instead of inside 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 is only present for in-store orders; deliveryAddress and pickUpTime only for delivery orders. When they do not apply the key is omitted, so treat a missing key the same as null. notes and items[].note can be null or empty. customer.companyName and customer.vatNumber appear only when supplied.
>
> Example order.created.v2 payload:
>
> {
>   "event": "order.created.v2",
>   "data": {
>     "id": "ord_abc123",
>     "code": "A1B2",
>     "asap": true,
>     "total": 2500,
>     "tip": 250,
>     "deliveryFee": 299,
>     "discountTotal": 0,
>     "orderType": "Delivery",
>     "pickUpTime": "2024-01-15T10:45:00Z",
>     "channel": "WEB",
>     "poNumber": null,
>     "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",
>         "printName": "Margherita",
>         "price": 1200,
>         "quantity": 1,
>         "plu": null,
>         "posId": null,
>         "taxRate": 20,
>         "note": null,
>         "modifiers": [
>           { "name": "Extra cheese", "printName": "Cheese", "price": 150, "quantity": 1, "plu": null, "posId": null, "taxRate": 20 }
>         ]
>       }
>     ],
>     "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 }
>       }
>     },
>     "deliveryAddress": {
>       "street1": "456 Oak Ave",
>       "street2": "Flat 2",
>       "city": "London",
>       "postCode": "E1 6AN",
>       "country": "UK",
>       "notes": null,
>       "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 PRINTER: STAR CLOUDPRNT PROTOCOL
>
> My printer is a Star Micronics printer with CloudPRNT. I will type this service's URL into the printer's configuration page, and the printer will then poll it. Implement the server side of Star's CloudPRNT HTTP protocol at the path /cloudprnt/<PRINTER_TOKEN>, where PRINTER_TOKEN is a long random secret from an environment variable. Tickets contain customer details, so any request to /cloudprnt whose token segment does not match must get 404 and nothing else. Below I write /cloudprnt for short; always include the token segment.
>
> - The printer POSTs a small JSON status object to /cloudprnt every few seconds. Respond with JSON. When there is nothing queued respond { "jobReady": false }. When there is a job respond { "jobReady": true, "mediaTypes": ["text/plain"], "jobToken": "<id of the job>", "deleteMethod": "DELETE" } — only ever hand out the job at the front of the queue.
> - The printer then GETs /cloudprnt?type=text/plain&token=<jobToken> (it may also send mac or uid query parameters; ignore them). Respond with the ticket body as plain text, Content-Type text/plain. GET must be repeatable: return the same job again if asked, do not remove it from the queue.
> - After printing, the printer sends DELETE /cloudprnt?token=<jobToken>&code=<result>. The code is a URL-encoded result: "200 OK" (or a code starting with 2) means it printed — mark the job printed and remove it from the queue. Any other code means the printer could not print it — mark the job "failed" with that code, remove it from the queue so the printer is not stuck on it, and leave it in the admin page so I can reprint it. DELETE must be idempotent: a repeat for a job already confirmed is fine, respond 200.
> - Never automatically re-queue a job the printer has fetched but not confirmed: the printer may have printed it and only lost the confirmation, and re-queueing would print it twice. Keep it in a "printing" state under its original token. If it has been in that state for more than 2 minutes, show it as "unconfirmed" in the admin page with a Reprint button, and let me decide.
> - Keep the whole poll cycle under a second; the printer polls constantly.
> - Star publishes the protocol at https://star-m.jp/products/s_print/sdk/StarCloudPRNT/manual/en/protocol-reference/index.html — follow it where it differs from my summary.
>
> Use plain text jobs. Take the line width from a COLUMNS environment variable: 48 for an 80 mm thermal printer (TSP100IV, mC-Print3), 42 for an SP742, or 32 for a 58 mm printer (mC-Print2), in the printer's default font. Every rule below that says 48 means COLUMNS. Use only ASCII characters. End every ticket with six blank lines so the paper feeds far enough to tear off.
>
> WHAT TO BUILD
>
> 1. POST /webhooks/storekit — the storekit endpoint. Verify the Svix signature using the official svix library and the STOREKIT_WEBHOOK_SECRET environment variable, against the raw request body. Return 400 if verification fails. Return 200 immediately once the event is saved, before rendering or printing anything. Ignore any svix-id you have already saved. Handle the event type order.created.v2; store and acknowledge any other event type but do nothing with it.
> 2. A renderer that turns an order.created.v2 payload into a ticket using MY TICKET LAYOUT below, and adds the ticket to the print queue.
> 3. The /cloudprnt endpoints described above, backed by the queue in the database.
> 4. GET /admin — a single page, protected by HTTP basic auth using ADMIN_PASSWORD, listing the last 50 tickets with order code, time, status (queued / printing / unconfirmed / printed / failed with the printer's code) and a Reprint button that queues the same ticket again. At the top, show when the printer last polled and a green/red indicator: red if it has not polled in the last 60 seconds. Also a Print test ticket button that queues a ticket with fake data so I can check the layout without a real order.
> 5. GET /health that returns 200 and { "ok": true } and nothing else; keep the queue length and last poll time on /admin only.
>
> MY TICKET LAYOUT
>
> Render every ticket exactly like this. Lines are COLUMNS characters wide (48 in the examples). Centre text where shown. Use rows of = and - characters as separators. Where I write UPPER CASE below I mean print it in upper case.
>
> - Line 1: a full row of =
> - Line 2: the order code (data.code) centred, printed on its own line and repeated on the next line so it stands out (my printer cannot do large fonts in plain text, so repetition is the emphasis).
> - Line 3: the order type, centred, mapped exactly like this: Delivery -> "DELIVERY", CateringDelivery -> "CATERING DELIVERY", Pickup -> "PICKUP", CateringPickup -> "CATERING PICKUP", Curbside -> "CURBSIDE", InStore -> table.name in upper case (e.g. "TABLE 5") or "IN STORE" if table is null, Billpay -> "BILL PAYMENT", anything else -> the raw value in upper case. For Delivery and CateringDelivery, then print on following lines the delivery address (deliveryAddress.street1, street2 if not empty, postCode) and customer.phone.
> - Line 4: "Ordered <HH:MM>" from createdAt in the venue's local time. If asap is false, add "FOR <HH:MM>" from deliveryTime on the same line, right-aligned. If asap is true, add "ASAP" instead.
> - Line 5: a full row of -
> - Then the items. For each item: quantity, space, item name in upper case, e.g. "2  MARGHERITA". Underneath, each modifier indented by five spaces with a leading "- " e.g. "     - Extra basil". If an item's note field has text in it, print it indented under its modifiers prefixed with "** ". One blank line between items.
> - Then a full row of -
> - If data.notes or any item's note contains the word "allergy" or "allergic" (any case), print three lines: a row of !, then "!!  ALLERGY - SEE NOTES  !!" centred, then a row of !. Then print data.notes in full when present, wrapped to 48 characters. If data.notes is present but nothing mentions allergies, print "Notes:" followed by the wrapped notes. If there are no order notes, print nothing else here.
> - Then a full row of -
> - Then the customer's first name and last initial only ("Jane D."), the item count ("3 items") and the total formatted from minor units with the currency code from a CURRENCY environment variable ("Total 25.00 GBP"). Do not guess a default: if CURRENCY or TIMEZONE is unset, refuse to start and log a clear message. No email, no full surname.
> - Last line: a full row of =, then six blank lines.
>
> DEFINITION OF DONE
>
> - [ ] A signed order.created.v2 event queues exactly one ticket, and returns 200 in under one second.
> - [ ] The same event sent twice queues one ticket, not two.
> - [ ] A request with a bad signature returns 400 and queues nothing.
> - [ ] /cloudprnt returns jobReady false when the queue is empty and hands out one job at a time when it is not. A wrong PRINTER_TOKEN gets 404.
> - [ ] A DELETE with a non-success code marks the job failed, not printed, and it can be reprinted from /admin.
> - [ ] A job fetched but never confirmed is shown as unconfirmed, and is not printed again unless I click Reprint.
> - [ ] Restarting the service does not lose queued or historical tickets.
> - [ ] /admin shows tickets, printer status and allows reprints and a test ticket.
> - [ ] The example payload in the context renders with a DELIVERY line followed by "456 Oak Ave", "Flat 2", "E1 6AN" and the phone number, then "1  MARGHERITA PIZZA" with "     - Extra cheese" under it, "Notes: Ring doorbell", "John D." and "Total 25.00 GBP", and no allergy banner.
> - [ ] A payload whose notes read "Severe nut allergy" renders the allergy banner.
>
> WHAT TO GIVE ME WHEN YOU ARE DONE
>
> 1. The public URL of the service, and the exact two URLs I will paste: the storekit webhook URL (…/webhooks/storekit) and the printer URL (…/cloudprnt/<PRINTER_TOKEN>).
> 2. The environment variables I must set (STOREKIT_WEBHOOK_SECRET, ADMIN_PASSWORD, PRINTER_TOKEN, COLUMNS, CURRENCY, TIMEZONE for the venue's local time, and anything else you added) and where to set them on this platform.
> 3. How to confirm it is running always-on, not sleeping between requests.
> 4. A one-paragraph explanation of how to test it end to end before I connect the real printer.
> ```

Le builder posera des questions ou vous rendra une application fonctionnelle. Corrigez les problèmes en collant l'erreur exacte, en suivant [Quand le builder se trompe](/docs/fr/guides/build/prompting-guide#when-the-builder-gets-it-wrong). Avant de passer à la suite, ouvrez `/admin`, cliquez sur le bouton de ticket de test, et lisez le texte du ticket rendu dans la file — vous pouvez encore ajuster la mise en page par des messages de suivi maintenant, avant qu'une imprimante ne soit impliquée.

## Étape 2 : connecter storekit

**1. Open the webhooks portal**

In the dashboard sidebar, go to **Settings** → **Webhooks**. If your plan includes webhooks and they are not yet enabled, click **Enable Webhooks**. If the page shows **Upgrade to enable webhooks**, [contact support](/docs/getting-started/contact-support) to discuss adding webhooks to your plan.

**2. 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.

**3. 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.

**4. 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.

Cochez uniquement `order.created.v2` pour cet endpoint. L'URL de webhook est celle qui se termine par `/webhooks/storekit`.

## Étape 3 : pointer l'imprimante vers votre application

**1. Ouvrez la page web propre à l'imprimante Star**

Imprimez les paramètres réseau de l'imprimante (la méthode varie selon le modèle — généralement en maintenant le bouton de feed enfoncé à la mise sous tension) et ouvrez l'adresse IP affichée dans un navigateur sur le même réseau.

**2. Saisissez l'URL de votre application**

Sur la page web de l'imprimante (l'interface de Star, pas le tableau de bord storekit), trouvez les paramètres CloudPRNT et activez-les. Saisissez l'URL d'imprimante fournie par votre builder (celle qui contient `/cloudprnt/` suivi d'un long token — gardez ce token privé, c'est lui qui empêche quiconque d'autre de lire vos tickets) comme URL de serveur, définissez un intervalle de polling de quelques secondes et enregistrez. Les noms exacts des champs varient selon le modèle et le firmware ; le manuel Star de votre imprimante fait référence.

**3. Vérifiez que l'imprimante interroge bien**

Rechargez `/admin`. L'indicateur d'imprimante passe au vert dès l'arrivée du premier polling. S'il reste rouge, c'est que l'imprimante n'atteint pas votre URL — vérifiez que l'URL est en `https://`, que vous avez enregistré les paramètres, et que l'imprimante a accès à Internet.

**4. Imprimez un ticket de test**

Cliquez sur le bouton de ticket de test dans `/admin`. L'imprimante le récupère lors de son prochain polling.

**5. Passez une vraie commande**

Commandez quelque chose de petit sur votre propre boutique. Un ticket dans votre mise en page devrait s'imprimer dans les quelques secondes suivant la confirmation de la commande.

## Idées pour votre mise en page

Une fois que le ticket de base s'imprime, la mise en page n'est plus qu'à un message de suivi du builder :

* **Sections par poste** : regroupez les articles par catégorie en blocs GRILL / FROID / BOISSONS, ou mettez en file un ticket séparé par section.
* **Matrice d'allergènes** : imprimez une ligne d'abréviations d'allergènes sous chaque article, à partir du nom de l'article ou d'une table de correspondance que vous maintenez dans la page d'administration.
* **Marqueurs de service** : imprimez un séparateur « ---- PLATS ---- » en fonction de la catégorie.
* **Compte à rebours avant le retrait** : pour les précommandes, imprimez « PRÊT DANS 45 MIN » à partir de l'heure demandée.
* **Deux exemplaires** : mettez le ticket en file deux fois — un pour le pass, un pour le livreur.

## Ce que cela ne fait pas

* **Cela ne modifie pas le ticket de storekit.** storekit imprime sur les imprimantes déclarées sous **Direct Printing** comme avant. Si vous ne voulez que votre ticket personnalisé, retirez l'imprimante de **Direct Printing** et pointez-la plutôt vers votre application — mais alors storekit ne sait plus que cette imprimante existe, si bien que le statut dans **Printers**, les alertes `printer.status.offline` et le journal d'impression de storekit ne s'y appliquent plus.
* **C'est votre service.** Le support storekit peut confirmer qu'une livraison `order.created.v2` a atteint votre URL et la réponse renvoyée ; il ne peut ni voir ni corriger l'application. Si elle cesse d'imprimer, commencez par `/admin` et par le journal de livraison dans le portail webhooks.
* **Les annulations ne sont pas imprimées** sauf si vous les ajoutez : `order.canceled` est un événement distinct, et un ticket « ANNULÉE » est un prompt de suivi naturel.
* **Texte brut uniquement.** Les logos, codes-barres et grandes polices nécessitent les formats binaires de Star, que le builder peut ajouter par la suite si vous demandez la prise en charge de `application/vnd.star.starprnt`.

## Voir aussi

> **[Écrire un prompt qui fonctionne](/docs/fr/guides/build/prompting-guide)**
>
> La structure derrière ce prompt, et quoi dire quand le builder se trompe.

> **[Configuration de l'imprimante](/docs/fr/guides/printing/printer-setup)**
>
> L'impression propre à storekit, y compris la manière de connecter une imprimante Star CloudPRNT à storekit.

> **[Payload order.created.v2](/docs/fr/developers/webhooks/webhook-events#order-created-v2)**
>
> Tous les champs disponibles pour votre mise en page.

> **[Vérifier les signatures](/docs/fr/developers/webhooks/verifying-signatures)**
>
> Ce que fait le builder quand il vérifie les en-têtes Svix.


## Related topics

- [6. Connecter une imprimante à tickets cloud](/docs/fr/getting-started/launch/connect-printing.md)
- [Configurer une imprimante cloud Star Micronics](/docs/fr/guides/printing/printer-setup.md)
- [Créer tes propres outils sur storekit](/docs/fr/guides/build/overview.md)
- [Dépanner la connectivité et l'impression Star](/docs/fr/guides/printing/troubleshooting.md)
- [Regroupement des commandes](/docs/fr/guides/orders/advanced/order-batching.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.