AutomationFlowsEmail & Gmail › Log Twilio SMS Delivery Statuses to Google Sheets and Alert Slack on Failures

Log Twilio SMS Delivery Statuses to Google Sheets and Alert Slack on Failures

ByKevin Yu @exekyute on n8n.io

This workflow receives Twilio SMS status callbacks, logs each delivery status (including decoded error reasons) to Google Sheets, and posts an alert to Slack when a message is marked failed or undelivered. Receives a POST webhook from Twilio each time an SMS message status…

Webhook trigger★★★★☆ complexity12 nodesGoogle SheetsSlack
Email & Gmail Trigger: Webhook Nodes: 12 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #17342 — we link there as the canonical source.

This workflow follows the Google Sheets → Slack recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "Log Twilio delivery statuses to Google Sheets and alert Slack on failures",
  "nodes": [
    {
      "id": "1dbea7da-05da-4339-8065-e9e10c501e49",
      "name": "Receive Twilio Status Callback",
      "type": "n8n-nodes-base.webhook",
      "position": [
        240,
        768
      ],
      "parameters": {
        "path": "twilio-status-callback",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "18ab6cbf-682e-4bc9-8351-932d340a2f9b",
      "name": "Load Twilio Error Code Map",
      "type": "n8n-nodes-base.set",
      "position": [
        512,
        768
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "error-map",
              "name": "error_map",
              "type": "object",
              "value": "={{ { \"30003\": \"Handset unreachable, phone is off or out of coverage\", \"30005\": \"Unknown destination handset, the number may be inactive\", \"30006\": \"Landline or a carrier that cannot receive SMS\", \"30007\": \"Carrier filtered the message as spam\", \"30008\": \"Carrier rejected it without giving a reason\", \"21610\": \"Recipient replied STOP and is unsubscribed\" } }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "f0adfa85-0526-4e55-b0b7-f736629754f7",
      "name": "Decode Status And Error Reason",
      "type": "n8n-nodes-base.set",
      "position": [
        768,
        768
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "received-at",
              "name": "received_at",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            },
            {
              "id": "message-sid",
              "name": "message_sid",
              "type": "string",
              "value": "={{ $(\"Receive Twilio Status Callback\").item.json.body?.MessageSid ?? $(\"Receive Twilio Status Callback\").item.json.MessageSid ?? \"\" }}"
            },
            {
              "id": "to-number",
              "name": "to_number",
              "type": "string",
              "value": "={{ $(\"Receive Twilio Status Callback\").item.json.body?.To ?? $(\"Receive Twilio Status Callback\").item.json.To ?? \"\" }}"
            },
            {
              "id": "message-status",
              "name": "message_status",
              "type": "string",
              "value": "={{ ($(\"Receive Twilio Status Callback\").item.json.body?.MessageStatus ?? $(\"Receive Twilio Status Callback\").item.json.MessageStatus ?? \"\").toString().toLowerCase() }}"
            },
            {
              "id": "error-code",
              "name": "error_code",
              "type": "string",
              "value": "={{ ($(\"Receive Twilio Status Callback\").item.json.body?.ErrorCode ?? $(\"Receive Twilio Status Callback\").item.json.ErrorCode ?? \"\").toString() }}"
            },
            {
              "id": "failure-reason",
              "name": "failure_reason",
              "type": "string",
              "value": "={{ ($(\"Receive Twilio Status Callback\").item.json.body?.ErrorCode ?? $(\"Receive Twilio Status Callback\").item.json.ErrorCode) ? ($json.error_map[($(\"Receive Twilio Status Callback\").item.json.body?.ErrorCode ?? $(\"Receive Twilio Status Callback\").item.json.ErrorCode).toString()] ?? \"Unmapped Twilio error code\") : \"\" }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "7072e77e-e4a5-4c4d-ae7d-4f4742d2317b",
      "name": "Append Delivery Ledger Row",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1120,
        768
      ],
      "parameters": {
        "columns": {
          "value": {
            "to_number": "={{ $json.to_number }}",
            "error_code": "={{ $json.error_code }}",
            "message_sid": "={{ $json.message_sid }}",
            "received_at": "={{ $json.received_at }}",
            "failure_reason": "={{ $json.failure_reason }}",
            "message_status": "={{ $json.message_status }}"
          },
          "schema": [
            {
              "id": "received_at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "received_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "message_sid",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "message_sid",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "to_number",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "to_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "message_status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "message_status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "error_code",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "error_code",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "failure_reason",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "failure_reason",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow"
        },
        "options": {
          "cellFormat": "RAW"
        },
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "REPLACE_WITH_YOUR_SHEET_TAB"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "REPLACE_WITH_YOUR_SPREADSHEET"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "77f63bdf-d5b3-4593-a983-75ab2c4621f7",
      "name": "Check For Failed Delivery",
      "type": "n8n-nodes-base.if",
      "position": [
        1360,
        768
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "is-failed",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $(\"Decode Status And Error Reason\").item.json.message_status }}",
              "rightValue": "failed"
            },
            {
              "id": "is-undelivered",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $(\"Decode Status And Error Reason\").item.json.message_status }}",
              "rightValue": "undelivered"
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "9e1a6648-3c0b-4850-9d37-45b756acf4a3",
      "name": "Post Failure Alert To Slack",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        1744,
        752
      ],
      "parameters": {
        "text": "=*SMS delivery failed*\nTo: {{ $(\"Decode Status And Error Reason\").item.json.to_number }}\nStatus: {{ $(\"Decode Status And Error Reason\").item.json.message_status }}\nError code: {{ $(\"Decode Status And Error Reason\").item.json.error_code || \"none reported\" }}\nReason: {{ $(\"Decode Status And Error Reason\").item.json.failure_reason || \"no reason available\" }}\nMessage SID: {{ $(\"Decode Status And Error Reason\").item.json.message_sid }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "REPLACE_WITH_YOUR_SLACK_CHANNEL"
        },
        "otherOptions": {
          "mrkdwn": true,
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "3e2d1b9c-2a08-49f3-96de-573953a23ffa",
      "name": "Return 200 To Twilio",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2000,
        784
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "noData"
      },
      "typeVersion": 1.5
    },
    {
      "id": "d0f713e7-73a5-4aaf-9b47-51f5117100eb",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -160,
        -192
      ],
      "parameters": {
        "width": 992,
        "height": 608,
        "content": "## Log Twilio delivery statuses to Google Sheets and alert Slack on failures\n\n### How it works\n\n1. Twilio POSTs a status callback to the webhook every time a message changes state.\n2. `Load Twilio Error Code Map` holds a static map of Twilio error codes to plain sentences.\n3. `Decode Status And Error Reason` pulls the SID, recipient, status and error code off the callback and resolves the code to a reason.\n4. `Append Delivery Ledger Row` writes every event to Google Sheets, successes included.\n5. `Check For Failed Delivery` passes only failed and undelivered on to Slack, and both branches answer Twilio with a 200.\n\n### Setup steps\n\n- [ ] Add a Google Sheets credential on `Append Delivery Ledger Row` and a Slack credential on `Post Failure Alert To Slack`.\n- [ ] Create a sheet with the header row received_at, message_sid, to_number, message_status, error_code, failure_reason.\n- [ ] Pick that sheet and tab in `Append Delivery Ledger Row`, and the alert channel in `Post Failure Alert To Slack`.\n- [ ] Activate, copy the production webhook URL, and set it as the StatusCallback on your Twilio messages.\n\n### Customization\n\nAdd a code to the map in `Load Twilio Error Code Map` to explain more failures, widen the IF in `Check For Failed Delivery` to catch other statuses, or swap Slack for email if that is where your team looks."
      },
      "typeVersion": 1
    },
    {
      "id": "ffefe8c6-ef63-4af7-98ae-bda3aceefbc8",
      "name": "Warning Validate Twilio Signature",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        896,
        -192
      ],
      "parameters": {
        "color": 3,
        "width": 448,
        "height": 208,
        "content": "## Validate the Twilio signature\n\nThe webhook is open by default. Twilio signs every callback with an X-Twilio-Signature header, so validate it before you trust the payload, or restrict the path to Twilio IP ranges."
      },
      "typeVersion": 1
    },
    {
      "id": "b0b5c5eb-ae72-4cd6-bcdb-c4136fff8e28",
      "name": "Section 1 Receive And Decode",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        176,
        544
      ],
      "parameters": {
        "color": 7,
        "width": 768,
        "height": 448,
        "content": "## Take in the callback and decode it\n\nTwilio POSTs a form-encoded payload on every message state change. One Set node holds the static error code map, the next one pulls the SID, recipient, status and error code off the callback, then resolves the code to a sentence a person can read."
      },
      "typeVersion": 1
    },
    {
      "id": "21685de6-36f3-4cc6-98f8-b02daef10e65",
      "name": "Section 2 Log And Route",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1024,
        544
      ],
      "parameters": {
        "color": 7,
        "width": 576,
        "height": 448,
        "content": "## Log every event, then route\n\nGoogle Sheets appends one row per callback, successes included, so the ledger is complete. Only after the write does the IF check the status, and only failed and undelivered continue."
      },
      "typeVersion": 1
    },
    {
      "id": "7504e6fa-e00a-4f91-a164-a8db63c677a0",
      "name": "Section 3 Alert And Respond",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1664,
        544
      ],
      "parameters": {
        "color": 7,
        "width": 544,
        "height": 448,
        "content": "## Alert Slack and answer Twilio\n\nSlack gets the recipient, the status, the raw code and the decoded reason. Both branches end at the same response node so Twilio always sees a 200 and stops retrying."
      },
      "typeVersion": 1
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Check For Failed Delivery": {
      "main": [
        [
          {
            "node": "Post Failure Alert To Slack",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Return 200 To Twilio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Delivery Ledger Row": {
      "main": [
        [
          {
            "node": "Check For Failed Delivery",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Twilio Error Code Map": {
      "main": [
        [
          {
            "node": "Decode Status And Error Reason",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Failure Alert To Slack": {
      "main": [
        [
          {
            "node": "Return 200 To Twilio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decode Status And Error Reason": {
      "main": [
        [
          {
            "node": "Append Delivery Ledger Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive Twilio Status Callback": {
      "main": [
        [
          {
            "node": "Load Twilio Error Code Map",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow receives Twilio SMS status callbacks, logs each delivery status (including decoded error reasons) to Google Sheets, and posts an alert to Slack when a message is marked failed or undelivered. Receives a POST webhook from Twilio each time an SMS message status…

Source: https://n8n.io/workflows/17342/ — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

This workflow automates HR onboarding by capturing new hires via a webhook, saving them to Google Sheets, emailing a Slack invite via Gmail, and notifying managers in Slack, then listening for Slack t

Google Sheets, Gmail, Slack +1
Email & Gmail

Complete Calendly automation that handles confirmations, cancellations and reschedules in a single workflow. WHAT IT DOES:

Google Sheets, Google Calendar, Slack +1
Email & Gmail

Who is this for? This template is ideal for event organizers, conference managers, and community teams who need an automated participant management system. Perfect for workshops, conferences, meetups,

Google Sheets, HTTP Request, Gmail +1
Email & Gmail

Automatically generate, validate, and deliver professional course completion certificates with zero manual work — from webhook request to PDF delivery in seconds.

Gmail, Google Sheets, Error Trigger +4
Email & Gmail

This workflow receives Stripe payment webhooks, looks up the customer’s order in Google Sheets, generates personalized HTML content with OpenAI, converts it to a PDF using PDFShift, then queues the re

Google Sheets, HTTP Request, Gmail +1