Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

BikashKatwal's avatar

Mailgun webhook

Can anyone provide mailgun webhook setup document with test domain? i.e sandboxxxxxx.mailgun.org. I have been following mailgun documents but it is getting very difficult for me to understand the docs and setup. if anyone could provide step by step mailgun webhook setup process than it would be a great help.

0 likes
12 replies
BikashKatwal's avatar

@bobbybouwmann I have done the setup and was able to send the emails using a sandbox user. Now I want to track the emails. If it is delivered, open. So I was trying to configure the webhook but I could not do it.

Sinnbeck's avatar
Sinnbeck
Best Answer
Level 102

Well. I have a simple controller + route setup for webhooks. One for route for delivered and one for failed. REMEMBER : Add the webhook url to $except in the VerifyCsrfToken middleware

public function emailDelivered(Request $request)
    {
        try {
            $this->mailgun_webhook->handleDelivered($request->all());

            return response('Success', 200);
        }
        catch (Exception $e) {
            return response($e->getMessage(), 406);
        }
    }

    public function emailFailed(Request $request)
    {
        try {
            $this->mailgun_webhook->handleFailed($request->all());

            return response('Success', 200);
        }
        catch (Exception $e) {
            return response($e->getMessage(), 406);
        }
    }

This then calls a simple parser that (MailgunWebhook class).

/**
     * Handle delivered message data
     *
     * @param array $data Incoming data.
     *
     * @return mixed
     * @throws \Exception Signature is invalid.
     */
    public function handleDelivered(array $data)
    {
        if (!$this->validate($data['signature'])) {
            throw new \Exception('Invalid signature!');
        }

        $event_data = $data['event-data'];
        $delivered_data = [
            'tags' => $event_data['tags'],
            'recipient' => $event_data['recipient'],
            'headers' => $event_data['message']['headers'],
            'timestamp' => $event_data['timestamp'],
        ];
        return ParseMailDelivered::dispatchNow($this->email, $delivered_data);
    }
/**
     * Handle failed message data
     *
     * @param array $data Incoming data.
     *
     * @return mixed
     * @throws \Exception Signature is invalid.
     */
    public function handleFailed(array $data)
    {
        if (!$this->validate($data['signature'])) {
            throw new \Exception('Invalid signature!');
        }

        $event_data = $data['event-data'];
        $delivered_data = [
            'tags' => $event_data['tags'],
            'recipient' => $event_data['recipient'],
            'headers' => $event_data['message']['headers'],
            'timestamp' => $event_data['timestamp'],
            'delivery_status' => $event_data['delivery-status'],
            'severity' => $event_data['severity'],
        ];
        return ParseMailFailed::dispatchNow($this->email, $delivered_data);
    }

    /**
     * @param array  $signature
     * @param string $api_key
     *
     * @return bool
     */
    protected function validate(array $signature, $api_key = null)
    {
        $timestamp = $signature['timestamp'];
        $token = $signature['token'];
        $signature = $signature['signature'];
        //Concat timestamp and token values
        if (empty($timestamp) || empty($token) || empty($signature)) {
            return false;
        }
        $api_key = $api_key ? $api_key : config('mailgun.api_key');

        $hmac = hash_hmac('sha256', $timestamp.$token, $api_key);
        if (function_exists('hash_equals')) {
            // hash_equals is constant time, but will not be introduced until PHP 5.6
            return hash_equals($hmac, $signature);
        } else {
            return $hmac === $signature;
        }
    }

This then dispatches a job (currently it just dispatches instantly as you can see), but you can do whatever you want to here. :) I just save it to the database in the job.

1 like
BikashKatwal's avatar

@sinnbeck Thanks for helping with my queries. I still have some queries. In the function handleDelivered and handleFailed, I can see ParseMailDelivered and ParseMailFailed classes. I want to know how the data is being parsed and dispatched,

Now, I could execute webhooks from mailgun dashboard only when I click "Text Webhook" button. but I was wondering, about executing the hooks when the email is being sent. When my code is sending the email, is the hooks are being executed. How can I debug/check that the hook are working. However, it returns data when I click Text Webhooks from mailgun dashboard.

Sinnbeck's avatar

All those classes do is save to the database.

To test it ser up a proper test domain and add the correct webhook urls to the mailgun interface. Be aware that they need a public url where they can send the information to. If you want to just see the data for testing, then add a pastebin url.

BikashKatwal's avatar

@sinnbeck I am following your suggestion and I am getting very close to it. I can see the code being executed but I am failing to get the values to update in the database. Could you please see the code below:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\MailgunWebhook;
use Illuminate\Support\Facades\DB;


class WebhookController extends Controller
{

