Webhook Guide
Webhooks let external services trigger agent runs in Upland Hub automatically. Instead of manually going to Agent Studio and clicking "Run," an external system sends an HTTP request and the agent executes.
When to Use Webhooks
| Scenario | Trigger Source | Agent |
|---|---|---|
| New Google review comes in | Zapier / Google Alerts | review_response |
| New lead in your CRM | HubSpot / Salesforce / Airtable | email_campaign |
| Client posts a new menu | Website CMS webhook | social_post |
| Weekly scheduled content | Cron job / Zapier Schedule | content_calendar |
| New product listed | E-commerce platform | social_post |
| Competitor posts something | Social monitoring tool | competitor_watch |
Setup
Step 1: Generate a Webhook Secret
Each organization needs a webhook secret. Generate one via the API:
# Get your access token first
TOKEN=$(curl -s http://localhost:8788/api/auth/dev-login -X POST | jq -r '.data.access_token')
# Generate webhook secret for a client org
curl -X POST http://localhost:8788/api/webhooks/ORG_ID/generate-secret \
-H "Authorization: Bearer $TOKEN"
Response:
{
"data": {
"webhook_secret": "whsec_a1b2c3d4e5f6...",
"webhook_url_template": "https://your-api.com/api/webhooks/ORG_ID/{agent_type}?secret=whsec_a1b2c3d4e5f6..."
}
}
Save the webhook_secret -- you will need it for every webhook call to this org.
Step 2: Construct the Webhook URL
The webhook URL follows this pattern:
POST /api/webhooks/:orgId/:agentType?secret=:webhookSecret
Parameters:
:orgId-- the organization ID (e.g.,org_abc123):agentType-- the agent to trigger (e.g.,review_response,social_post,email_campaign):webhookSecret-- the secret generated in Step 1
Example:
POST https://your-api.com/api/webhooks/org_abc123/review_response?secret=whsec_a1b2c3d4e5f6...
Step 3: Send Agent Input in the Request Body
The request body is a JSON object containing the agent's input fields. Different agents expect different fields (see the Agent Reference for each agent's input fields).
curl -X POST "http://localhost:8788/api/webhooks/org_abc123/review_response?secret=whsec_a1b2c3..." \
-H "Content-Type: application/json" \
-d '{
"platform": "Google",
"rating": 5,
"reviewer_name": "Mike T",
"review_text": "Best cocktails in town. The Smoked Old Fashioned is incredible."
}'
Response (201 Created):
{
"data": {
"run_id": "agent_run_xyz789",
"status": "queued",
"agent_type": "review_response"
}
}
The agent run is now queued and will execute asynchronously. Check its status via the API (see API Quickstart).
Example: Zapier Integration for Google Reviews
Automatically draft review responses when a new Google review comes in.
Prerequisites
- Zapier account (free tier works)
- Google Business Profile connected to Zapier
- Webhook secret generated for the client org
Zapier Zap Configuration
Trigger:
- App: Google My Business
- Event: New Review
- Account: Connect the client's Google account
Action:
- App: Webhooks by Zapier
- Event: POST
- URL:
https://your-api.com/api/webhooks/org_abc123/review_response?secret=whsec_a1b2c3... - Payload Type: JSON
- Data:
| Key | Value (Zapier fields) |
|---|---|
platform | Google |
rating | {{Review Star Rating}} |
reviewer_name | {{Reviewer Name}} |
review_text | {{Review Comment}} |
date | {{Review Create Time}} |
What Happens
- Customer leaves a Google review
- Zapier detects it (checks every 1-15 minutes depending on plan)
- Zapier sends a POST request to Upland Hub's webhook endpoint
- The Review Response agent generates a draft response
- The response appears in the Review Queue for approval
- You (or the client) approve it
- If Google Business Profile is connected, the response auto-publishes
Example: CRM Integration for Email Campaigns
Trigger a personalized email campaign when a new lead enters your CRM.
Setup with Make (Integromat) or Zapier
Trigger: New contact in HubSpot/Airtable/Google Sheets
Action: HTTP POST to:
https://your-api.com/api/webhooks/org_abc123/email_campaign?secret=whsec_a1b2c3...
Body:
{
"campaign_type": "welcome",
"purpose": "Welcome new email subscriber and introduce the business",
"audience_segment": "New subscribers this week",
"key_message": "Welcome to the family - here's what we're about"
}
What Happens
- New lead enters the CRM
- Automation sends webhook to Upland Hub
- Email Campaign agent generates a welcome email with subject line variants, body, and CTA
- Content appears in Review Queue
- Approve and send via your email platform (Resend, Mailchimp, etc.)
Example: Scheduled Content Calendar via Cron
Trigger a monthly content calendar generation on the 1st of every month.
Using a Cron Service (e.g., cron-job.org, EasyCron)
Set up a monthly cron job that hits:
curl -X POST "https://your-api.com/api/webhooks/org_abc123/content_calendar?secret=whsec_a1b2c3..." \
-H "Content-Type: application/json" \
-d '{
"month": "April 2026",
"posts_per_week": 4,
"platforms": ["instagram", "facebook"]
}'
Schedule: 0 9 1 * * (9 AM on the 1st of every month)
Error Handling
Error Responses
| Status | Meaning | Response Body |
|---|---|---|
| 201 | Success -- run queued | { "data": { "run_id": "...", "status": "queued", "agent_type": "..." } } |
| 401 | Invalid or missing webhook secret | { "error": "Invalid webhook secret" } |
| 402 | Organization has no credits remaining | { "error": "Insufficient credits" } |
| 404 | Organization not found, inactive, or agent not enabled | { "error": "Organization not found or inactive" } or { "error": "Agent X is not enabled for this organization" } |
Common Issues
"Invalid webhook secret"
- Double-check the
secretquery parameter matches the one generated for this org - Secrets start with
whsec_ - Regenerate if lost -- there is no way to retrieve an existing secret
"Agent X is not enabled for this organization"
- The agent must be enabled in the client's agent configs
- Check via:
GET /api/agents/orgs/:orgIdwith an auth token
"Insufficient credits"
- The org has 0 credits remaining
- Credits reset monthly (automatic cron) or can be topped up manually
- Check balance via:
GET /api/orgs/:orgIdwith an auth token
"Organization not found or inactive"
- The org ID is wrong, or the org has been deactivated
- Double-check the org ID in the webhook URL
Rate Limits
Webhooks are rate-limited to 30 requests per minute per IP address. This is enforced via Cloudflare KV-based rate limiting.
If you exceed the limit, you will receive a 429 Too Many Requests response. Wait and retry.
For high-volume integrations, batch your requests or space them out.
Security Best Practices
- Never expose webhook secrets in client-side code. Secrets should only be stored in server-side configurations (Zapier, Make, your backend).
- Rotate secrets periodically. Call the
generate-secretendpoint to create a new secret. The old one is immediately invalidated. - Use HTTPS in production. Webhook secrets are passed as query parameters -- always use HTTPS to prevent interception.
- Monitor the audit log. Every webhook-triggered run is logged in the audit log with
action: 'webhook_triggered'andsource: 'webhook'. Check for unexpected activity.
Testing Webhooks with curl
Quick Test (local development)
# 1. Get a token and find an org ID
TOKEN=$(curl -s http://localhost:8788/api/auth/dev-login -X POST | jq -r '.data.access_token')
ORG_ID=$(curl -s http://localhost:8788/api/orgs -H "Authorization: Bearer $TOKEN" | jq -r '.data[1].id')
# 2. Generate a webhook secret
SECRET=$(curl -s -X POST "http://localhost:8788/api/webhooks/$ORG_ID/generate-secret" \
-H "Authorization: Bearer $TOKEN" | jq -r '.data.webhook_secret')
echo "Org: $ORG_ID"
echo "Secret: $SECRET"
# 3. Trigger an agent run via webhook
curl -X POST "http://localhost:8788/api/webhooks/$ORG_ID/social_post?secret=$SECRET" \
-H "Content-Type: application/json" \
-d '{
"topic": "Weekend brunch specials",
"platforms": ["instagram"],
"mood": "warm and inviting"
}'
# 4. Check the run status
RUN_ID=$(curl -s -X POST "http://localhost:8788/api/webhooks/$ORG_ID/social_post?secret=$SECRET" \
-H "Content-Type: application/json" \
-d '{"topic": "Test post"}' | jq -r '.data.run_id')
curl -s "http://localhost:8788/api/agents/runs/$RUN_ID" \
-H "Authorization: Bearer $TOKEN" | jq '.data.status'
Verify the Webhook Was Logged
# Check audit log for webhook triggers
curl -s "http://localhost:8788/api/admin/audit-log?action=webhook_triggered" \
-H "Authorization: Bearer $TOKEN" | jq '.data'