{
  "name": "meiti <-> HubSpot (Tickets)",
  "nodes": [
    {
      "parameters": {
        "content": "## meiti <-> HubSpot (Tickets) - 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.tickets.read/write`. Copy the key.\n_Tickets require HubSpot Service Hub (the ticket scopes only appear when it is enabled)._\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.\nTicket -> find the latest *open* ticket 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/tickets/search",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ filterGroups: [{ filters: [ { propertyName: 'associations.contact', operator: 'EQ', value: $json.contactId }, { propertyName: 'hs_pipeline_stage', operator: 'NOT_IN', values: ['4'] } ] }], sorts: [{ propertyName: 'createdate', direction: 'DESCENDING' }], properties: ['hs_pipeline_stage'], limit: 1 }) }}",
        "options": {}
      },
      "id": "search-ticket",
      "name": "Search Ticket",
      "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": "ticket-found",
      "name": "Ticket Found",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1900,
        240
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/tickets",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ properties: { subject: (($('meiti Webhook').item.json.body.projectData||{}).projectName || ('meiti - ' + ($('meiti Webhook').item.json.body.eventType || 'Manual'))), hs_pipeline: '0', hs_pipeline_stage: '1', content: [ (($('meiti Webhook').item.json.body.projectData||{}).inquirySummary ? ('Anfrage:\\n' + $('meiti Webhook').item.json.body.projectData.inquirySummary) : null), (($('meiti Webhook').item.json.body.projectData||{}).currentSummary ? ('Aktuell:\\n' + $('meiti Webhook').item.json.body.projectData.currentSummary) : null) ].filter(Boolean).join('\\n\\n').slice(0, 65000) }, associations: [{ to: { id: $('Resolve Contact').item.json.contactId }, types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 16 }] }] }) }}",
        "options": {}
      },
      "id": "create-ticket",
      "name": "Create Ticket",
      "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-ticket",
      "name": "Resolve Ticket",
      "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 Ticket",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Ticket": {
      "main": [
        [
          {
            "node": "Ticket Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ticket Found": {
      "main": [
        [
          {
            "node": "Resolve Ticket",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create Ticket",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Ticket": {
      "main": [
        [
          {
            "node": "Resolve Ticket",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolve Ticket": {
      "main": [
        [
          {
            "node": "Build Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Response": {
      "main": [
        [
          {
            "node": "Respond to meiti",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}