Reliable integration

How to process webhooks correctly

A webhook can arrive late, twice, or at the worst moment. Build an endpoint that can handle it.

20 minutes · Symfony and Messenger

First, the short version

What is a webhook?

A webhook is an HTTP request another service sends when an event occurs — for example, when a customer pays for an order.

Do not treat the endpoint as proof. Verify the signature first. Then safely store the event or put it on a queue. Only then return a successful response.

Get ready

What you need

Start with a small, safe foundation.

  • A public HTTPS URL for the endpoint. A provider usually cannot reach your local computer directly.
  • The provider documentation: the event format, signature header, and test mode. Do not skip the HTTP header.
  • A signing secret in an environment variable, not in source code.
  • A database or message queue where you can quickly hand off further work.

Step 1

Build an endpoint that handles repeats

The key rule is: verify, save, enqueue the work, respond. Do not do all the work inside the HTTP request.

1. Accept only the intended request

  1. Create a separate POST route. Do not put it behind a regular form or user login.
  2. Read the raw request body. The signature is often calculated from it, not from a PHP field you already changed.
  3. Check the event type and identifier. Log unknown types and ignore them safely.
#[Route('/webhooks/payment', methods: ['POST'])]
Symfony routing

2. Verify the signature before using data

  1. Follow the provider documentation exactly. It often uses an HMAC signature and timestamp.
  2. Compare signatures safely. On failure, return 400 or 401 and do not process the event.
  3. Never send the signing secret to the browser, logs, or an error response.
hash_equals($expectedSignature, $receivedSignature)
PHP: hash_equals

3. Process the event outside the response

  1. Store the event ID with a unique constraint. This gives you idempotence: the same event changes nothing a second time.
  2. Put only the data needed for the next step on the queue. Store the large original payload separately if you need it for investigation.
  3. After saving successfully, quickly return status 200 or 204. Leave long work to a consumer.
php bin/console messenger:consume async -vv
Symfony Messenger

Step 2

Check the whole flow

Do not wait for the first real payment. Use the provider test event.

  1. Send a test event

    Find the webhook test in the service administration. You expect a quick 2xx response and a record that it was received.

  2. Send it one more time

    The log should show the second attempt, but the order or payment must not change a second time. This checks idempotence.

  3. Check the queue and result

    The worker must finish the task. On failure, you should see the reason and a safe plan for another attempt.

    php bin/console messenger:failed:show

If something goes wrong

Common problems

The provider repeats the same event

This is normal after an outage or slow response. Store a unique event ID and process it only once. Idempotence helps here.

The signature does not match

Check the signing secret, raw request body, and header name. Do not bypass verification just to make a test pass.

The endpoint is slow or returns 500

Return success only after the event is safely saved. Move emails, API calls, and other slow work to a message queue.

A failed task disappears

Set limited retries, logging, and a failed transport. Endless retries can make an error and its cost worse.

Done

Your webhook has a solid foundation.

Your webhook is now verified, processed once, and leaves long work to the queue.

Request a call

I will call you on the next working day between 9:00 and 17:00.

You can also call me directly.

+420 605 181 728

Leave your phone number and send a callback request.

By sending, you agree to processing your data in order to handle your request.