    public function emailDelivered(Request $request)
    {
        try {
            $this->handleDelivered($request->all());
            return response('Success', 200);
        } catch (Exception $ex) {
            return response($ex->getMessage(), 406);
        }
    }

    public function emailFailed(Request $request)
    {
        $mailgun_webhook = new MailgunWebhook();
        try {
            return $mailgun_webhook->handleFailed($request->all());
            return response('Success', 200);
        } catch (Exception $ex) {
            return response($ex->getMessage(), 406);
        }
    }

    public function handleDelivered(array $data)
    {
        if (!$this->validateWebhook($data['signature'])) {
            throw new \Exception('Invalid signature!');
        }

        $event_data = $data['event-data'];
//        $event_data = $data['user-variables'];
        $delivered_data = [
            'tags' => $event_data['tags'],
            'recipient' => $event_data['recipient'],
            'headers' => $event_data['message']['headers'],
            'timestamp' => $event_data['timestamp'],
        ];
        DB::select('CALL halls.`insert_invoices`(74,"H4H0001",30,"Hello Desc",15,315,"BOM")');
//        $insertDetails = DB::select('CALL insert_invoices(?,?,?,?,?,?,?)', [
//            '216',
//            $event_data['invoiceNumber'],
//            $event_data['term'],
//            $event_data['info'],
//            $event_data['gst_amount'],
//            $event_data['gross_total'],
//            'CommonWealth Bank'
//        ]);
    }

    public function handleFailed(array $data)
    {
        if (!$this->validateWebhook($data['signature'])) {
            throw new \Exception('Invalid signature!');
        }

        $event_data = $data['event-data'];
        $delivered_data = [
            'tags' => $event_data['tags'],
            'recipient' => $event_data['recipient'],
            'headers' => $event_data['message']['headers'],
            'timestamp' => $event_data['timestamp'],
            'delivery_status' => $event_data['delivery-status'],
            'severity' => $event_data['severity'],
        ];
        //return ParseMailFailed::dispatchNow($this->email, $delivered_data);
    }

    public function validateWebhook(array $signature, $api_key = null)
    {
        $timestamp = $signature['timestamp'];
        $token = $signature['token'];
        $signature = $signature['signature'];
        //Concat timestamp and token values
        if (empty($timestamp) || empty($token) || empty($signature)) {
            return false;
        }
        $api_key = $api_key ? $api_key : env('MAILGUN_WEBHOOK');

        $hmac = hash_hmac('sha256', $timestamp . $token, $api_key);
        if (function_exists('hash_equals')) {
            // hash_equals is constant time, but will not be introduced until PHP 5.6
            return hash_equals($hmac, $signature);
        } else {
            return $hmac === $signature;
        }
    }
}

handleDelivered inserts the data because I have provided static values. But I want to insert the data provided from the webhook. so I did

  $insertDetails = DB::select('CALL insert_invoices(?,?,?,?,?,?,?)', [
            '216',//This is for test
            $event_data['invoiceNumber'],
            $event_data['term'],
            $event_data['info'],
            $event_data['gst_amount'],
            $event_data['gross_total'],
            'CommonWealth Bank'
        ]);

to get the value and it is not working. I don't know if it is the correct way to render the value. If I run the hook in webhook interface out database update code. I could only see Response:Success. I dont see any data even if I do return $event_data. How can I update the data in the database?

Sinnbeck's avatar

What do you mean you run the webhook? The url is called from mailgun right? Use telescope to see if they hit your route with status 200. I had issues myself with getting status 403. Remember to use post for the route and to add it to the $except array mentioned earlier

BikashKatwal's avatar

@sinnbeck Yes it has been used. I am using ngrok and it show status 200 ok. When the email is sent.

Route::post('webhooks/email_delivered','WebhookController@emailDelivered');
 protected $except = [
        'webhooks/*',
    ];
https://9992218b.ngrok.io/api/webhooks/email_delivered

in Delivered Messages

Url is called from the mail gun and it enters the handleDelivered also. Here, I want to update the database. but I cannot see any values in $event_data = $data['event-data'];.

Sinnbeck's avatar

Ok great. What I did was to use this to debug the incoming data

Log::info(json_encode ($data));
BikashKatwal's avatar

@sinnbeck Thank you so much for your help. I can now update the database using webhooks. Thank you so much. This whole discussion was so important to get the things done. I will write a medium story and share it with you. I would like to share your name too in the article if you don't mind.

Setting hooks without your help was not possible. Thank you so much.

Sinnbeck's avatar

Happy to help as always. The name is René Sinnbeck

Please consider marking best answer :)

Please or to participate in this conversation.