{
  "name": "SeoPrimo - credits.low email alert",
  "nodes": [
    {
      "parameters": {
        "content": "## SeoPrimo: credits.low by email\n1. Activate the workflow and add its **Production URL** as a webhook endpoint in SeoPrimo (Integrations, Webhooks) for the event `credits.low` (sent at most once every 24 hours).\n2. Paste the endpoint secret (`whsec_...`) in **Verify signature**.\n3. Set the n8n env var `NODE_FUNCTION_ALLOW_BUILTIN=crypto`.\n4. Pick your SMTP credential and the recipient in **Send email**.",
        "height": 250,
        "width": 460
      },
      "id": "de4f1d2d-bc2f-5bf2-9758-56ff8bdb2f33",
      "name": "Setup",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        -140
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "seoprimo-credits-low",
        "responseMode": "responseNode",
        "options": {
          "rawBody": true
        }
      },
      "id": "edc641c1-3b7f-5dde-8cb9-2713e9bccbff",
      "name": "SeoPrimo webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        300
      ],
      "webhookId": "15a9a09d-bbf6-58bc-8d7c-3508a4692323"
    },
    {
      "parameters": {
        "jsCode": "// Verify X-SeoPrimo-Signature on the RAW body (Webhook node: option \"Raw Body\" ON).\n// Needs the env var NODE_FUNCTION_ALLOW_BUILTIN=crypto on the n8n server (or '*').\nconst crypto = require('crypto');\n\n// Paste the endpoint secret (whsec_...) here, or expose it as the env var SEOPRIMO_WEBHOOK_SECRET.\nlet secret = 'whsec_REPLACE_WITH_YOUR_SECRET';\ntry { if ($env.SEOPRIMO_WEBHOOK_SECRET) secret = $env.SEOPRIMO_WEBHOOK_SECRET; } catch (e) { /* env access blocked */ }\nconst TOLERANCE_SECONDS = 300;\n\nconst item = $input.first();\nconst header = String(item.json.headers['x-seoprimo-signature'] || '');\nconst raw = await this.helpers.getBinaryDataBuffer(0, 'data'); // the bytes exactly as received\n\nlet valid = false;\ntry {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));\n  const t = parseInt(parts.t, 10);\n  const received = String(parts.v1 || '');\n  const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(raw).digest('hex');\n  const fresh = Math.abs(Date.now() / 1000 - t) <= TOLERANCE_SECONDS;\n  valid = fresh\n    && received.length === expected.length\n    && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));\n} catch (e) {\n  valid = false;\n}\n\nconst event = valid ? JSON.parse(raw.toString('utf8')) : {};\nreturn [{ json: { valid, id: event.id, type: event.type, created: event.created, data: event.data || {} } }];\n"
      },
      "id": "b9d257c2-a9a3-510c-924a-05ef83ad1ca7",
      "name": "Verify signature",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "7f2c3a82-98c0-5042-92f1-252f98ba8a86",
              "leftValue": "={{ $json.valid }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "0ee70c34-7cae-5c14-ab66-76aa0e74c8f7",
      "name": "Signature valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        480,
        300
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { \"received\": true } }}",
        "options": {
          "responseCode": 200
        }
      },
      "id": "1fb2923d-f179-5741-8310-cb3222eaad30",
      "name": "Respond 200",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        720,
        200
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { \"error\": \"invalid_signature\" } }}",
        "options": {
          "responseCode": 401
        }
      },
      "id": "df12f1b1-4764-56de-9a7b-4bb1e675aa8e",
      "name": "Respond 401",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        720,
        440
      ]
    },
    {
      "parameters": {
        "fromEmail": "seoprimo@example.com",
        "toEmail": "you@example.com",
        "subject": "=SeoPrimo: only {{ $('Verify signature').item.json.data.available }} credits left",
        "emailFormat": "text",
        "text": "=Your SeoPrimo credits are running low.\n\nAvailable: {{ $('Verify signature').item.json.data.available }}\nMonthly plan credits: {{ $('Verify signature').item.json.data.monthly_credits }}\nAlert threshold: {{ $('Verify signature').item.json.data.threshold }}\n\nTop up from the panel: https://seoprimo.it/app/\n",
        "options": {}
      },
      "id": "3fc1fac8-6ac1-5c40-9b2a-e7e298d88d94",
      "name": "Send email",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        960,
        200
      ],
      "credentials": {
        "smtp": {
          "id": "REPLACE_WITH_CREDENTIAL_ID",
          "name": "SMTP account"
        }
      }
    }
  ],
  "connections": {
    "SeoPrimo webhook": {
      "main": [
        [
          {
            "node": "Verify signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify signature": {
      "main": [
        [
          {
            "node": "Signature valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Signature valid?": {
      "main": [
        [
          {
            "node": "Respond 200",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond 401",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respond 200": {
      "main": [
        [
          {
            "node": "Send email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "21584bac-9573-58a4-87b6-cc6a63d7ea65",
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
