- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Webhooks: How They Work and How to Make Them Reliable
About Post
Writing a webhook endpoint takes ten minutes. A route, a controller, read the JSON, update the payment. It works in testing on the first try.
Then, in production, it gets a request it can't verify, the same event twice, two events in the wrong order, and a retry storm because your endpoint took too long to answer. None of it shows up in the happy-path demo. All of it shows up eventually.
Let's look at how webhooks actually work, and the handful of rules that turn a ten-minute endpoint into one you can trust with money.
Polling vs webhooks: checking the door vs a doorbell
Say you're waiting for a payment provider to confirm a payment. You have two options.
Polling: ask every few seconds. "Is it paid? Is it paid? How about now?" It's simple, and it's wasteful: most requests return "no", and you still find out late, up to one polling interval after it happened.
Webhooks: give the provider a URL and let it call you when something happens. A doorbell instead of opening the door every minute to check. The provider sends an HTTP POST to your endpoint with a JSON body describing the event, like payment.succeeded.
The catch is that a doorbell can be pressed by anyone, more than once, and at awkward times. That's what the rest of this article is about.
Rule 1: verify the signature, every time
Your webhook URL is public. Anyone who finds it can POST a fake "payment succeeded" to it. So providers sign each request: they compute an HMAC of the body using a secret only you and they know, and send it in a header. You compute the same HMAC and compare.
Exact header names and formats vary by provider, so always follow their docs. A common shape, with a timestamp included in the signed data, looks like this:
private function verify(Request $request): void
{
$timestamp = (string) $request->header('X-Webhook-Timestamp');
$signature = (string) $request->header('X-Webhook-Signature');
$expected = hash_hmac(
'sha256',
$timestamp.'.'.$request->getContent(),
config('services.payments.webhook_secret'),
);
abort_unless($timestamp !== '' && hash_equals($expected, $signature), 401);
// Reject old requests so a captured one can't be replayed later
abort_if(abs(time() - (int) $timestamp) > 300, 401);
}
Three details matter here. Sign the raw body (getContent()), not re-encoded JSON, because re-encoding can change the bytes. Compare with hash_equals(), which takes the same time whether the strings differ at the first character or the last, so an attacker can't learn the signature by timing your responses. And the timestamp check stops replays of an old, validly signed request.
Rule 2: respond fast, work later
Providers wait only a limited time for your response. If your endpoint sends emails, generates a PDF and calls two APIs before answering, it will eventually time out. The provider marks the delivery as failed and sends it again, while your first attempt is still running.
So the endpoint should do as little as possible: verify, store, queue, return 2xx. The real work happens in a queued job.
Rule 3: expect duplicates
Webhook providers deliver at least once, not exactly once. Retries after timeouts, network blips and provider-side issues all mean you'll see the same event twice sooner or later. Every event carries an ID; use it as an idempotency key, protected by a unique index on that column:
public function __invoke(Request $request): Response
{
$this->verify($request);
$event = WebhookEvent::createOrFirst(
['provider_event_id' => $request->json('id')],
['type' => $request->json('type'), 'payload' => $request->getContent()],
);
if ($event->wasRecentlyCreated) {
ProcessWebhookEvent::dispatch($event);
}
return response()->noContent();
}
createOrFirst() tries the insert first and falls back to fetching the existing row if the unique index rejects it, so two copies arriving at the same moment can't both be processed. A duplicate gets a polite 204 and nothing else happens.
Storing the raw payload also gives you a free audit trail. When someone asks "did we ever receive the confirmation for this payment?", you can answer in seconds.
Rule 4: don't trust the order
Events can arrive out of order. You might get payment.refunded before payment.succeeded if the first delivery of the success event failed and was retried later. Two defences:
- Treat the webhook as a notification, then fetch the current state from the provider's API inside your job. The API tells you how things are now, whatever order the events came in.
- Never move state backwards. If your payment is already
refunded, a latesucceededevent shouldn't flip it back.
Rule 5: make failures visible
Once processing happens in a queue, failures are quiet by default. Give the job sensible retries and backoff, log the event ID with every error, and alert on jobs landing in the failed jobs table. A webhook that silently fails is worse than one that loudly fails, because nobody knows to look.
Keep a reconciliation fallback for anything important: a scheduled task that asks the provider for recent payments and compares them with your records. Webhooks are fast; reconciliation is the safety net for the rare one that never arrives.
A Laravel note on routing
Webhook routes can't carry a CSRF token. Either define the route in routes/api.php, which has no CSRF middleware, or exclude the path in bootstrap/app.php with $middleware->validateCsrfTokens(except: ['webhooks/*']). The signature check replaces CSRF here, which is exactly why Rule 1 is not optional.
The reliable webhook receiver, in one line: verify the signature, store the event under a unique ID, queue the work, return 2xx quickly, and process in a way that's safe to run twice and in any order.
If you're the one sending webhooks
Flip the rules around and be the provider you'd want: sign every payload, include a unique event ID and a timestamp, retry failed deliveries with exponential backoff, and give your users a way to see recent deliveries and resend one. Document your signature scheme with a working code sample. Your integrators will thank you.
Checklist
- HMAC signature on the raw body, compared with
hash_equals() - Timestamp tolerance to stop replays
- Unique index on the provider's event ID
- Store, queue, return
2xxfast - Fetch current state; never move status backwards
- Retries, alerts on failures, and a reconciliation job
Which of these did you learn the hard way? My bet is on duplicates.

Be first to comment it...