Gareth Wilson Gareth Wilson

Building real-estate lead calling with Google Forms and Bland AI

Published


A buyer fills out an inquiry form on a listing page. A few minues later an AI voice agent has already called them, confirmed the property they're interested in, qualified their budget and timeline, and booked a viewing slot on the agent's calendar for Tuesday at 6pm. The human agent reads the call summary on Monday morning, walks into the appointment prepared, and closes faster than the three other agencies the buyer also inquired with.

That's the workflow real-estate teams are quietly assembling right now with off-the-shelf parts: a Google Form on the listing page, Bland AI as the voice agent, and a CRM or calendar on the back end. The hard part isn't any single piece — Bland's API is straightforward, Google Forms are free, and every CRM has webhooks. The hard part is the glue. Specifically, what happens to a form submission between the moment a buyer hits "Submit" and the moment the phone rings, and what happens to Bland's call-completion webhook when it fires back into your stack.

This guide walks through that glue layer end to end: the architecture, seven concrete steps to wire it up, and the production concerns you need to know about.

The flow

There are two webhook flows in this workflow, and both need to be reliable:

flowchart TB
    A[Google Form<br/>submission] --> B[Apps Script<br/>trigger]
    B -->|POST JSON| C[Hookdeck<br/>inbound source]
    C -->|transform + filter<br/>+ rate limit + retry| D[Bland AI<br/>POST /v1/calls]
    D -.->|outbound call| E[Buyer's phone]
    E -.->|call ends| F[Bland AI<br/>call-completion webhook]
    F -->|POST JSON| G[Hookdeck<br/>callback source]
    G -->|transform + retry| H[CRM / calendar /<br/>Slack / database]
  1. Form to Bland — must not drop a submission, even when Bland's API is throttled or briefly down. A lost submission is a lost lead.
  2. Bland's call-completion webhook back to your stack — carries the entire value of the system: the qualification data, the booked appointment, the call transcript. A lost callback means a human agent walks into the appointment with no context, or never finds out the appointment was booked.

Most teams build this with a try/except around an HTTPS call and a single POST /webhooks/bland endpoint on their app server. That's enough to demo. It's not enough to run, and the reasons it isn't enough are the reasons Hookdeck sits in this picture.

What you'll need

Before you start, make sure you have:

  • A Google account with edit access to the form you want to use as the lead source
  • A Bland AI account with API access and at least one phone number provisioned
  • A Hookdeck Event Gateway account — the free tier covers this entire workflow at low volume
  • Hookdeck CLI installed locally: npm install hookdeck-cli -g or brew install hookdeck/hookdeck/hookdeck.
  • A destination for call results — for this walkthrough we'll use a simple webhook URL on your CRM, but Slack, a Google Sheet via Apps Script, or a database endpoint all work.

Step 1: Wire Google Forms to fire a webhook

Google Forms doesn't fire webhooks natively. The cleanest workaround is a bound Apps Script trigger that runs on every submission.

Open your form, click the three-dot menu, and choose Script editor. Replace the default code with this:

const HOOKDECK_URL = 'https://hkdk.events/YOUR_SOURCE_ID';

function onFormSubmit(e) {
  const responses = e.namedValues;

  const payload = {
    submitted_at: new Date().toISOString(),
    form_id: e.source.getId(),
    name: responses['Full name']?.[0] || null,
    phone: responses['Phone number']?.[0] || null,
    email: responses['Email']?.[0] || null,
    property_id: responses['Property reference']?.[0] || null,
    inquiry_type: responses['What are you interested in?']?.[0] || null,
    notes: responses['Anything else we should know?']?.[0] || null,
  };

  UrlFetchApp.fetch(HOOKDECK_URL, {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  });
}

You'll fill in HOOKDECK_URL in step 2. For now, save the script, then add an installable trigger: Triggers → Add Trigger → choose the function onFormSubmit, event source From form, event type On form submit. Authorize it when prompted.

That's the inbound side done. Every form submission will now POST a normalized JSON payload to whatever URL you put in HOOKDECK_URL.

Step 2: Set up the Hookdeck Event Gateway source for form submissions

In the Hookdeck Event Gateway dashboard, create a new connection. The first piece is the source — the public URL Apps Script will post to.

  • Click Create Connection → New Source
  • Choose the HTTP source type (since Apps Script is the producer, not a known vendor)
  • Name it google-forms-property-inquiries

Hookdeck Event Gateway will generate a URL of the form https://hkdk.events/<source-id>. Copy it, paste it into the HOOKDECK_URL constant in your Apps Script, and save.

Submit a test entry into the form. You should see the event appear in the Hookdeck Event Gateway dashboard within a second or two, with the full payload visible. If you don't, check the Apps Script execution log — most issues here are authorisation prompts that weren't accepted.

Step 3: Add the destination — Bland AI

Still inside the connection, configure the destination:

  • Type: HTTP
  • URL: https://api.bland.ai/v1/calls
  • Authentication: add a custom HTTP header authorization set to your Bland API key (Hookdeck stores this encrypted; it won't appear in payloads or logs)

Bland AI's POST /v1/calls endpoint expects a payload roughly like:

{
  "phone_number": "+15551234567",
  "task": "You are calling on behalf of Acme Realty about the buyer's inquiry on property 1024 Oak Lane. Confirm interest, qualify their budget and timeline, and offer to book a 30-minute viewing.",
  "voice": "maya",
  "webhook": "https://hkdk.events/YOUR_CALLBACK_SOURCE_ID",
  "metadata": {
    "lead_name": "Jane Buyer",
    "lead_email": "jane@example.com",
    "property_id": "1024-oak-lane",
    "form_submitted_at": "2026-05-14T21:47:00Z"
  }
}

Apps Script doesn't produce this shape — it produces the flat schema from step 1. The transformation layer bridges the gap.

Step 4: Add transformation, filter, and rate-limit rules

This is where Hookdeck Event Gateway does most of its work for this use case. In the same connection, add the following rules:

Transformation — reshape Apps Script's payload into Bland's expected schema. Hookdeck Event Gateway transformations run as JavaScript:

addHandler('transform', (request, context) => {
  const body = request.body;

  // Convert to E.164 — Bland expects international format
  const phone = body.phone?.replace(/[^\d+]/g, '');
  const phone_e164 = phone?.startsWith('+') ? phone : `+1${phone}`;

  const property_id = body.property_id || 'unknown';
  const inquiry_type = body.inquiry_type || 'general inquiry';

  request.body = {
    phone_number: phone_e164,
    task: [
      `You are calling on behalf of Acme Realty about a ${inquiry_type}`,
      `on property ${property_id}.`,
      `The lead's name is ${body.name || 'the prospect'}.`,
      `Confirm their interest, qualify budget and timeline,`,
      `and offer to book a 30-minute viewing this week.`,
    ].join(' '),
    voice: 'maya',
    webhook: 'https://hkdk.events/YOUR_CALLBACK_SOURCE_ID',
    metadata: {
      lead_name: body.name,
      lead_email: body.email,
      property_id: body.property_id,
      form_submitted_at: body.submitted_at,
    },
  };

  return request;
});

Filter — drop submissions that can't result in a valid call. Hookdeck Event Gateway filters can be expressed as JSON match rules:

{
  "body": {
    "phone": {
      "$regex": "^[+]?[0-9 ()\\-]{7,}$"
    },
    "property_id": {
      "$exists": true,
      "$ne": null
    }
  }
}

Anything that fails this filter is logged but never delivered to Bland — so test submissions, fat-finger typos with no phone, and form spam don't burn Bland credits.

Rate limit — cap delivery to your Bland concurrency budget. For most Bland accounts, 5 requests per second with a burst of 10 is comfortable:

  • Rate: 5 per second
  • Burst: 10

When fifty form submissions arrive in a minute because a listing went viral, Hookdeck Event Gateway queues them and feeds them to Bland at a sustainable rate. The leads still get called; you just don't get a wall of 429s.

Retry policy — Bland will occasionally return transient 5xx or 429. Configure exponential backoff:

  • Initial delay: 30 seconds
  • Max attempts: 10
  • Max age: 24 hours
  • Apply on status codes: 429, 500, 502, 503, 504

This is your reliability guarantee on the inbound leg. As long as the Hookdeck Event Gateway source ingested the submission, it will keep trying to reach Bland until either Bland accepts it or 24 hours have passed.

Step 5: Test the inbound leg locally with the CLI

Before going live, route the connection through the CLI so you can inspect every request before it hits Bland.

Open a new terminal:

hookdeck login
hookdeck listen 3000 google-forms-property-inquiries

The CLI will register a temporary CLI destination on the connection and stream events to localhost:3000. Run a tiny local server that just logs the body:

// inspect.js
const http = require('http');
http.createServer((req, res) => {
  let body = '';
  req.on('data', chunk => body += chunk);
  req.on('end', () => {
    console.log('---');
    console.log('Headers:', req.headers);
    console.log('Body:', body);
    res.writeHead(200);
    res.end('ok');
  });
}).listen(3000);

node inspect.js, then submit a real test entry on the form. You'll see the transformed payload (exactly what Bland would receive) printed in your terminal. If the phone number isn't being normalized correctly, or the task prompt is missing the property reference, this is where you'll catch it. Iterate on the transformation, press r in the CLI terminal to replay the most recent event, and tighten the loop until it's right.

Once the payload looks correct, stop the CLI and reconnect the destination to Bland's real API URL.

Step 6: Wire Bland's callback back through Hookdeck

When Bland completes a call (or fails one, or transfers it), it sends a webhook to whatever URL you specified in the webhook field of the call payload. The same primitives apply on the way back — signature verification, transformation, filtering, retries.

In Event Gateway, create a second connection:

  • Source: a new HTTP source named bland-call-callbacks. Copy its URL and update the webhook field in your transformation from step 4 to use this URL.
  • Destination: your CRM webhook endpoint, or a Slack incoming webhook, or whatever consumes the call outcome.

For the callback connection, configure:

Signature verification — Bland signs webhooks; verify them on ingestion so you don't process spoofed payloads. See Bland's docs for the current signing scheme, and configure the verification key in the Hookdeck source settings.

Filter by event type — Bland sends callbacks for completed calls, failed calls, transfers, and recording-ready events. If your CRM only wants completed calls, filter:

{
  "body": {
    "status": "completed"
  }
}

You can also create separate connections for each event type — one to your booking endpoint, one to a call.failed Slack channel, one to your recording storage pipeline.

Transformation — Bland's webhook payload is rich (transcript, summary, call duration, metadata you passed in, recording URL). Reshape it into your CRM's contact schema:

addHandler('transform', (request, context) => {
  const body = request.body;

  request.body = {
    contact: {
      name: body.metadata?.lead_name,
      email: body.metadata?.lead_email,
      phone: body.to,
    },
    interaction: {
      type: 'ai_outbound_call',
      duration_seconds: body.call_length,
      summary: body.summary,
      transcript_url: body.transcript_url,
      recording_url: body.recording_url,
      property_id: body.metadata?.property_id,
      booked_appointment: body.summary_data?.appointment_time || null,
    },
    source: 'bland-ai',
    call_id: body.call_id,
  };

  return request;
});

Retry policy — aggressive on the callback leg, because losing a callback is losing the entire payoff:

  • Initial delay: 15 seconds
  • Max attempts: 15
  • Max age: 72 hours

If your CRM is down for an hour during a deploy, Hookdeck Event Gateway will keep retrying. If it's down for two days, you can fix it and replay the affected events from the dashboard.

Step 7: Run the full chain end to end

You're now ready for a real test. Submit a form entry with your own phone number. You should see, in order:

  1. The submission appear in the Hookdeck dashboard for the google-forms-property-inquiries connection
  2. A successful 200 response from Bland after the transformation runs
  3. Your phone ring within 5–10 seconds
  4. The AI agent run through the qualification script
  5. A callback appear in the bland-call-callbacks connection after the call ends
  6. A delivery to your CRM/Slack/database with the call summary, transcript, and any booked appointment

If any step fails, the Hookdeck Event Gateway dashboard shows you exactly where: which connection, which event, what the request and response looked like. No log-spelunking required.

Why Hookdeck and not just a try/except in your app server?

You could put all of this directly in your application code. Voice platforms have three properties that make that the wrong choice once you're past the demo:

They're bursty. Real-estate leads cluster — open-house weekends, new listings, the moment a Google Ads campaign goes live. A listing that gets featured on a popular property aggregator can deliver fifty form submissions in an hour. If you fire fifty POST /v1/calls requests in a row, you'll get 429s on the back half, and the leads that 429 are real leads. Hookdeck Event Gateway's queueing and rate limiting smooth bursts into a sustainable stream without you writing a job queue.

Their callbacks are retry-sensitive. Bland's default retry policy on webhooks is decent, but you don't get to configure it per-endpoint or replay events from history. Hookdeck Event Gateway stores every callback and lets you tune retries, replay individual events after you fix a bug, and bulk-replay an entire time window after a deploy went bad.

They fail in ways that need debugging, not just retrying. Calls fail — numbers are wrong, voicemails are picked up, transfers don't connect, the LLM hallucinates a property address. Every one of those becomes a support ticket that turns into a forensic investigation of your application logs. With Hookdeck Event Gateway, the dashboard shows you the exact payload Bland sent, the exact response your endpoint returned, and the exact transformation that ran in between. The forensic step takes a minute, not an afternoon.

You can put all of this on your own infrastructure: a queue, a retry worker, a transformation step, a payload store, an observability layer, a replay tool. That's the work Hookdeck Event Gateway collapses into a connection in a dashboard. The hours you don't spend building and maintaining that infrastructure are hours you spend on the AI agent and the customer experience, which is where your real competitive value lives.

Going to production

A few additional things change between the demo and the deployment:

Observability the business will use. Hookdeck's Issues feature surfaces failure patterns automatically — repeated retries on the same endpoint, signature verification failures, payload spikes. Set up a Slack alert when call.failed events spike on a Saturday afternoon, so your on-call engineer hears about a Bland outage before the head of sales does.

Tune retries for the user-perceived deadline. A buyer who filled out a form expects a call within minutes, not hours. If Bland is having a bad twenty minutes, you want aggressive early retries and then a fallback — perhaps a Slack alert to a human agent — rather than silently retrying for 24 hours while the lead goes cold. Hookdeck's retry policies are configurable per connection.

Replay deliberately. When you update the AI agent's prompt, you'll want to re-process recent leads through the new version — or at least understand what they would have said. Hookdeck Event Gateway's event replay lets you re-fire any historical event into the same destination, or a new one, on demand. This is also how you migrate from Bland to another voice provider without losing history.

Handle PII responsibly. Phone numbers, names, and email addresses pass through this pipeline. Event Gateway supports payload redaction so sensitive fields don't appear in dashboards or logs. Configure it before you go live.

What to build next

This pattern generalizes: Swap Google Forms for Typeform or a Webflow form. Swap Bland for ElevenLabs or any other voice provider. Swap the use case from real-estate inquiries to insurance quotes, dental appointment confirmations, or post-purchase customer outreach. The plumbing stays the same; the prompts and the schemas change.

If you're building any of this, the fastest way to get past the demo phase is to stop maintaining your own webhook infrastructure. Start with the Hookdeck free tier (you can run this entire workflow without paying anything until you hit real volume) and use the CLI to keep your development loop fast.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.