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.

· Dheer Gupta, Co-Founder · 8 min read
Share

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.write and webhooks.write scopes, 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.

  1. Detection (your system): CRM workflow, form webhook, or custom polling detects a form submission with no meeting booked after a delay (15–60 minutes).
  2. API Call (your system → SkipUp): POST /api/v1/meeting_requests with the prospect’s email, an organizer, and rich context about the lead.
  3. Scheduling Conversation (SkipUp AI): SkipUp emails the prospect, negotiates times based on real calendar availability, and books the meeting.
  4. Webhook Events (SkipUp → your system): SkipUp fires meeting_request.booked or meeting_request.follow_up_sent to 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:

EventWhat It Means for Recovery
meeting_request.bookedRecovery succeeded — the prospect booked. Update CRM to “Meeting Scheduled.”
meeting_request.follow_up_sentSkipUp sent a follow-up to an unresponsive prospect. Pause other nurture sequences to avoid double-outreach.
meeting_request.cancelledRequest 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 EventCRM Status
meeting_request.createdMeeting Requested
meeting_request.follow_up_sentFollow-up Sent
meeting_request.bookedMeeting Scheduled
meeting_request.cancelledRe-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:

StatusCauseAction
401Invalid or missing API keyCheck Authorization: Bearer sk_live_* header
403Missing required scopeVerify API key has meeting_requests.write scope
422Validation error (e.g., organizer not a workspace member)Check request body; do not retry without fixing the input
429Rate limit exceededImplement exponential backoff

Extending your existing SkipUp integration

If you already use the SkipUp API for direct meeting scheduling, add recovery as a new capability:

  1. Add idempotency keys to existing create calls to prevent duplicates during retries: Idempotency-Key: lead:{lead_id}:recovery:{date}.
  2. Subscribe to meeting_request.follow_up_sent to 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.
  3. 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.

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.

DG
Dheer Gupta Co-Founder

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.

Similar Posts