Webhooks come in two flavours, both in Settings → Webhooks:
- Outbound — Groundbase POSTs events to a URL of your choice whenever something happens in your CRM (contact created, deal won, SMS received, etc.). Use this to push CRM data into Zapier, Make, n8n, Slack, a custom server, anything that can receive HTTPS.
- Inbound — Groundbase exposes a URL that the outside world POSTs to. Each POST creates or updates entities (contacts, tags, notes, tasks…). Use this to receive leads from Facebook Lead Ads, Typeform, Calendly, custom contact forms, or anything that can send HTTPS.
Pick the tab in Settings → Webhooks for the direction you want.
Outbound webhooks
Adding an outbound webhook
Settings → Webhooks → Add webhook.
- Endpoint URL — where we POST events (must be
https://for production) - Description — optional internal label
- Events — tick "All events (*)" to get everything, or pick specific event types (chips below the All toggle)
- Click Add webhook
- Save the signing secret that appears in the dialog. We never show it again. Lose it → just delete the webhook and create a new one.
What we send
Every event is a POST request with this JSON body:
{
"id": "f8a1c4e2-9b3d-4f72-a8e1-1c2b3d4e5f60",
"type": "contact.created",
"created_at": "2026-06-09T14:23:11.842Z",
"user_id": "a26a6272-63d9-421e-a5ef-a9398620f739",
"entity_id": "b502bd84-7482-4c6c-981a-3762ee26ba6f",
"data": {
"contact_id": "b502bd84-7482-4c6c-981a-3762ee26ba6f",
"first_name": "Adam",
"last_name": "Rao",
"email": null,
"phone": "14164515566"
}
}
…and these headers:
Content-Type: application/json
User-Agent: Groundbase-Webhook/1.0
X-GB-Event: contact.created
X-GB-Delivery: <uuid> — unique per attempt; survive your idempotency check
X-GB-Signature: sha256=<64 hex chars>
Verifying the signature
X-GB-Signature is sha256=<hex of HMAC-SHA256(secret, raw body bytes)>. Compute the same hash on your end and compare — equal means the payload came from us and wasn't tampered with.
Node.js / Express:
const crypto = require('crypto');
app.post('/webhooks/groundbase', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-gb-signature'] || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.GB_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('Bad signature');
}
const event = JSON.parse(req.body.toString());
console.log('Got', event.type, event.entity_id);
res.status(200).send('ok');
});
Python / Flask:
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ['GB_WEBHOOK_SECRET'].encode()
@app.route('/webhooks/groundbase', methods=['POST'])
def hook():
sig = request.headers.get('X-GB-Signature', '')
expected = 'sha256=' + hmac.new(SECRET, request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
event = request.get_json()
print('Got', event['type'], event['entity_id'])
return 'ok', 200
Reliability
- Timeout: 15 seconds per attempt. Respond fast.
- Retries: 5 attempts on non-2xx, with exponential backoff at 1 min → 5 min → 30 min → 2 hours → 12 hours. After that the delivery is marked
deadand we stop trying. - Auto-pause: after 20 consecutive failed deliveries we pause the subscription. Fix your receiver, then re-enable from the Settings UI (the failure counter resets).
- Idempotency: the same event can be retried with the same
X-GB-Deliveryid. Use it as a dedup key on your side — saving by it makes re-deliveries safe. - Delivery log: Click any subscription row to see the last 100 attempts, with status code, retry count, and response body (first 4KB).
Available event types
24 event types currently fire:
Contacts — contact.created, contact.updated, contact.deleted
Companies — company.created, company.updated, company.deleted
Deals — deal.created, deal.updated, deal.deleted, deal.stage_changed (includes from/to stage)
Tasks — task.created, task.updated, task.completed, task.deleted
Notes — note.created
Tags — tag.applied
SMS — sms.sent, sms.received, sms.delivered (Twilio confirmation), sms.failed
Calls — call.completed, voicemail.received
Email — email.sent
Use ["*"] as the events array to subscribe to all of them.
Limits
- Max 20 active subscriptions per account. Hit the limit? Delete one before adding more.
- HTTPS strongly recommended. We accept
http://for testing only (e.g. webhook.site). - Body size: ~10-50 KB typical, never larger than 200 KB.
Test it before going live
In the Settings → Webhooks list, click Test on a subscription row. We queue a synthetic contact.created event with entity_id="test-event" and data.test=true. Should hit your endpoint within ~60 seconds (the per-minute delivery cron).
webhook.site is the easiest sandbox — open the page, copy the unique URL it gives you, paste it into a Groundbase webhook subscription, fire a Test, and watch the request show up live.
Inbound webhooks
Inbound webhooks let external systems push data into Groundbase. Each inbound webhook gives you a unique URL — when something POSTs JSON to that URL, Groundbase creates contacts, applies tags, drops notes, etc. based on the body.
Adding an inbound webhook
Settings → Webhooks → Inbound tab → Add inbound webhook.
- Name — a label like "Facebook Lead Ads" or "Typeform contact form"
- Description — optional internal notes
- Require HMAC signature — optional extra security layer. Skip unless your sender supports it (Stripe, GitHub, your own code). The URL token alone is already a long random secret.
- Click Create webhook
- Save the URL that appears. We never show the full URL again. (You can always rotate to a fresh one from the list.) If you ticked HMAC, save the signing secret too.
How it works — native mode
The simplest setup: POST a JSON body in Groundbase's native shape.
curl -X POST 'https://api.groundbasecrm.com/inbound/<your-token>' \
-H 'Content-Type: application/json' \
-d '{
"event": "contact.create",
"data": {
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com",
"phone": "+14165551234",
"tags": ["lead", "fb-ads"]
}
}'
That single POST creates a contact AND applies both tags. Most automation tools (Zapier, Make, n8n) can shape any payload into this format — point your tool at the URL and configure the body template.
Supported native events
- Contacts —
contact.create,contact.update,contact.find_or_create- Update matches by
id,email, orphone(in that order) - Both
createandupdateaccept atags: [...]array applied at write time
- Update matches by
- Companies —
company.create,company.find_or_create(matched byname, case-insensitive) - Deals —
deal.create(acceptstitle,value_centsorvaluein dollars,pipeline_id,stage_id, links to contact/company) - Notes —
note.create(target viacontact_id,company_id, ordeal_id, plus abody) - Tasks —
task.create(title+ optionaldue_at,description,kind,location, contact link) - Tags —
tag.apply/tag.remove. Acceptsentity_type+entity_id. For contacts you can useemailorphoneinstead ofentity_id.
Tags are case-insensitive and auto-created on first use.
Facebook Lead Ads example — mapped mode
FB Lead Ads sends payloads that don't match Groundbase's native shape. Use Mapped mode in Settings → Webhooks → Inbound → Add inbound webhook → "Mapped" tile.
- Paste a real sample FB Lead payload into the Sample payload box. Groundbase parses it and lights up every field path so you can pick from a dropdown.
- Click Add action. Pick
contact.find_or_createas the event. - Map the FB fields onto Groundbase fields:
first_name←lead.full_name.first(or wherever FB puts it in your sample)last_name←lead.full_name.lastemail←lead.emailphone←lead.phone_number
- Add a static value: key =
tags, value =lead,fb-ads— Groundbase will apply both tags to every new contact. - Add a second action:
note.createwith body field mapped to a custom message andcontact_idmapped from the contact you just upserted (or use email/phone matching —contact.find_or_createreturned the contact, and any subsequent action with the same email/phone resolves to the same row). - Live preview at the bottom of each action card shows exactly what Groundbase will receive for that action — verify before saving.
The two actions run in order on every POST. Re-saves are immediate. Use the edit icon on the row to come back and tweak the mapping later.
If your sender isn't supported well by mapped mode, you can also use native mode through a middleman like Zapier or Make — set the trigger to "New Lead in Facebook Lead Ads", set the action to "Webhook POST" with your Groundbase inbound URL, and build the body in { event, data } shape.
Security
- URL token — the secret part of your inbound URL is 48 random URL-safe characters. Treat it like a password. Anyone with the URL can write to your CRM.
- HMAC signature (optional) — when enabled, every POST must include
X-Groundbase-Signature: sha256=<hex>where the hex is HMAC-SHA256 of the raw body using the signing secret. We acceptX-Hub-Signature-256(GitHub style) andX-Webhook-Signatureas aliases. Mismatch → 401. - IP allowlist (optional, configured via API) — limit which source IPs can POST to your URL.
- Rotate any time — the URL button on each row mints a new token; the old URL stops working immediately.
Reliability
- Body size cap — 64 KB. Larger payloads → 413.
- Deduplication — we deduplicate by
external_idif your payload has one (we recognizeid,event_id,external_id,leadgen_id,response_id,booking_id), otherwise by SHA-256 of the body, in a 24-hour window. Re-sending the same payload returns 200 withstatus: "duplicate"and skips processing — safe to retry. - Auto-pause — 50 consecutive processing failures pauses the webhook. Re-enable from Settings once the sender is fixed. Signature/IP rejections do NOT count — those are sender misconfig, not webhook issues.
- Delivery log — every POST is logged with payload preview, source IP, status, and per-action outcome. Last 100 visible by expanding any webhook row.
Test it
# Quick smoke test from a terminal:
curl -X POST 'https://api.groundbasecrm.com/inbound/<your-token>' \
-H 'Content-Type: application/json' \
-d '{"event":"contact.create","data":{"first_name":"Test","email":"test@example.com","tags":["webhook-test"]}}'
Should return {"ok": true, "status": "processed", "actions": [...]}. Refresh your contacts list — there should be a "Test" contact with the webhook-test tag applied.