Skip to content

Powered by Grav CMS + Helios Theme

SendGrid

This page is for a store sending through SendGrid that wants delivered, bounced and complained figures on its campaign screens. Read Delivery reports and providers first for what a provider webhook is and what the store does with each event.

Before you begin

  • A Twilio SendGrid account with a sending API key.
  • The grav-plugin-email-sendgrid plugin at 1.1.2 or later, with the Grav Email plugin at 5.2.0 or later on PHP 8.1 or later.
  • The Email plugin's Mail Engine set to Sendgrid.

What the API keys must be allowed to do

SendGrid keys carry permissions, and this plugin needs two different sets. Use one Full Access key for both, or two keys.

Key Permission it needs What it is for
API Key (api_key) Mail Send Sending. SendGrid recommends sending with a key that has only this.
Setup API Key (setup_api_key) Read and change the Event Webhook settings. SendGrid's own instruction for the Signed Event Webhook is a Full Access key. The Set up in SendGrid button only: reading the account's event webhooks, creating or updating one, and turning signing on. Nothing sends with it.

Important

A Mail Send key cannot read or create an event webhook. Leave Setup API Key empty and the button falls back to the sending key, which works only when that key has full access.

SendGrid shows a key once, at creation, and cannot retrieve a lost one. An account is limited to 100 keys.

Nothing here needs a Sender Authentication permission. The plugin reports what SendGrid's DNS should say from convention rather than asking the account.

Configure the transport

The transport is a Grav plugin, so its own settings live in the Grav admin's plugin list. The webhook that reports deliveries back is set up from the Providers card in the KahunaCart admin, in the next section.

  1. 1

    In the Grav admin's plugin list, open Email SendGrid and set Transport to API (faster) or SMTP.

  2. 2

    Paste your sending key into API Key.

    A key that can only send mail will send mail perfectly well and be refused when the Set up button tries to create the webhook. See the permissions above.

  3. 3

    Open Email in the same plugin list, set Mail Engine to Sendgrid, and set the From address to an address on a domain you have authenticated in SendGrid.

Field Default What it does
enabled on The plugin's own switch.
transport api api posts to api.sendgrid.com; smtp connects to smtp.sendgrid.net on port 465 with TLS.
api_key empty The sending key. Required.
public_key empty Verification key: the ECDSA public key from the Signed Event Webhook. Every event is refused without it.
setup_api_key empty Used by the Set up button only. Falls back to api_key.

Both transports carry custom headers, so List-Unsubscribe and List-Unsubscribe-Post reach the wire either way.

Set the webhook up in one click

  1. 1

    Go to KahunaCart → Add-ons → Newsletters → Settings → Providers and click Generate a secret on the SendGrid card.

  2. 2

    Click Set up in SendGrid.

    It also turns signing on and saves the verification key for you, which is a separate step by hand because SendGrid cannot turn signing on when a webhook is created.

The button makes three calls, in the order SendGrid requires.

  1. GET /v3/user/webhooks/event/settings/all looks for a webhook already pointed at this address. It matches on the URL, ignoring the trailing slash and the host's case, never on the friendly name, which SendGrid says need not be unique.
  2. POST /v3/user/webhooks/event/settings, or PATCH .../{id} when one was found. All eleven event flags are sent explicitly, because these endpoints replace a webhook's settings rather than merge into them.
  3. PATCH /v3/user/webhooks/event/settings/signed/{id} turns signing on. SendGrid cannot turn signing on at creation, and this call is the only place the public key is handed over.

The button writes the key into email-sendgrid.yaml for you. If that write fails, the key is printed in the button's own message with a sentence saying where to paste it. SendGrid never shows it again.

Pressing the button twice updates the existing webhook rather than creating a second one.

The eleven flags

Flag Set to Why
delivered on The store's delivered figure.
bounce on Carries both the hard bounce and the soft blocked.
spam_report on A complaint.
open on An open the store's own pixel may have missed.
click on A click the store's own redirect may have missed.
dropped on A message SendGrid refused to send.
deferred off SendGrid is still trying. Nothing acts on it.
processed off One request per message sent, for nothing.
unsubscribe off SendGrid's own list, not the store's.
group_unsubscribe off The same.
group_resubscribe off The same.

Do it by hand instead

  1. 1

    In SendGrid, go to Settings → Mail Settings, then Event Webhooks under Webhook Settings, and click Create new webhook.

  2. 2

    Paste the webhook address from the Providers card into Post URL.

  3. 3

    Under Actions to be posted, tick Delivered, Bounced, Dropped, Spam Reports, Opened and Clicked.

    Leave Processed, Deferred and the three unsubscribe events off. Processed only means SendGrid took the message, and the unsubscribe events are SendGrid's own list rather than the store's.

  4. 4

    Under Security features, turn on Enable Signed Event Webhook, then click Save.

  5. 5

    Reopen the webhook and copy the verification key it now shows into Verification key in the Email SendGrid plugin's settings.

    The key only exists once signing is on, which is why this is the last step rather than the first.

Important

The verification key does not exist until you press Save with signing on. Turning signing off and saving deletes the key pair.

Paste the key exactly as SendGrid prints it. The plugin adds the PEM armour, and a whole PEM pasted in is passed through unchanged.

There is no header to add. See the note about custom args below.

What each event means

