# SendGrid delivery problems notifications on your phone > Get one push notification per SendGrid Event Webhook batch that summarises its bounces, blocks, drops and spam reports, and a Highest-priority one if your account is suspended. - Company: SendGrid (https://www.justpush.io/recipes/sendgrid) - Event: `bounce` (bounce, dropped and spam report) - Tags: Email delivery, Email deliverability - Install: https://studio.justpush.io/recipes/sendgrid/delivery-problems - Web page: https://www.justpush.io/recipes/sendgrid/delivery-problems ## Setup 1. Click Install in Studio and sign in. The recipe is added to your account and you get a personal webhook URL. 2. In SendGrid, go to Settings > Mail Settings > Event Webhooks and click Create new webhook. 3. Paste your webhook URL (shown in Studio after install) as the Post URL and, under Actions to be posted, tick the bounce, dropped and spam report events. 4. Click Test Your Integration to send a batch of example events, then Save. 5. Under **Actions to be posted**, tick the bounce, dropped and spam report actions. Tick deferred too only if you also add `deferred` to `?events=` (see below). You can leave Delivered, Opened, Clicked and the rest off; the recipe ignores them anyway. 6. SendGrid sends events in **batches** (a JSON array, usually a few seconds' worth), so you get **one push per batch** that counts what went wrong, such as **🚨 1 spam report, 2 bounces**, and names the first address. Priority follows the worst kind in the batch: spam reports are High, bounces and drops are Normal, blocks and deferrals are Low. 7. Emails that SendGrid drops because the address already bounced, unsubscribed or reported spam are skipped, since SendGrid is protecting you. Add `?all=1` to count those too. 8. To choose what pushes, add `?events=` with a comma-separated list of `spamreport`, `bounce`, `dropped`, `blocked` and `deferred`. For example, spam reports only: 9. ``` 10. your webhook URL (shown in Studio after install)?events=spamreport 11. ``` 12. If SendGrid sends an **account status change** (suspension, deactivation or ban for compliance reasons), you get a Highest-priority push, whatever `?events=` says. 13. **Test Your Integration** sends a batch of SendGrid's example events, which gives one push. SendGrid can sign requests (Signed Event Webhook, `X-Twilio-Email-Event-Webhook-Signature`) and use OAuth, but JustPush doesn't check either yet, so keep your endpoint URL private. ## Code Studio calls `handleRequest(request)` with the incoming webhook (`{ method, headers, body, query, raw }`) and sends the message object it returns. Returning `null` sends nothing. ```js // SendGrid → Event Webhook (bounces, blocks, drops, spam reports) // SendGrid POSTs a JSON array of events. This sends one push per batch that summarises the // problems in it ("1 spam report, 2 bounces") and ignores delivered, open, click and the rest. // Account status changes (suspension, deactivation, ban) push at Highest priority. // Docs: https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event // Problem kinds, loudest first, with their labels and priorities. const KINDS = { spamreport: { one: "spam report", many: "spam reports", emoji: "🚨", priority: 1 }, // hurts your reputation bounce: { one: "bounce", many: "bounces", emoji: "â†Šī¸", priority: 0 }, // permanent; worth knowing dropped: { one: "dropped email", many: "dropped emails", emoji: "đŸšĢ", priority: 0 }, blocked: { one: "block", many: "blocks", emoji: "⛔", priority: -1 }, // temporary; may still arrive deferred: { one: "deferral", many: "deferrals", emoji: "âŗ", priority: -1 }, // SendGrid keeps retrying } const ORDER = ["spamreport", "bounce", "dropped", "blocked", "deferred"] const DEFAULT_KINDS = ["spamreport", "bounce", "dropped", "blocked"] // Drops where SendGrid is protecting you from a known-bad address; expected, so skipped by default. const SUPPRESSED_DROPS = ["Bounced Address", "Unsubscribed Address", "Spam Reporting Address"] // Account status changes and what they mean. const ACCOUNT = { compliance_suspend: { title: "🚨 SendGrid account suspended", message: "Email delivery is blocked", priority: 2 }, compliance_deactivate: { title: "🚨 SendGrid account deactivated", message: "Queued email was deleted", priority: 2 }, compliance_ban: { title: "🚨 SendGrid account banned", message: "Your account has been banned", priority: 2 }, reactivate: { title: "✅ SendGrid account reactivated", message: "Email delivery is back on", priority: -1 }, } // Look a key up without tripping over inherited names like "toString". function pick(table, key, fallback) { return Object.prototype.hasOwnProperty.call(table, key) ? table[key] : fallback } // A trimmed string, or null. function text(value) { return typeof value === "string" && value.trim() ? value.trim() : null } // Which problem kind an event is, or null. function kindOf(event) { if (event.event === "bounce") return event.type === "blocked" ? "blocked" : "bounce" return ["spamreport", "dropped", "deferred"].includes(event.event) ? event.event : null } // One event in words: "sam@example.com ¡ 550 unknown recipient". function describe(kind, event) { const email = text(event.email) ?? "A recipient" if (kind === "spamreport") return `${email} marked your email as spam` const reason = kind === "deferred" ? text(event.response) : text(event.reason) ?? text(event.bounce_classification) return `${email} ¡ ${reason ?? KINDS[kind].one}` } function handleRequest(request) { // SendGrid always sends an array; accept a single event object too (e.g. hand-made tests). const body = request.body const events = (Array.isArray(body) ? body : body && typeof body === "object" && body.event ? [body] : []) .filter((e) => e && typeof e === "object" && typeof e.event === "string") if (!events.length) { console.log("Not a SendGrid Event Webhook batch — skipping") return null } // ?events=spamreport,bounce limits which kinds push; ?all=1 also counts drops to suppressed addresses. const query = request.query || {} const all = ["1", "true", "yes"].includes(String(query.all ?? "").toLowerCase()) const wanted = text(query.events) ? String(query.events).toLowerCase().split(",").map((s) => s.trim()).filter((s) => ORDER.includes(s)) : DEFAULT_KINDS const groups = {} for (const event of events) { const kind = kindOf(event) if (!kind || !wanted.includes(kind)) continue if (kind === "dropped" && !all && SUPPRESSED_DROPS.includes(event.reason)) continue if (!groups[kind]) groups[kind] = [] groups[kind].push(event) } const kinds = ORDER.filter((k) => groups[k]) const summary = kinds .map((k) => `${groups[k].length} ${groups[k].length === 1 ? KINDS[k].one : KINDS[k].many}`) .join(", ") // An account status change outranks everything else in the batch. const status = events.find((e) => e.event === "account_status_change" && pick(ACCOUNT, e.type, null)) if (status) { const account = ACCOUNT[status.type] return { title: account.title, message: account.message + (summary ? ` ¡ also ${summary}` : ""), topic: "SendGrid", priority: account.priority, // Highest: nothing is being delivered right now } } if (!kinds.length) { console.log(`No problems in this batch of ${events.length} event(s) — skipping`) return null } // Lead with the loudest kind's first event. const lead = KINDS[kinds[0]] const first = groups[kinds[0]][0] const total = kinds.reduce((n, k) => n + groups[k].length, 0) return { title: `${lead.emoji} ${summary}`, message: describe(kinds[0], first) + (total > 1 ? ` ¡ +${total - 1} more` : ""), topic: "SendGrid", priority: lead.priority, } } ``` ## SendGrid bounce webhook payload (sample) ```json [ { "email": "alex@example.com", "event": "delivered", "smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>", "category": "welcome", "response": "250 OK", "timestamp": 1790670569, "sg_event_id": "rWVYmVk90MjZJ9iohOBa3w==", "sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0" }, { "email": "jamie@example.com", "event": "spamreport", "smtp-id": "<14c5d75ce94.dfd.64b469@ismtpd-555>", "timestamp": 1790670570, "sg_event_id": "37nvH5QBz858KGVYCM4uOA==", "sg_message_id": "14c5d75ce94.dfd.64b469.filter0001.16648.5515E0B88.0" }, { "type": "bounce", "email": "sam@example.com", "event": "bounce", "reason": "550 5.1.1 unknown recipient", "status": "5.1.1", "smtp-id": "<14c5d75ce95.dfd.64b469@ismtpd-555>", "category": "welcome", "timestamp": 1790670571, "sg_event_id": "6g4ZI7SA-xmRDv57GoPIPw==", "sg_message_id": "14c5d75ce95.dfd.64b469.filter0001.16648.5515E0B88.0", "bounce_classification": "Invalid Address" }, { "type": "bounce", "email": "kim@example.com", "event": "bounce", "reason": "550 5.2.1 mailbox disabled", "status": "5.2.1", "smtp-id": "<14c5d75ce96.dfd.64b469@ismtpd-555>", "category": "welcome", "timestamp": 1790670572, "sg_event_id": "7h5ZJ8TB-ynSEw68HpQJQx==", "sg_message_id": "14c5d75ce96.dfd.64b469.filter0001.16648.5515E0B88.0", "bounce_classification": "Mailbox Unavailable" }, { "ip": "192.0.2.10", "email": "alex@example.com", "event": "open", "category": "welcome", "timestamp": 1790670580, "useragent": "Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", "sg_event_id": "FOTFFO0ecsBE-zxFXfs6WA==", "sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0", "sg_machine_open": false } ] ``` ## FAQ ### Does this work on iPhone and Android? Yes. Install the JustPush app from the App Store or Google Play and sign in. Every phone signed in to your account gets the notification. ### Do I need to write code? No. Install the recipe in Studio, paste your webhook URL into SendGrid and you are done. The code is there if you want to change the text, the sound or the buttons. ### Can I change what the notification says? Yes. After install the recipe's code is yours. Edit it in Studio and test it against the sample payload before you save. ### What does it cost? JustPush is free for 30 days. After that it's $19.99 a year, or $39.99 once. There is no extra charge for recipes.