Build a lead recovery pipeline with the SkipUp API
Build a lead recovery pipeline with the SkipUp REST API. Create meeting requests, verify webhook signatures, and close the loop.
What this covers: An end-to-end recovery pipeline — detecting form-to-meeting drop-offs, creating meeting requests via API, listening for booking events via webhooks, and closing the loop with your CRM. What this does NOT cover: Individual endpoint reference (see API docs) or no-code setup (see Zapier workflow guide). Prerequisites: SkipUp API key with
meeting_requests.writeandwebhooks.writescopes, a detection trigger (CRM webhook, form tool webhook, or custom polling), an HTTPS endpoint for receiving webhooks. Setup time: ~30 minutes to create your first recovery request. ~2 hours for the full pipeline with webhooks and error handling.
If you need API integration for lead recovery meeting scheduling, the SkipUp docs tell you how to call each endpoint — but this article tells you when to call them, how to detect the trigger condition, how to listen for the outcome, and how to close the loop. The pipeline pattern is what turns individual API calls into a recovery system.
For the complete recovery strategy, see The Complete Guide to Recovering Leads Who Submit Forms But Never Book Meetings. Prefer no-code? See Build a Zapier Workflow to Recover Abandoned Meeting Bookings.
What does the recovery pipeline look like?
The pipeline has four stages. You build stages 1 and 4. SkipUp handles stages 2 and 3.
- Detection (your system): CRM workflow, form webhook, or custom polling detects a form submission with no meeting booked after a delay (15–60 minutes).
- API Call (your system → SkipUp): POST
/api/v1/meeting_requestswith the prospect’s email, an organizer, and rich context about the lead. - Scheduling Conversation (SkipUp AI): SkipUp emails the prospect, negotiates times based on real calendar availability, and books the meeting.
- Webhook Events (SkipUp → your system): SkipUp fires
meeting_request.bookedormeeting_request.follow_up_sentto your webhook endpoint. Your system updates the CRM, triggers alerts, or logs analytics.
When your system detects a form submission without a booking, SkipUp takes over the scheduling conversation. You own detection and orchestration. SkipUp owns the scheduling conversation.
How do you create a recovery request?
POST to /api/v1/meeting_requests with an idempotency key to make retries safe. The response is 202 Accepted — processing is asynchronous, and SkipUp AI initiates the scheduling conversation via email after the request is created.
Two details that matter for recovery specifically:
include_introduction: true— tells the AI to introduce itself before proposing times. Essential for abandoned leads who did not initiate the conversation.context.description— pass rich lead context (company, role, form data, intent signals) so the AI’s outreach email is personalized, not generic. This is the strongest lever for recovery email effectiveness. Up to 5,000 characters.
curl
curl -X POST https://api.skipup.ai/api/v1/meeting_requests \
-H "Authorization: Bearer sk_live_abc123..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: lead:9f2k1x:recovery:2026-02-08" \
-d '{
"organizer_email": "[email protected]",
"participants": [
{
"email": "[email protected]",
"name": "Sarah Chen",
"timezone": "America/Los_Angeles"
}
],
"include_introduction": true,
"context": {
"title": "Demo: YourProduct for Acme Corp",
"purpose": "Product demo requested via website form",
"description": "Sarah submitted a demo request form on 2026-02-08. VP of Operations at Acme Corp (50-200 employees). Primary interest: automating vendor onboarding workflows. Q2 deadline for vendor selection.",
"duration_minutes": 30,
"timeframe": {
"start": "2026-02-10T00:00:00Z",
"end": "2026-02-21T00:00:00Z"
}
}
}'
Python
import requests
from datetime import date
SKIPUP_API_KEY = "sk_live_abc123..."
SKIPUP_BASE_URL = "https://api.skipup.ai/api/v1"
def create_recovery_request(lead):
"""Create a SkipUp meeting request for an abandoned form lead."""
today = date.today().isoformat()
idempotency_key = f"lead:{lead['id']}:recovery:{today}"
response = requests.post(
f"{SKIPUP_BASE_URL}/meeting_requests",
headers={
"Authorization": f"Bearer {SKIPUP_API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
json={
"organizer_email": lead["assigned_rep_email"],
"participants": [
{
"email": lead["email"],
"name": lead["full_name"],
"timezone": lead.get("timezone", "America/New_York"),
}
],
"include_introduction": True,
"context": {
"title": f"Demo: YourProduct for {lead['company']}",
"purpose": "Product demo requested via website form",
"description": lead["form_context"],
"duration_minutes": 30,
"timeframe": {
"start": "2026-02-10T00:00:00Z",
"end": "2026-02-21T00:00:00Z",
},
},
},
)
if response.status_code == 202:
return response.json()
error = response.json().get("error", {})
raise Exception(f"SkipUp API error: {error.get('type')} - {error.get('message')}")
Node.js (18+)
const SKIPUP_API_KEY = "sk_live_abc123...";
const SKIPUP_BASE_URL = "https://api.skipup.ai/api/v1";
async function createRecoveryRequest(lead) {
const today = new Date().toISOString().split("T")[0];
const idempotencyKey = `lead:${lead.id}:recovery:${today}`;
const response = await fetch(`${SKIPUP_BASE_URL}/meeting_requests`, {
method: "POST",
headers: {
Authorization: `Bearer ${SKIPUP_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({
organizer_email: lead.assignedRepEmail,
participants: [
{
email: lead.email,
name: lead.fullName,
timezone: lead.timezone || "America/New_York",
},
],
include_introduction: true,
context: {
title: `Demo: YourProduct for ${lead.company}`,
purpose: "Product demo requested via website form",
description: lead.formContext,
duration_minutes: 30,
timeframe: {
start: "2026-02-10T00:00:00Z",
end: "2026-02-21T00:00:00Z",
},
},
}),
});
if (response.status === 202) {
return await response.json();
}
const body = await response.json();
throw new Error(
`SkipUp API error: ${body.error?.type} - ${body.error?.message}`
);
}
Idempotency key strategy: Derive keys from business data — lead:{lead_id}:recovery:{YYYY-MM-DD} — to ensure the same lead is not re-enrolled in recovery twice on the same day, even if your webhook handler retries or a batch job reprocesses. The 24-hour TTL (scoped per API key) means a new day naturally produces a fresh key if you need a second attempt.
Gotcha: The organizer must be an active workspace member. If the organizer email does not match an active membership, the API returns a 422 validation error. Verify organizer membership before calling or handle the error in your retry logic.
How do you listen for booking events?
Now that you can create recovery requests, you need to know when they succeed. Register a webhook endpoint to receive events when the scheduling conversation progresses. The events that matter for recovery pipelines:
| Event | What It Means for Recovery |
|---|---|
meeting_request.booked | Recovery succeeded — the prospect booked. Update CRM to “Meeting Scheduled.” |
meeting_request.follow_up_sent | SkipUp sent a follow-up to an unresponsive prospect. Pause other nurture sequences to avoid double-outreach. |
meeting_request.cancelled | Request was cancelled. Re-enter the lead into your nurture flow. |
Register the endpoint
curl -X POST https://api.skipup.ai/api/v1/webhook_endpoints \
-H "Authorization: Bearer sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.example.com/webhooks/skipup",
"description": "Lead recovery pipeline events",
"events": [
"meeting_request.booked",
"meeting_request.follow_up_sent",
"meeting_request.cancelled"
]
}'
Verify webhook signatures
SkipUp signs every delivery with HMAC-SHA256. The X-Webhook-Signature header has the format t={timestamp},v1={hex_digest}. The signed payload is {timestamp}.{raw_request_body}.
Python:
import hmac
import hashlib
def verify_webhook_signature(payload_body, signature_header, secret):
parts = dict(part.split("=", 1) for part in signature_header.split(","))
timestamp = parts["t"]
received_signature = parts["v1"]
signed_payload = f"{timestamp}.{payload_body}"
expected_signature = hmac.new(
secret.encode("utf-8"),
signed_payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected_signature, received_signature):
raise ValueError("Invalid webhook signature")
Node.js:
import crypto from "node:crypto";
function verifyWebhookSignature(payloadBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((part) => part.split("=", 2))
);
const signedPayload = `${parts.t}.${payloadBody}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(signedPayload)
.digest("hex");
const expected = Buffer.from(expectedSignature, "utf-8");
const received = Buffer.from(parts.v1, "utf-8");
if (!crypto.timingSafeEqual(expected, received)) {
throw new Error("Invalid webhook signature");
}
}
Close the loop with your CRM. Map webhook events to CRM statuses for pipeline tracking:
| Webhook Event | CRM Status |
|---|---|
meeting_request.created | Meeting Requested |
meeting_request.follow_up_sent | Follow-up Sent |
meeting_request.booked | Meeting Scheduled |
meeting_request.cancelled | Re-enter Nurture |
How do you handle errors and retries?
On the webhook side: SkipUp retries failed deliveries (non-2xx or timeout) up to 8 times with polynomial backoff over ~40 minutes. Timeout per attempt is 10 seconds. Respond with 2xx immediately and process events asynchronously. If an endpoint accumulates 10+ failures in 24 hours, SkipUp auto-disables it. Monitor with:
def check_webhook_health():
response = requests.get(
f"{SKIPUP_BASE_URL}/webhook_endpoints",
headers={"Authorization": f"Bearer {SKIPUP_API_KEY}"},
)
for endpoint in response.json().get("data", []):
if not endpoint.get("enabled"):
alert_ops_team(
f"Webhook endpoint disabled: {endpoint['url']} "
f"(reason: {endpoint.get('disabled_reason', 'unknown')})"
)
On the API side: Handle these error responses in your retry logic:
| Status | Cause | Action |
|---|---|---|
| 401 | Invalid or missing API key | Check Authorization: Bearer sk_live_* header |
| 403 | Missing required scope | Verify API key has meeting_requests.write scope |
| 422 | Validation error (e.g., organizer not a workspace member) | Check request body; do not retry without fixing the input |
| 429 | Rate limit exceeded | Implement exponential backoff |
Extending your existing SkipUp integration
If you already use the SkipUp API for direct meeting scheduling, add recovery as a new capability:
- Add idempotency keys to existing create calls to prevent duplicates during retries:
Idempotency-Key: lead:{lead_id}:recovery:{date}. - Subscribe to
meeting_request.follow_up_sentto monitor recovery effectiveness. This event fires when the AI sends follow-up outreach to an unresponsive prospect — use it to coordinate with other nurture sequences and avoid double-outreach. - Batch-process historical leads by querying your CRM for contacts who submitted forms but never booked, then creating meeting requests in bulk. Use idempotency keys (derived from lead ID + date) to make the batch safe to re-run. Respect 429 rate limit responses with exponential backoff.
What should you build next?
Start with one form, one detection trigger, one SkipUp API call. Add webhooks to measure the booking rate. Iterate from there.
- For the complete recovery strategy, see The Complete Guide to Recovering Leads Who Submit Forms But Never Book Meetings.
- Prefer no-code? See Build a Zapier Workflow to Recover Abandoned Meeting Bookings.
- For endpoint reference: API docs, Webhooks, Authentication, Quickstart.
Let's automate
your scheduling
Spend less time updating tools and more time closing deals.
Free for your first 10 meetings. No credit card required.
Product leader who spent 10 years at HappyCo as VP of Product, scaling the company from $1M to over $20M in revenue and leading market-defining product launches in multifamily real estate. Founded Okonomi, an AI-first ERP for food businesses, and spent 8 years running Web Dissect, a product development consultancy for B2B SaaS companies. Now building SkipUp to transform how businesses schedule meetings with AI. Writes about the operational problems he has spent his career solving: stakeholder alignment, meeting coordination, and the gap between lead capture and first conversation.