Integrations / Reply handoff

First reply to HubSpot deal + Slack

When a contact replies to your LinkedIn outreach for the first time, create a deal in HubSpot and post the message to a Slack channel so your team can jump on the warm lead.

Why this matters

The first reply is the most valuable signal in outbound. Most teams don't learn about it until someone reads the inbox manually, by which point the lead has cooled. A Slack ping plus an auto-created deal closes the gap from minutes to seconds.

The trigger

  • Event name: contact.first_inbound_message
  • Fires: when an inbound message arrives from a contact who has never previously sent you one. The is_first filter is implicit in the event itself; you can also set is_first: true defensively.
  • Idempotency: the same delivery is retried up to 5 times on 5xx responses. Use Webhook-Event-Id to dedupe before creating the deal.

Setup

Make.com handles the fan-out cleanly. n8n has a similar shape; replace the Make modules with HTTP Request nodes.

  1. In Make, create a scenario with a Webhooks → Custom webhook trigger. Copy the URL.
  2. Subscribe Crispy to contact.first_inbound_message:
    curl -X POST https://crispy.sh/api/v1/subscriptions \
      -H "Authorization: Bearer crispy_your_api_key" \
      -H "Content-Type: application/json" \
      -d '{
        "event": "contact.first_inbound_message",
        "target_url": "https://hook.us2.make.com/your-make-webhook-id",
        "filter": {
          "is_first": true
        }
      }'
  3. Verify the signature using the HMAC verification snippet. In Make, use a Tools → Run JavaScript module before any side effects.
  4. Add a Router module with two parallel branches:
    • Branch A: HubSpot → Create Deal. Map the contact email and any custom_attrs you want on the deal record. Use payload.data.contact.external_ids.hubspot_contact_id if you imported it.
    • Branch B: Slack → Create a message. Post to #replies with the contact name and a snippet of payload.data.message.text.
  5. Activate the scenario. New replies will fan out within seconds.

Sample payload

{
  "payload_version": "1",
  "id": "00000000-0000-0000-0000-000000000901",
  "event": "contact.first_inbound_message",
  "timestamp": "2026-04-29T14:12:33.000Z",
  "webhook_id": "00000000-0000-0000-0000-0000000009h2",
  "account_id": "00000000-0000-0000-0000-0000000009a1",
  "data": {
    "contact": {
      "id": "00000000-0000-0000-0000-0000000009c2",
      "workspace_id": "00000000-0000-0000-0000-0000000009w1",
      "account_id": "00000000-0000-0000-0000-0000000009a1",
      "connection_status": "connected",
      "current_campaign_id": "00000000-0000-0000-0000-0000000009p1",
      "tags": ["q4-target"],
      "custom_attrs": {
        "industry": "SaaS",
        "title": "VP Engineering"
      },
      "external_ids": {
        "hubspot_contact_id": "hs-12345"
      }
    },
    "message": {
      "id": "msg_abc123",
      "text": "Thanks for reaching out — yes, I'd like to learn more.",
      "received_at": "2026-04-29T14:12:30.000Z"
    },
    "source_event": "message.inbound_received",
    "triggered_at": "2026-04-29T14:12:33.000Z"
  }
}

Troubleshooting

I'm getting duplicate Slack messages.
Crispy retries on 5xx. Add a dedup step keyed on Webhook-Event-Id before creating the HubSpot deal or posting to Slack.
HubSpot is rejecting the contact email as already existing.
Use HubSpot's “Upsert” pattern: search by email first, attach the deal to the existing contact if found, otherwise create-then-attach. Or pre-populate external_ids.hubspot_contact_id on import so the payload already carries the link.