[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-idempotency-keys-webhooks-side-effects":3,"blog-nav-idempotency-keys-webhooks-side-effects":27},{"slug":4,"title":5,"description":6,"body":7,"category":8,"readTime":9,"tags":10,"coverImage":14,"coverImageAlt":15,"heroImage":16,"heroImageAlt":15,"metaTitle":17,"metaDescription":18,"ogImage":19,"noindex":20,"published":21,"publishAt":22,"sortOrder":23,"updatedAt":22,"status":24,"path":25,"bodyHtml":26},"idempotency-keys-webhooks-side-effects","Idempotency keys: one webhook, one side effect","Retries are normal. Store the key with the result and enforce it with a unique constraint so a second delivery cannot create a second row.","Providers retry. Queues retry. People double-click. If each delivery creates a row, one payment becomes two receipts, or one booking becomes two.\n\n[QR Ph payments](\u002Fblog\u002Fqr-ph-payments-philippine-merchants) and [digital payments for online stores](\u002Fblog\u002Fdigital-payments-philippine-online-stores) already say the order update should be webhook-driven, verified, and idempotent. They stop at the word. This is the mechanism. It is not a wallet setup guide.\n\n[Queues](\u002Fblog\u002Fqueues-work-outside-the-http-request) will run a job more than once. That is normal. The side effect still has to happen once.\n\n## The pattern\n\nThe caller sends a key for that operation. Stripe’s [`Idempotency-Key`](https:\u002F\u002Fdocs.stripe.com\u002Fapi\u002Fidempotent_requests) header is the well-known version: the same key returns the stored result instead of performing the operation again.\n\nWe store the key with the result. A **unique constraint** on the key is what makes the second delivery lose. Code that checks and then inserts, without the constraint, still races.\n\n1. Verify the caller. A signature check comes before any of this. Idempotency does not authenticate anyone.\n2. If the key already has a stored result, return it.\n3. Otherwise do the work in a transaction and store the key with that result.\n4. If the unique insert loses, read the winner’s result and return that.\n\nThe key is per operation, not per customer and not per day. Reusing one key for two different payments is a bug in the client.\n\n## What this is not\n\nIt is not a reason to add a service. A column and a unique index on the existing app is the usual fix. It does not replace the payment articles. Those still cover how GCash, Maya, and QR Ph should update an order. This covers why the second delivery must not create a second one.\n\n## Straight next step\n\nIf a retry has already created duplicate bookings, receipts, or stock movements, [send one example pair](\u002Fcontact). We will put the key on that operation before we talk about a new integration.","Engineering",6,[11,12,13],"API","Webhooks","Laravel","\u002Fimages\u002Fjournal\u002Fidempotency-keys-webhooks-side-effects-cover.webp?v=20261013b","Person writing on a printed form","\u002Fimages\u002Fjournal\u002Fidempotency-keys-webhooks-side-effects-hero.webp?v=20261013b","Idempotency Keys for Webhooks | PrimeCode","How an API stores an idempotency key so a retried webhook updates one record instead of creating a second side effect.","\u002Fimages\u002Fjournal\u002Fidempotency-keys-webhooks-side-effects-og.webp?v=20261013b",false,true,"2026-10-19T01:00:00.000Z",53,"live","\u002Fblog\u002Fidempotency-keys-webhooks-side-effects","\u003Cp>Providers retry. Queues retry. People double-click. If each delivery creates a row, one payment becomes two receipts, or one booking becomes two.\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"\u002Fblog\u002Fqr-ph-payments-philippine-merchants\" rel=\"noopener noreferrer\">QR Ph payments\u003C\u002Fa> and \u003Ca href=\"\u002Fblog\u002Fdigital-payments-philippine-online-stores\" rel=\"noopener noreferrer\">digital payments for online stores\u003C\u002Fa> already say the order update should be webhook-driven, verified, and idempotent. They stop at the word. This is the mechanism. It is not a wallet setup guide.\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"\u002Fblog\u002Fqueues-work-outside-the-http-request\" rel=\"noopener noreferrer\">Queues\u003C\u002Fa> will run a job more than once. That is normal. The side effect still has to happen once.\u003C\u002Fp>\n\u003Ch2>The pattern\u003C\u002Fh2>\n\u003Cp>The caller sends a key for that operation. Stripe’s \u003Ca href=\"https:\u002F\u002Fdocs.stripe.com\u002Fapi\u002Fidempotent_requests\" rel=\"noopener noreferrer\">\u003Ccode>Idempotency-Key\u003C\u002Fcode>\u003C\u002Fa> header is the well-known version: the same key returns the stored result instead of performing the operation again.\u003C\u002Fp>\n\u003Cp>We store the key with the result. A \u003Cstrong>unique constraint\u003C\u002Fstrong> on the key is what makes the second delivery lose. Code that checks and then inserts, without the constraint, still races.\u003C\u002Fp>\n\u003Col>\n\u003Cli>Verify the caller. A signature check comes before any of this. Idempotency does not authenticate anyone.\u003C\u002Fli>\n\u003Cli>If the key already has a stored result, return it.\u003C\u002Fli>\n\u003Cli>Otherwise do the work in a transaction and store the key with that result.\u003C\u002Fli>\n\u003Cli>If the unique insert loses, read the winner’s result and return that.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>The key is per operation, not per customer and not per day. Reusing one key for two different payments is a bug in the client.\u003C\u002Fp>\n\u003Ch2>What this is not\u003C\u002Fh2>\n\u003Cp>It is not a reason to add a service. A column and a unique index on the existing app is the usual fix. It does not replace the payment articles. Those still cover how GCash, Maya, and QR Ph should update an order. This covers why the second delivery must not create a second one.\u003C\u002Fp>\n\u003Ch2>Straight next step\u003C\u002Fh2>\n\u003Cp>If a retry has already created duplicate bookings, receipts, or stock movements, \u003Ca href=\"\u002Fcontact\" rel=\"noopener noreferrer\">send one example pair\u003C\u002Fa>. We will put the key on that operation before we talk about a new integration.\u003C\u002Fp>\n",{"prev":28,"related":43},{"slug":29,"title":30,"description":31,"category":8,"readTime":9,"tags":32,"coverImage":34,"coverImageAlt":35,"heroImage":36,"heroImageAlt":35,"metaTitle":37,"metaDescription":38,"ogImage":39,"noindex":20,"published":21,"publishAt":40,"sortOrder":41,"updatedAt":40,"status":24,"path":42},"api-rate-limiting-before-traffic","Rate-limit the API before the expensive routes exist","Login, OTP, search, webhooks, and unbounded page size are the routes to limit first. OWASP API4:2023 is the control, not a new security product.",[11,33,13],"Security","\u002Fimages\u002Fjournal\u002Fapi-rate-limiting-before-traffic-cover.webp?v=20261013b","Padlock resting on a laptop keyboard","\u002Fimages\u002Fjournal\u002Fapi-rate-limiting-before-traffic-hero.webp?v=20261013b","Rate-Limit the API Before Traffic Grows | PrimeCode","Where a Laravel API needs rate limits first — login, OTP, search, and webhooks — and what OWASP API4:2023 actually asks for.","\u002Fimages\u002Fjournal\u002Fapi-rate-limiting-before-traffic-og.webp?v=20261013b","2026-10-16T01:00:00.000Z",52,"\u002Fblog\u002Fapi-rate-limiting-before-traffic",[44,46,62],{"slug":29,"title":30,"description":31,"category":8,"readTime":9,"tags":45,"coverImage":34,"coverImageAlt":35,"heroImage":36,"heroImageAlt":35,"metaTitle":37,"metaDescription":38,"ogImage":39,"noindex":20,"published":21,"publishAt":40,"sortOrder":41,"updatedAt":40,"status":24,"path":42},[11,33,13],{"slug":47,"title":48,"description":49,"category":8,"readTime":9,"tags":50,"coverImage":53,"coverImageAlt":54,"heroImage":55,"heroImageAlt":54,"metaTitle":56,"metaDescription":57,"ogImage":58,"noindex":20,"published":21,"publishAt":59,"sortOrder":60,"updatedAt":59,"status":24,"path":61},"queues-work-outside-the-http-request","Queues: work that does not belong in the HTTP request","The request commits the booking or the order. Mail, PDFs, webhooks, and exports belong on a worker, and that worker must be safe to retry.",[13,51,52],"Queues","Architecture","\u002Fimages\u002Fjournal\u002Fqueues-work-outside-the-http-request-cover.webp?v=20261013b","Warehouse aisles of packed orders waiting to move","\u002Fimages\u002Fjournal\u002Fqueues-work-outside-the-http-request-hero.webp?v=20261013b","Queues: Work That Leaves the HTTP Request | PrimeCode","Which web-app tasks belong in a Laravel queue — mail, webhooks, exports — and why a timeout is the wrong place to notice.","\u002Fimages\u002Fjournal\u002Fqueues-work-outside-the-http-request-og.webp?v=20261013b","2026-10-14T01:00:00.000Z",51,"\u002Fblog\u002Fqueues-work-outside-the-http-request",{"slug":63,"title":64,"description":65,"category":8,"readTime":66,"tags":67,"coverImage":71,"coverImageAlt":72,"heroImage":73,"heroImageAlt":72,"metaTitle":74,"metaDescription":75,"ogImage":76,"noindex":20,"published":21,"publishAt":77,"sortOrder":78,"updatedAt":77,"status":24,"path":79},"postgresql-vs-mysql-for-applications","PostgreSQL or MySQL: choose for the workload, not the habit","MySQL is enough for ordinary relational apps. PostgreSQL earns its place with overlap rules, JSON you query, and search in the database.",7,[68,69,70],"PostgreSQL","MySQL","Database","\u002Fimages\u002Fjournal\u002Fpostgresql-vs-mysql-for-applications-cover.webp?v=20261013b","Server racks with network cables in a data room","\u002Fimages\u002Fjournal\u002Fpostgresql-vs-mysql-for-applications-hero.webp?v=20261013b","PostgreSQL vs MySQL for Applications | PrimeCode","How to choose PostgreSQL or MySQL for a business application — constraints, JSON, and search — without a benchmark contest.","\u002Fimages\u002Fjournal\u002Fpostgresql-vs-mysql-for-applications-og.webp?v=20261013b","2026-10-09T01:00:00.000Z",49,"\u002Fblog\u002Fpostgresql-vs-mysql-for-applications"]