API and Recipes
GoHighLevel Inbound Webhooks: Stripe and Shopify Payment Event Recipes
By Marnix Geerkens. Published 2026-05-28. Updated 2026-05-28.
In short
An inbound webhook in GoHighLevel is a URL you give to an external service like Stripe or Shopify. When a payment succeeds, the service POSTs event data to that URL, and GHL fires a workflow. You can map fields from the payload to contact custom fields, update opportunity stages, send confirmation messages, and add tags, all without writing any server code.
- Create a workflow with the "Inbound Webhook" trigger to get a unique GHL webhook URL.
- Paste that URL into Stripe's webhook settings or Shopify's webhook endpoint config.
- Map payload fields (customer email, amount, order ID) to GHL contact custom fields in the workflow.
- Always verify the webhook signature to ensure the payload is genuine before acting on it.
Endpoints
| Method | Path | Scopes | Rate limit |
|---|---|---|---|
| POST | https://backend.leadconnectorhq.com/hooks/WORKFLOW_ID/webhook-trigger | No token required (inbound webhook URL) | See GHL API docs |
| GET | /workflows | workflows.readonly | See GHL API docs |
Authentication
Inbound webhook URLs are public by default. Anyone who knows the URL can POST to it. This is why you must verify the signature on every incoming request before acting on the data.
GoHighLevel inbound webhooks do not natively verify Stripe or Shopify signatures. You need a small middleware function (a serverless function or an edge function) that verifies the signature and then forwards the sanitized payload to the GHL webhook URL.
The GHL webhook URL is generated when you save the workflow with the "Inbound Webhook" trigger. Copy it from the trigger settings. It looks like: https://backend.leadconnectorhq.com/hooks/WORKFLOW_ID/webhook-trigger.
Alternatively, use GHL's native Stripe integration if it covers your use case. The native integration handles payment events with no custom code. The recipes below are for scenarios where you need more control or are using Shopify.
Recipe 1. Verify a Stripe webhook signature in a serverless function
Stripe signs every webhook with an HMAC-SHA256 signature in the Stripe-Signature header. Verify it with the Stripe SDK before forwarding the event to GHL. If verification fails, return 400 and do nothing.
Deploy this as a Vercel Edge Function, a Cloudflare Worker, or an AWS Lambda. The function verifies the signature, extracts the fields you need, and POSTs them to your GHL inbound webhook URL.
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const GHL_WEBHOOK_URL = process.env.GHL_INBOUND_WEBHOOK_URL;
export async function POST(req) {
const rawBody = await req.text();
const sig = req.headers.get('stripe-signature');
let event;
try {
event = stripe.webhooks.constructEvent(
rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return new Response('Signature verification failed', { status: 400 });
}
if (event.type === 'payment_intent.succeeded') {
const pi = event.data.object;
await fetch(GHL_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: pi.receipt_email,
amount: pi.amount / 100, // convert cents to dollars
currency: pi.currency,
paymentIntentId: pi.id,
description: pi.description,
}),
});
}
return new Response('ok', { status: 200 });
}import json
import os
import stripe
import requests
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
GHL_WEBHOOK_URL = os.environ["GHL_INBOUND_WEBHOOK_URL"]
def handler(event, context):
body = event["body"]
sig = event["headers"].get("stripe-signature", "")
try:
evt = stripe.Webhook.construct_event(
body, sig, os.environ["STRIPE_WEBHOOK_SECRET"]
)
except stripe.error.SignatureVerificationError:
return {"statusCode": 400, "body": "Bad signature"}
if evt["type"] == "payment_intent.succeeded":
pi = evt["data"]["object"]
requests.post(
GHL_WEBHOOK_URL,
json={
"email": pi.get("receipt_email"),
"amount": pi["amount"] / 100,
"currency": pi["currency"],
"paymentIntentId": pi["id"],
},
)
return {"statusCode": 200, "body": "ok"}Recipe 2. Forward a Shopify order to GHL
Shopify sends an HMAC-SHA256 signature in the X-Shopify-Hmac-SHA256 header. Verify it before forwarding. Then extract the customer email, order total, and any line items you want to map to GHL custom fields.
In the GHL workflow, map the incoming JSON fields to contact custom fields and add tags like "paying-customer" or "shopify-order".
import crypto from 'crypto';
const GHL_WEBHOOK_URL = process.env.GHL_INBOUND_WEBHOOK_URL;
const SHOPIFY_SECRET = process.env.SHOPIFY_WEBHOOK_SECRET;
export async function POST(req) {
const rawBody = await req.text();
const hmacHeader = req.headers.get('x-shopify-hmac-sha256');
const digest = crypto
.createHmac('sha256', SHOPIFY_SECRET)
.update(rawBody, 'utf8')
.digest('base64');
if (digest !== hmacHeader) {
return new Response('Bad HMAC', { status: 401 });
}
const order = JSON.parse(rawBody);
await fetch(GHL_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: order.email,
firstName: order.billing_address?.first_name,
lastName: order.billing_address?.last_name,
phone: order.billing_address?.phone,
orderTotal: order.total_price,
orderId: order.id,
tags: ['shopify-order', 'paying-customer'],
}),
});
return new Response('ok', { status: 200 });
}import hashlib
import hmac
import json
import os
import requests
from flask import Flask, request, abort
app = Flask(__name__)
GHL_WEBHOOK_URL = os.environ["GHL_INBOUND_WEBHOOK_URL"]
SHOPIFY_SECRET = os.environ["SHOPIFY_WEBHOOK_SECRET"].encode()
@app.route("/shopify-order", methods=["POST"])
def shopify_order():
raw = request.get_data()
digest = hmac.new(SHOPIFY_SECRET, raw, hashlib.sha256).digest()
import base64
expected = base64.b64encode(digest).decode()
if not hmac.compare_digest(expected, request.headers.get("X-Shopify-Hmac-SHA256", "")):
abort(401)
order = json.loads(raw)
requests.post(GHL_WEBHOOK_URL, json={
"email": order.get("email"),
"orderTotal": order.get("total_price"),
"orderId": order.get("id"),
"tags": ["shopify-order", "paying-customer"],
})
return "ok", 200Recipe 3. Map payload fields to a GHL contact in the workflow
Inside your GHL workflow, after the Inbound Webhook trigger, add an "Update Contact" action. Use the field-mapping UI to pull values from the incoming payload (accessed as {{trigger.email}}, {{trigger.orderTotal}}, etc.) and write them to contact fields.
Add a "Add Tag" action with the tags you forwarded in the payload. Add a "Create or Update Opportunity" action to log the payment as a won deal in your pipeline.
Trigger: Inbound Webhook
Webhook URL: (auto-generated, copy from trigger settings)
Action 1: Find Contact (search by {{trigger.email}})
Action 2: Update Contact
- First Name: {{trigger.firstName}}
- Last Name: {{trigger.lastName}}
- Custom Field "Last Order Total": {{trigger.orderTotal}}
- Custom Field "Last Order ID": {{trigger.orderId}}
Action 3: Add Tag
- Tags: {{trigger.tags}} (or hardcode: shopify-order, paying-customer)
Action 4: Create Opportunity
- Pipeline: Sales Pipeline
- Stage: Closed Won
- Monetary Value: {{trigger.orderTotal}}
- Status: won
Action 5: Send Confirmation SMS or EmailCommon errors and fixes
GHL never receives the event: the middleware function URL you gave Stripe or Shopify is wrong, or the function is not deployed. Test the URL with a manual curl POST first.
Signature verification always fails: you are verifying the parsed body instead of the raw bytes. Always read the raw request body before passing it to the HMAC check.
200 from GHL but contact not updated: the email field in your payload does not match any contact. Add a "Create Contact" step before the update step to handle new customers.
Workflow not triggering: the GHL workflow must be published (not draft) and the inbound webhook trigger must be active. Check the workflow status in GHL.
Copy as an MCP tool
{
"name": "trigger_ghl_webhook",
"description": "POST a payment event payload to a GoHighLevel inbound webhook URL to trigger a workflow.",
"inputSchema": {
"type": "object",
"properties": {
"webhookUrl": { "type": "string", "description": "The GHL inbound webhook URL" },
"email": { "type": "string" },
"orderTotal": { "type": "number" },
"orderId": { "type": "string" },
"tags": { "type": "array", "items": { "type": "string" } }
},
"required": ["webhookUrl", "email"]
}
}You need a GoHighLevel account to use the API. Start the 30-day trial through our link.
Frequently asked questions
Can I skip the middleware and send Stripe directly to GHL?
You can, but you lose signature verification. Without it, anyone who finds your GHL webhook URL can POST fake payment events. The middleware step is a one-time setup and protects you from bad data corrupting your contacts.
Does GoHighLevel have a native Stripe integration?
Yes. GHL has a Payments section with a native Stripe connection. If your use case fits (standard checkout, order management), the native integration is simpler. The middleware recipe is for situations where you need custom field mapping or are processing events from multiple payment sources.
How do I test the inbound webhook without real payments?
Use the Stripe CLI to send test events: "stripe trigger payment_intent.succeeded". For Shopify, use the webhook test feature in the Shopify admin (Settings > Notifications > Webhooks > Send test notification). In both cases, check the GHL workflow execution log to confirm the payload arrived and fields were mapped correctly.
What happens if GHL returns an error on my POST?
Both Stripe and Shopify retry failed webhook deliveries on a schedule (Stripe retries over 3 days, Shopify over 48 hours). Design your middleware to return 200 only after the GHL POST succeeds, so retries work correctly.