SendGrid event Becomes Notes
delivered delivered
bounce with type: bounce hard bounce The address is suppressed and the subscriber is set to bounced.
bounce with type: blocked soft bounce Three in thirty days are treated as a hard one.
spamreport complaint The address is suppressed and the subscriber is unsubscribed.
open open Stamped where the store's own pixel has not already.
click click The same.
dropped refused message or refused address Decided from the reason. See below.
processed, deferred, unsubscribe, group_unsubscribe, group_resubscribe, account_status_change skipped with a note Not an error. SendGrid does not retry it.

The event name in the payload is spamreport, the API flag is spam_report, and the checkbox is Spam Reports. All three are correct in their own place.

The seven dropped reasons

Reason Read as Effect
Unsubscribed Address the address Suppressed, subscriber set to bounced.
Bounced Address the address The same.
Spam Reporting Address the address The same.
Invalid the address The same.
Invalid SMTPAPI header the message The send is marked bounced. The subscriber is untouched.
Spam Content (if spam checker app enabled) the message The same.
Recipient List over Package Quota the message The same.
anything else the message One more refused message costs less than losing a customer.

The reasons are matched on words, case-insensitively, with the message words read first. "Invalid SMTPAPI header" contains "invalid" and "Spam Content" contains "spam", so reading the address words first would put both in the wrong half.

Provider quirks

SendGrid echoes no headers. Its Event Webhook carries no message headers on any event, so setting X-Grav-Send-Id as a plain header does nothing for SendGrid's reports. It does carry the send's custom args, as top-level keys on the event object. To use that path, send a custom arg of the same name; over SMTP that is the unique_args map inside X-SMTPAPI. The parser reads it where present and needs nothing where it is not.

smtp-id is the correlation to rely on. SendGrid defines it as the unique id the originating system attached to the message, which is the store's own Message-ID. SendGrid documents two gaps in its own ids: sg_message_id is absent on delayed and asynchronous bounces, and custom args do not attach to a bounce carrying a Return-Path. smtp-id survives both.

The id in an event is not quite the id the send returned. SendGrid's API answers a send with X-Message-Id: Q16RD9etQBG1HyE4UCl3VQ, and every event about that message carries sg_message_id: Q16RD9etQBG1HyE4UCl3VQ.recvd-6d4864cb4-xz4sd-1-6AA09607-D.0, which is the same id with SendGrid's internal routing on the end. The store takes the routing off before matching. Worth knowing if you are ever comparing the two by eye and wondering why they look different.

Signing is over the raw bytes. A proxy or middleware that re-encodes the body is the usual cause of "the SendGrid signature did not verify".

A request is always a list. SendGrid batches events and sends a JSON array even for one. A bare object, which turns up in SendGrid's own documentation and from the Test button, is wrapped rather than refused. At most 1,000 events in one request are read.

Do not put personal data in a custom arg. SendGrid's own warning: categories and unique arguments are stored as not-PII, generally cannot be redacted or removed, and may persist after you leave the platform.

EU regional subusers are not supported by the Set up button. The plugin calls https://api.sendgrid.com, the base for global users and subusers. An EU regional subuser is on https://api.eu.sendgrid.com and gets a 401 from the button, reported in SendGrid's own words. Set the webhook up by hand instead.

Nothing deletes a webhook. Remove one in SendGrid's dashboard, where you can see what else points at it.

DNS for the Health tab

The Health screen checks your sending domain's SPF, DKIM and DMARC. SendGrid generates two different sets of records, depending on whether automated security is on.

Automated security SPF DKIM
On (the default) a CNAME on em<id>.yourdomain.com into SendGrid's zone CNAMEs at s1._domainkey and s2._domainkey into a per-account SendGrid subdomain
Off a TXT reading v=spf1 include:sendgrid.net ~all a TXT at m1._domainkey holding the key itself

The DKIM selector cannot be guessed and this plugin does not ask SendGrid for it. Put the part before ._domainkey into deliverability.dkim_selector on KahunaCart → Add-ons → Newsletters → Settings → Health. Two selectors during a rotation is fine: separate them with a comma.

Checking it actually works

Important

The numbers on a campaign screen are not evidence that SendGrid is reporting anything. Opens and clicks are counted by the store's own tracking pixel and click redirect, and those run whether or not a webhook has ever arrived. A campaign can show delivered, opened and clicked while SendGrid has said nothing.

The Providers card is the honest answer. If it says nothing has been received, nothing has.

Send a one-recipient campaign to an address you can read, open it, press a link, then check the SendGrid card. That is the only check that tells a working webhook apart from a well-drawn screen.

Troubleshooting

Symptom Cause
Every event refused, webhook green in SendGrid Verification key is empty, or holds a key from a webhook that has since been re-saved.
"the SendGrid signature did not verify" Usually a proxy or middleware that re-encoded the request body.
"the SendGrid verification key could not be read" Something other than the key is in the field.
The Set up button says SendGrid refused it The setup key lacks permission over the Event Webhook, or the account is an EU regional subuser.
"SendGrid turned signing on but did not hand back a verification key" Turn signing on in the dashboard, save, and copy the key across by hand.
No open or click events at all Open and click tracking are account settings in SendGrid, separate from the webhook.
Delivery reports stopped after renaming the send header Nothing to do here. SendGrid correlates on smtp-id, which is the store's own Message-ID.