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

ScenarioTrigger SourceAgent
New Google review comes inZapier / Google Alertsreview_response
New lead in your CRMHubSpot / Salesforce / Airtableemail_campaign
Client posts a new menuWebsite CMS webhooksocial_post
Weekly scheduled contentCron job / Zapier Schedulecontent_calendar
New product listedE-commerce platformsocial_post
Competitor posts somethingSocial monitoring toolcompetitor_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:
KeyValue (Zapier fields)
platformGoogle
rating{{Review Star Rating}}
reviewer_name{{Reviewer Name}}
review_text{{Review Comment}}
date{{Review Create Time}}

What Happens

  1. Customer leaves a Google review
  2. Zapier detects it (checks every 1-15 minutes depending on plan)
  3. Zapier sends a POST request to Upland Hub's webhook endpoint
  4. The Review Response agent generates a draft response
  5. The response appears in the Review Queue for approval
  6. You (or the client) approve it
  7. 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

  1. New lead enters the CRM
  2. Automation sends webhook to Upland Hub
  3. Email Campaign agent generates a welcome email with subject line variants, body, and CTA
  4. Content appears in Review Queue
  5. 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

StatusMeaningResponse Body
201Success -- run queued{ "data": { "run_id": "...", "status": "queued", "agent_type": "..." } }
401Invalid or missing webhook secret{ "error": "Invalid webhook secret" }
402Organization has no credits remaining{ "error": "Insufficient credits" }
404Organization 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 secret query 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/:orgId with 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/:orgId with 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

  1. Never expose webhook secrets in client-side code. Secrets should only be stored in server-side configurations (Zapier, Make, your backend).
  2. Rotate secrets periodically. Call the generate-secret endpoint to create a new secret. The old one is immediately invalidated.
  3. Use HTTPS in production. Webhook secrets are passed as query parameters -- always use HTTPS to prevent interception.
  4. Monitor the audit log. Every webhook-triggered run is logged in the audit log with action: 'webhook_triggered' and source: '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'