{
  "name": "meiti <-> HubSpot (Deals)",
  "nodes": [
    {
      "parameters": {
        "content": "## meiti <-> HubSpot (Deals) - setup\n\n**1. HubSpot:** Settings -> Integrations -> **Service Keys** -> create a Service Key\n(HubSpot's modern replacement for legacy private apps).\nScopes (least privilege): `crm.objects.contacts.read/write` and `crm.objects.deals.read/write`. Copy the key.\n_Deals live in core CRM on every HubSpot tier._\n\n**2. n8n credential:** when prompted, create a **Bearer Auth** credential and paste your\nService Key as the **Token** (n8n adds the \"Authorization: Bearer\" header). One credential, all nodes.\n\n**3. meiti:** copy this workflow's **Production URL** (meiti Webhook node) into\nmeiti -> CRM Synchronisation -> Webhook. Activate the workflow, then test with the project\nWebhook Trigger button.\n\n---\n**Logic (mirrors meiti CRM adapter):** Contact -> find by phone/email, else create.\nDeal -> find the latest *open* deal for the contact, else create one linked to it.\nReturns both ids to meiti, plus any contact fields HubSpot knows that meiti is missing (e.g. name).\n\nWARNING - Security: you SHOULD verify the request is from meiti. This template does not,\nto keep setup simple. meiti sends the Bearer token you set in its webhook settings. To enforce\nit, add an IF after *meiti Webhook* checking `{{ $json.headers.authorization }}` equals\n`Bearer <your-token>` before continuing.",
        "height": 500,
        "width": 470,
        "color": 4
      },
      "id": "setup-note",
      "name": "Setup & Security",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -220,
        110
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "meiti-hubspot",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "meiti-webhook",
      "name": "meiti Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        360,
        300
      ],
      "webhookId": "meiti-hubspot"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-1",
              "leftValue": "={{ (( $json.body.contactData || {} ).phoneNumber || ( $json.body.contactData || {} ).email || '').toString().trim().length > 0 }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ]
        },
        "options": {}
      },
      "id": "has-identifier",
      "name": "Has Identifier",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        580,
        300
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ {} }}",
        "options": {}
      },
      "id": "respond-empty",
      "name": "Respond Empty",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        800,
        460
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/contacts/search",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ query: (( $json.body.contactData || {} ).phoneNumber || ( $json.body.contactData || {} ).email || '').toString().trim(), properties: ['email','phone','firstname','lastname','company','city','address','zip'], limit: 1 }) }}",
        "options": {}
      },
      "id": "search-contact",
      "name": "Search Contact",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        800,
        240
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-1",
              "leftValue": "={{ $json.total }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ]
        },
        "options": {}
      },
      "id": "contact-found",
      "name": "Contact Found",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1020,
        240
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/contacts",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ properties: Object.fromEntries(Object.entries({ firstname: ($('meiti Webhook').item.json.body.contactData||{}).firstName, lastname: ($('meiti Webhook').item.json.body.contactData||{}).lastName, email: ($('meiti Webhook').item.json.body.contactData||{}).email, phone: ($('meiti Webhook').item.json.body.contactData||{}).phoneNumber, company: ($('meiti Webhook').item.json.body.contactData||{}).company, city: ($('meiti Webhook').item.json.body.contactData||{}).city, address: ($('meiti Webhook').item.json.body.contactData||{}).addressLine1, zip: ($('meiti Webhook').item.json.body.contactData||{}).postCode }).filter(function(e){ return e[1] != null && e[1] !== ''; })) }) }}",
        "options": {}
      },
      "id": "create-contact",
      "name": "Create Contact",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1240,
        360
      ]
    },
    {
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "asg-1",
              "name": "contactId",
              "value": "={{ $json.results ? $json.results[0].id : $json.id }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": false,
        "options": {}
      },
      "id": "resolve-contact",
      "name": "Resolve Contact",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1460,
        240
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/deals/search",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ filterGroups: [{ filters: [ { propertyName: 'associations.contact', operator: 'EQ', value: $json.contactId }, { propertyName: 'dealstage', operator: 'NOT_IN', values: ['closedwon','closedlost'] } ] }], sorts: [{ propertyName: 'createdate', direction: 'DESCENDING' }], properties: ['dealstage'], limit: 1 }) }}",
        "options": {}
      },
      "id": "search-deal",
      "name": "Search Deal",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1680,
        240
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-1",
              "leftValue": "={{ $json.total }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ]
        },
        "options": {}
      },
      "id": "deal-found",
      "name": "Deal Found",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1900,
        240
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/deals",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ properties: { dealname: (($('meiti Webhook').item.json.body.projectData||{}).projectName || ('meiti - ' + ($('meiti Webhook').item.json.body.eventType || 'Manual'))), pipeline: 'default', dealstage: 'appointmentscheduled' }, associations: [{ to: { id: $('Resolve Contact').item.json.contactId }, types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 3 }] }] }) }}",
        "options": {}
      },
      "id": "create-deal",
      "name": "Create Deal",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2120,
        360
      ]
    },
    {
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "asg-1",
              "name": "projectId",
              "value": "={{ $json.results ? $json.results[0].id : $json.id }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": false,
        "options": {}
      },
      "id": "resolve-deal",
      "name": "Resolve Deal",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2340,
        240
      ]
    },
    {
      "parameters": {
        "jsCode": "const body = $('meiti Webhook').first().json.body || {};\nconst c = body.contactData || {};\nconst searchOut = $('Search Contact').first().json || {};\nconst hs = ((searchOut.results && searchOut.results[0]) ? searchOut.results[0].properties : {}) || {};\nconst contactId = $('Resolve Contact').first().json.contactId;\nconst projectId = $input.first().json.projectId;\nconst fill = (m, h) => (m && String(m).trim()) ? null : (h || null);\nconst contactData = { crmContactId: String(contactId) };\nconst pairs = [\n  ['firstName', c.firstName, hs.firstname],\n  ['lastName', c.lastName, hs.lastname],\n  ['email', c.email, hs.email],\n  ['company', c.company, hs.company],\n  ['city', c.city, hs.city],\n  ['addressLine1', c.addressLine1, hs.address],\n  ['postCode', c.postCode, hs.zip],\n];\nfor (const p of pairs) { const v = fill(p[1], p[2]); if (v) contactData[p[0]] = v; }\nreturn [{ json: { requestContactUpdate: true, contactData, requestProjectUpdate: true, projectData: { crmProjectId: String(projectId) } } }];"
      },
      "id": "build-response",
      "name": "Build Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2560,
        240
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {}
      },
      "id": "respond-to-meiti",
      "name": "Respond to meiti",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2780,
        240
      ]
    }
  ],
  "connections": {
    "meiti Webhook": {
      "main": [
        [
          {
            "node": "Has Identifier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Identifier": {
      "main": [
        [
          {
            "node": "Search Contact",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Empty",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Contact": {
      "main": [
        [
          {
            "node": "Contact Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Contact Found": {
      "main": [
        [
          {
            "node": "Resolve Contact",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Contact": {
      "main": [
        [
          {
            "node": "Resolve Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolve Contact": {
      "main": [
        [
          {
            "node": "Search Deal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Deal": {
      "main": [
        [
          {
            "node": "Deal Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Deal Found": {
      "main": [
        [
          {
            "node": "Resolve Deal",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create Deal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Deal": {
      "main": [
        [
          {
            "node": "Resolve Deal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolve Deal": {
      "main": [
        [
          {
            "node": "Build Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Response": {
      "main": [
        [
          {
            "node": "Respond to meiti",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}