{
  "name": "Stripe L4: Subscription lifecycle",
  "nodes": [
    {
      "name": "Webhook",
      "webhookId": "c3d5f7b9-sub-lifecycle",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [-500, 60],
      "parameters": {
        "httpMethod": "POST",
        "path": "stripe-subscription-lifecycle",
        "options": {
          "rawBody": true
        }
      }
    },
    {
      "name": "Verify Stripe signature",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-280, 60],
      "parameters": {
        "jsCode": "const crypto = require('crypto');\n\nconst headers = $input.first().json.headers ?? {};\nconst sigHeader = headers['stripe-signature'];\nif (!sigHeader) {\n  throw new Error('No Stripe-Signature header on this request');\n}\n\n// Raw Body is on, so the exact bytes Stripe signed are the binary property \"data\".\nconst rawBody = (await this.helpers.getBinaryDataBuffer(0, 'data')).toString('utf8');\n\n// Header shape: t=1757073600,v1=abc...,v1=def... — more than one v1 during a secret roll.\nlet timestamp = null;\nconst signatures = [];\nfor (const part of sigHeader.split(',')) {\n  const [key, value] = part.trim().split('=');\n  if (key === 't') timestamp = value;\n  if (key === 'v1') signatures.push(value);\n}\nif (!timestamp || signatures.length === 0) {\n  throw new Error('Stripe-Signature header is not in the expected format');\n}\n\nconst secret = $env.STRIPE_WEBHOOK_SECRET;\nif (!secret) {\n  throw new Error('STRIPE_WEBHOOK_SECRET is not readable from this Code node');\n}\n\nconst expected = crypto\n  .createHmac('sha256', secret)\n  .update(`${timestamp}.${rawBody}`)\n  .digest('hex');\nconst expectedBuf = Buffer.from(expected, 'hex');\n\nconst matched = signatures.some((sig) => {\n  const given = Buffer.from(sig, 'hex');\n  return given.length === expectedBuf.length && crypto.timingSafeEqual(given, expectedBuf);\n});\nif (!matched) {\n  throw new Error('Stripe signature verification failed');\n}\n\nconst ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));\nif (!Number.isFinite(ageSeconds) || ageSeconds > 300) {\n  throw new Error('Stripe event timestamp outside the five-minute tolerance');\n}\n\nreturn [{ json: JSON.parse(rawBody) }];"
      }
    },
    {
      "name": "Route on event type",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [60, 0],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "t1",
                    "leftValue": "={{ $json.type }}",
                    "rightValue": "customer.subscription.",
                    "operator": {
                      "type": "string",
                      "operation": "startsWith"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "subscription"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "t2",
                    "leftValue": "={{ $json.type }}",
                    "rightValue": "invoice.paid",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "invoice.paid"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "t3",
                    "leftValue": "={{ $json.type }}",
                    "rightValue": "invoice.payment_failed",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "invoice.payment_failed"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "other"
        }
      }
    },
    {
      "name": "Route on subscription status",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [180, 60],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "s1",
                    "leftValue": "={{ $json.data.object.status }}",
                    "rightValue": "incomplete",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "incomplete"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "s7",
                    "leftValue": "={{ $json.data.object.status }}",
                    "rightValue": "incomplete_expired",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "incomplete_expired"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "s2",
                    "leftValue": "={{ $json.data.object.status }}",
                    "rightValue": "trialing",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "trialing"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "s3",
                    "leftValue": "={{ $json.data.object.status }}",
                    "rightValue": "active",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "active"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "s4",
                    "leftValue": "={{ $json.data.object.status }}",
                    "rightValue": "past_due",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "past_due"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "s5",
                    "leftValue": "={{ $json.data.object.status }}",
                    "rightValue": "unpaid",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "unpaid"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "s6",
                    "leftValue": "={{ $json.data.object.status }}",
                    "rightValue": "canceled",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "canceled"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "s8",
                    "leftValue": "={{ $json.data.object.status }}",
                    "rightValue": "paused",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "paused"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "name": "No access yet",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [500, -180],
      "parameters": {}
    },
    {
      "name": "Grant trial access",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [500, -60],
      "parameters": {}
    },
    {
      "name": "Grant / extend access",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [500, 60],
      "parameters": {}
    },
    {
      "name": "Warn — keep access",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [500, 180],
      "parameters": {}
    },
    {
      "name": "Revoke access",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [500, 300],
      "parameters": {}
    },
    {
      "name": "Revoke and offboard",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [500, 420],
      "parameters": {}
    },
    {
      "name": "Extend access on renewal",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [760, 320],
      "parameters": {}
    },
    {
      "name": "Log only — no lifecycle action",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [760, 600],
      "parameters": {}
    },
    {
      "name": "Never paid — close it out",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [500, -320],
      "parameters": {}
    },
    {
      "name": "Paused — hold access, ask for a card",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [500, 600],
      "parameters": {}
    },
    {
      "parameters": {},
      "name": "At risk — email, alert, keep access",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [760, 460]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Verify Stripe signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Stripe signature": {
      "main": [
        [
          {
            "node": "Route on event type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route on event type": {
      "main": [
        [
          {
            "node": "Route on subscription status",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extend access on renewal",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "At risk — email, alert, keep access",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log only — no lifecycle action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route on subscription status": {
      "main": [
        [
          {
            "node": "No access yet",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Never paid — close it out",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Grant trial access",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Grant / extend access",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Warn — keep access",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Revoke access",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Revoke and offboard",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Paused — hold access, ask for a card",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {}
}
