{
  "name": "blue·auth · CMD batch (2–100 PDFs)",
  "meta": {
    "templateCreatedBy": "blue·auth",
    "description": "Receives PDFs for the same signer, creates a CMD batch in one idempotent request, and returns the secure signing link. The PIN and OTP never pass through n8n."
  },
  "nodes": [
    {
      "id": "note-setup",
      "name": "Secure setup",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [-760, -320],
      "parameters": {
        "width": 760,
        "height": 270,
        "content": "## CMD batch · one-time setup\n1. In **Create CMD batch**, add a **Header Auth** credential: `Authorization` = `Bearer ba_live_…`.\n2. Copy the **Production URL** from the **CMD events** webhook and replace `BATCH_EVENTS_URL`.\n3. Set `BLUEAUTH_CMD_BATCH_SECRET` in the n8n environment and use the same value as `webhookSecret`.\n4. Activate the workflow.\n\nEach PDF must include `pdfName` and a temporary HTTPS URL (`pdfUrl`). `documentHash` is optional: blue·auth validates the SHA-256 when provided and calculates it when omitted. The signer enters their mobile number, PIN, and OTP only on the secure blue·auth page."
      }
    },
    {
      "id": "input",
      "name": "Document input",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [-700, 40],
      "webhookId": "blueauth-cmd-batch-input",
      "parameters": {
        "httpMethod": "POST",
        "path": "blueauth-cmd-batch-input",
        "responseMode": "responseNode",
        "options": {}
      }
    },
    {
      "id": "validate",
      "name": "Validate batch",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-420, 40],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const body = $input.first().json.body ?? $input.first().json;\nconst docs = body.documents;\nif (!Array.isArray(docs) || docs.length < 2 || docs.length > 100) throw new Error('Send between 2 and 100 documents.');\nif (!body.signer?.email) throw new Error('signer.email is required.');\nif (!body.batchKey) throw new Error('batchKey is required for idempotency.');\nconst seen = new Set();\nfor (const [index, doc] of docs.entries()) {\n  if (!doc.pdfName || !/\\.pdf$/i.test(doc.pdfName)) throw new Error(`Document ${index + 1}: invalid pdfName.`);\n  if (!/^https:\\/\\//i.test(doc.pdfUrl || '')) throw new Error(`Document ${index + 1}: an HTTPS pdfUrl is required.`);\n  if (doc.documentHash && !/^[a-f0-9]{64}$/i.test(doc.documentHash)) throw new Error(`Document ${index + 1}: invalid documentHash.`);\n  const identity = (doc.documentHash || doc.pdfUrl).toLowerCase();\n  if (seen.has(identity)) throw new Error(`Document ${index + 1}: duplicate PDF.`);\n  seen.add(identity);\n}\nreturn [{ json: { documents: docs, signerEmail: body.signer.email, signerName: body.signer.fullName || '', idempotencyKey: body.batchKey, webhookUrl: body.webhookUrl || 'BATCH_EVENTS_URL', webhookSecret: $env.BLUEAUTH_CMD_BATCH_SECRET, validityDays: body.validityDays || 1 } }];"
      }
    },
    {
      "id": "create-batch",
      "name": "Create CMD batch",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [-120, 40],
      "parameters": {
        "method": "POST",
        "url": "https://blueauth.co/api/v1/cmd-batches",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": { "timeout": 300000 }
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE",
          "name": "BlueAuth (Authorization: Bearer ba_live_…)"
        }
      }
    },
    {
      "id": "response",
      "name": "Return secure link",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [180, 40],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ batchId: $json.batchId, signingUrl: $json.signingUrl, status: $json.status, documentCount: $json.itemCount, reused: $json.reused || false }) }}",
        "options": {}
      }
    },
    {
      "id": "events",
      "name": "CMD events",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [-420, 430],
      "webhookId": "blueauth-cmd-batch-events",
      "parameters": {
        "httpMethod": "POST",
        "path": "blueauth-cmd-batch-events",
        "responseMode": "onReceived",
        "options": { "rawBody": true }
      }
    },
    {
      "id": "verify",
      "name": "Verify HMAC",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-120, 430],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const crypto = require('crypto');\nconst incoming = $input.first().json;\nconst payload = incoming.body ?? incoming;\nconst raw = JSON.stringify(payload);\nconst header = (incoming.headers || {})['x-blueauth-signature'] || '';\nconst secret = $env.BLUEAUTH_CMD_BATCH_SECRET || '';\nif (!secret) throw new Error('BLUEAUTH_CMD_BATCH_SECRET is not configured.');\nconst expected = crypto.createHmac('sha256', secret).update(raw, 'utf8').digest('hex');\nif (!header || header.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected))) throw new Error('Invalid webhook HMAC.');\nreturn [{ json: payload }];"
      }
    },
    {
      "id": "event-switch",
      "name": "Batch result",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [170, 430],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "conditions": [
                  { "leftValue": "={{ $json.event }}", "rightValue": "cmd.batch.completed", "operator": { "type": "string", "operation": "equals" } }
                ]
              },
              "renameOutput": true,
              "outputKey": "Completed"
            },
            {
              "conditions": {
                "conditions": [
                  { "leftValue": "={{ $json.event }}", "rightValue": "cmd.batch.partial_failed", "operator": { "type": "string", "operation": "equals" } }
                ]
              },
              "renameOutput": true,
              "outputKey": "Needs attention"
            }
          ]
        },
        "options": { "fallbackOutput": "extra" }
      }
    }
  ],
  "connections": {
    "Document input": { "main": [[{ "node": "Validate batch", "type": "main", "index": 0 }]] },
    "Validate batch": { "main": [[{ "node": "Create CMD batch", "type": "main", "index": 0 }]] },
    "Create CMD batch": { "main": [[{ "node": "Return secure link", "type": "main", "index": 0 }]] },
    "CMD events": { "main": [[{ "node": "Verify HMAC", "type": "main", "index": 0 }]] },
    "Verify HMAC": { "main": [[{ "node": "Batch result", "type": "main", "index": 0 }]] }
  },
  "pinData": {},
  "settings": { "executionOrder": "v1" },
  "active": false,
  "versionId": "blueauth-cmd-batch-public-v2"
}
