Embed GillyReach

Add B2B prospecting and lead generation to your platform with our simple iframe integration.

Try it yourself

Experience our AI-powered prospecting engine with a live demo. No sign-up required.

Why Integrate GillyReach?

  • - GillyReach is a B2B prospecting and lead generation software that allows users to build highly targeted leads lists containing both businesses and contacts.
  • - We offer an AI first architecture where users can build lists from a conversational interface.
  • - Our lists are iterative, allowing for users to continuously refine them.
  • - We handle the user interface, data, and infrastructure. This allows you to have a modern prospecting engine without significant engineering overhead.
  • - We offer LinkedIn profile data, emails, and phone numbers of contacts.
  • - You can meter usage per user and upsell our functionality to them.

High-level Architecture

  • - GillyReach is embedded via an iframe inside the host platform.
  • - End users do not create or manage GillyReach logins.
  • - The host platform authenticates server-to-server using a workspace secret.
  • - The host backend creates a short-lived embed token and returns an iframe URL to its frontend.
  • - All credit tracking, spend limits, and auto-billing are enforced server-side by GillyReach.
  • - The iframe communicates with the host app using postMessage events.

Authentication Model

Workspace Secret (server-side only)

  • - One secret per customer workspace / integration install.
  • - Stored only on the host platform backend.
  • - Never exposed to browsers or iframe URLs.

Embed Token (browser-safe)

  • - Short-lived (5–15 minutes).
  • - Created by GillyReach using the workspace secret and accessed by the host using the GillyReach API.
  • - Passed to the iframe as a query parameter or used to refresh GillyReach authentication upon token expiration.
  • - Encodes authorization and identity context.

Token Claims:

  • workspace_id
  • user_email
  • list_id (optional)
  • should_confirm (boolean)
  • host_origin
  • scopes
  • exp

API Endpoints

1. Create Embed Token

POST/iframe/embed_tokens

Auth: Authorization: Bearer <workspace_secret>

Request:

curl -sS -X POST "https://api.gillyreach.io/iframe/embed_tokens" \
  -H "Authorization: Bearer $GR_WORKSPACE_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "user_email": "user@company.com",
    "list_id": "lst_123",
    "should_confirm": true
  }'

Response:

{
  "token": "<jwt>",
  "expires_in": 900,
  "embed_url": "https://app.gillyreach.io/embed?token=..."
}

The returned embed_url should be loaded directly in an iframe. The token can be used to refresh GillyReach authentication upon token expiration.

2. Get All Lists for a User (by email)

GET/iframe/get_lists

Auth: Embed token

Request:

curl -sS -X GET "https://api.gillyreach.io/iframe/get_lists" \
  -H "Authorization: Bearer $GR_EMBED_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "list_id": "list_123",
    "offset": 0,
    "limit": 10
  }'

Response:

{
  "lists": [
    {
      "list_id": "lst_123",
      "name": "Denver GCs",
      "created_at": "2025-01-12T18:22:00Z",
      "counts": { "companies": 1200, "contacts": 3400 }
    }
  ],
  "next_index": 11
}

Note:

  • - If offset = 0 and limit = None, all lists will be returned.
  • - If next_index == None, all data has been returned.
  • - To paginate, set offset equal to next_index.

3. Get List with Data (by list ID)

GET/iframe/get_list_with_data

Auth: Embed token

Request:

curl -sS -X GET "https://api.gillyreach.io/iframe/get_list_with_data" \
  -H "Authorization: Bearer $GR_EMBED_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "list_id": "lst_123",
    "offset": 0,
    "limit": 10,
    "tab": "companies" or "contacts"
  }'

Response:

{
  "companies": [{
    "name": "ACME Corp",
    "website": "acme.com",
    "address": "123 Industry Lane, Denver, CO 80123",
    "linkedin": "https://linkedin.com/acme",
    "revenue_millions": 20,
    "headcount": 50
  }],
  "contacts": [{
    "first_name": "John",
    "last_name": "Doe",
    "title": "CEO",
    "company_name": "ACME Corp",
    "company_website": "acme.com",
    "company_linkedin": "https://linkedin.com/acme",
    "linkedin": "https://linkedin.com/john_doe_acme",
    "location": "Denver, Colorado",
    "email": "john@acme.com",
    "phone": ["(720) 123-4567", "(720) 456-7890"],
    "phone_type": ["mobile", "landline"]
  }],
  "next_index": 11
}

Note:

  • - If offset = 0 and limit = None, all items will be returned.
  • - If next_index == None, all data has been returned.
  • - To paginate, set offset equal to next_index.
  • - If "tab" == "contacts", the companies array will be empty and vice versa.

iframe Embedding

Embed the GillyReach iframe in your application:

<iframe
  src="https://app.gillyreach.io/embed?token=..."
  sandbox="allow-scripts allow-forms allow-same-origin allow-popups"
  style="width:100%;height:100%;border:0"
></iframe>

Security Headers (GillyReach)

  • Content-Security-Policy: frame-ancestors <allowed host origins>
  • Short token TTL enforced

postMessage Event Protocol

Messages Sent by GR iframe → host (outbound)

Outbound events include source and type.

{
  "source": "gillyreach",
  "type": "event_name",
  "request_id": "uuid",
  "payload": {}
}

Messages Sent by host → GR iframe (inbound)

All messages sent from the host to the iframe follow this general format:

{
  "type": "event_type",
  "request_id": "uuid",  
  "payload": {}
}

Response to confirm_required

When the iframe emits a confirm_required event, the host must respond with the same request_id and indicate whether the user confirmed:

{
  "type": "confirm_result",
  "request_id": "uuid",  // Same ID from the confirm_required event
  "payload": { "confirmed": true }
}

Response to auth_expired

When the iframe emits an auth_expired event, the host should fetch a new token and send it to refresh authentication:

{
  "type": "refresh_token",
  "request_id": "uuid",
  "payload": {
    "token": "<new_jwt>"
  }
}

Events Emitted by iframe → Host

ready

Emitted once the iframe UI has loaded and is ready to receive user interaction and host messages.

{
  "source": "gillyreach",
  "type": "ready",
  "request_id": "uuid",
  "payload": {}
}

Typical usage: Host can hide loading states and begin listening for spend/export events.

list_complete

Emitted when user is finished building list.

{
  "source": "gillyreach",
  "type": "list_ready",
  "request_id": "uuid",
  "payload": {
    "list_id": "lst_123"
  }
}

Typical usage: Host can associate downstream actions (export, automation runs) with the returned list_id.

confirm_required

Sent when should_confirm=true and a spend requires approval.

{
  "source": "gillyreach",
  "type": "confirm_required",
  "request_id": "uuid",
  "payload": {
    "operation": "enrich",
    "estimated_credits": 120,
    "list_id": "lst_123"
  }
}

credit_spend

{
  "source": "gillyreach",
  "type": "credit_spend",
  "request_id": "uuid",
  "payload": {
    "operation": "enrich",
    "credits": 120,
    "balance_before": 1000,
    "balance_after": 880
  }
}

auth_expired

Emitted when the iframe token is expired or a request returns 401/403 and the iframe cannot continue.

{
  "source": "gillyreach",
  "type": "auth_expired",
  "request_id": "uuid",
  "payload": {
    "reason": "token_expired"
  }
}

Recommended behavior: Fetch a new embed token from your backend and send it to the iframe via refresh_token postMessage event. This maintains iframe state without a full reload.

insufficient_funds

Emitted when a spend cannot be completed because the workspace has insufficient credits and auto top-up is disabled, capped, or failed.

{
  "source": "gillyreach",
  "type": "insufficient_funds",
  "request_id": "uuid",
  "payload": {
    "operation": "export",
    "required_credits": 120,
    "available_credits": 50,
    "auto_topup_attempted": true,
    "auto_topup_result": "failed" or "disabled"
  }
}

Recommended behavior: Contact GillyReach support to request credit increase or implement higher auto billing limit.

Setup and Billing

  • - You will receive an embedding token directly from the GillyReach team.
  • - The GillyReach team can assist with implementation.
  • - Usage based credit re-purchase limits and max monthly spend will be configured to protect budget and ensure users have access to contacts.
  • - Credit purchases up to the configured monthly spend limit will be carried out automatically via Stripe.
  • - For example, if you dip below n credits a purchase will be initiated bringing your balance up to y credits. This will automatically happen until you hit x dollars in monthly spend. This is configurable by the GillyReach team.

Reference Implementation

This section shows an example of how to embed GillyReach into your product.

What You Implement

  1. Backend token proxy (Node.js) that stores GR_WORKSPACE_SECRET and returns { embed_url } to your frontend.
  2. Frontend embed component (React) that:
    • - fetches embed_url
    • - renders the iframe
    • - handles all iframe events
    • - (optionally) prompts users to confirm credit spend
    • - (optionally) meters usage per user
    • - refreshes embed token when it expires

Node.js Backend (Express) — Token Proxy Endpoint

Store your workspace secret in an environment variable: GR_WORKSPACE_SECRET=...

Implement endpoints that your frontend calls for initial load and token refresh.

import express from "express";

const app = express();
app.use(express.json());

// POST /api/gr/embed-url
// Body: { user_email: string, list_id?: string, should_confirm?: boolean }
// Returns the full embed URL for initial iframe load
app.post("/api/gr/embed-url", async (req, res) => {
  try {
    const { user_email, list_id, should_confirm } = req.body || {};

    if (!user_email) {
      return res.status(400).json({ error: "missing_user_email" });
    }

    const r = await fetch("https://api.gillyreach.io/iframe/embed_tokens", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.GR_WORKSPACE_SECRET}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        user_email,
        list_id: list_id || null,
        should_confirm: !!should_confirm,
      }),
    });

    if (!r.ok) {
      const detail = await r.text();
      return res.status(502).json({ error: "gillyreach_error", detail });
    }

    const data = await r.json();
    return res.json({ embed_url: data.embed_url });
  } catch (e) {
    return res.status(500).json({ error: "server_error" });
  }
});

// POST /api/gr/refresh-token
// Body: { user_email: string, list_id?: string, should_confirm?: boolean }
// Returns just the token for in-place credential refresh
app.post("/api/gr/refresh-token", async (req, res) => {
  try {
    const { user_email, list_id, should_confirm } = req.body || {};

    if (!user_email) {
      return res.status(400).json({ error: "missing_user_email" });
    }

    const r = await fetch("https://api.gillyreach.io/iframe/embed_tokens", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.GR_WORKSPACE_SECRET}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        user_email,
        list_id: list_id || null,
        should_confirm: !!should_confirm,
      }),
    });

    if (!r.ok) {
      const detail = await r.text();
      return res.status(502).json({ error: "gillyreach_error", detail });
    }

    const data = await r.json();
    return res.json({ token: data.token });
  } catch (e) {
    return res.status(500).json({ error: "server_error" });
  }
});

app.listen(3000, () => console.log("GR token proxy listening on :3000"));

React Frontend — Complete Embed Implementation

import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";

// Set this to the origin that serves the embedded GillyReach UI
const GR_IFRAME_ORIGIN = "https://app.gillyreach.io";

/**
 * Props
 * - userEmail: end-user identifier in your app
 * - listId: optional, open a list on load
 * - shouldConfirm: if true, GR will require host confirmation before spend
 */
export function GillyReachEmbed({ userEmail, listId, shouldConfirm = false }) {
  const iframeRef = useRef(null);

  // embedUrl changes will reload the iframe
  const [embedUrl, setEmbedUrl] = useState(null);

  // host-side status (optional)
  const [status, setStatus] = useState(null);

  const requestBody = useMemo(
    () => ({
      user_email: userEmail,
      list_id: listId || null,
      should_confirm: !!shouldConfirm,
    }),
    [userEmail, listId, shouldConfirm]
  );

  const fetchEmbedUrl = useCallback(async () => {
    setStatus(null);

    const r = await fetch("/api/gr/embed-url", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(requestBody),
      credentials: "include",
    });

    if (!r.ok) {
      setStatus({ kind: "error", message: "Failed to initialize GillyReach embed." });
      return;
    }

    const { embed_url } = await r.json();

    // Cache-buster ensures a full reload in SPAs
    setEmbedUrl(embed_url + "&t=" + Date.now());
  }, [requestBody]);

  const refreshToken = useCallback(async (request_id) => {
    const r = await fetch("/api/gr/refresh-token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(requestBody),
      credentials: "include",
    });

    if (!r.ok) {
      setStatus({ kind: "error", message: "Failed to refresh GillyReach token." });
      return;
    }

    const { token } = await r.json();

    // Send new token to iframe via postMessage
    iframeRef.current?.contentWindow?.postMessage(
      {
        type: "refresh_token",
        request_id: request_id,
        payload: { token },
      },
      GR_IFRAME_ORIGIN
    );
  }, [requestBody]);

  // Initial load and re-load when userEmail/listId/shouldConfirm changes
  useEffect(() => {
    if (!userEmail) return;
    fetchEmbedUrl();
  }, [userEmail, listId, shouldConfirm, fetchEmbedUrl]);

  // Handle GR -> host messages
  useEffect(() => {
    function onMessage(event) {
      if (event.origin !== GR_IFRAME_ORIGIN) return;

      const msg = event.data;
      if (!msg || msg.source !== "gillyreach" || !msg.type || !msg.request_id) return;

      switch (msg.type) {
        case "ready": {
          // iframe is ready
          return;
        }

        case "list_complete": {
          // list is ready for outbound, handle navigation as needed.
          return;
        }

        case "confirm_required": {
          // You can optionally prompt the user to confirm credit spend
          // For this example, we auto-confirm. In production, show a confirmation dialog.
          const confirmed = true; // or result from confirmation dialog
          
          iframeRef.current?.contentWindow?.postMessage(
            { type: "confirm_result", request_id: msg.request_id, payload: { 'confirmed': confirmed } },
            GR_IFRAME_ORIGIN
          );
          return;
        }

        case "credit_spend": {
          // optional: update your own meters
          return;
        }

        case "auth_expired": {
          // Token expired or auth failed; refresh token via postMessage
          refreshToken(msg.request_id);
          return;
        }

        case "insufficient_funds": {
          // Handle retry/failure notification as needed.
          return;
        }

        default:
          // Unknown message type — ignore
          return;
      }
    }

    window.addEventListener("message", onMessage);
    return () => window.removeEventListener("message", onMessage);
  }, [refreshToken]);

  return (
    <div style={{ width: "100%", height: "100%" }}>
      {embedUrl && (
        <iframe
          ref={iframeRef}
          src={embedUrl}
          title="GillyReach"
          style={{ width: "100%", height: "100%", border: 0 }}
          sandbox="allow-scripts allow-forms allow-same-origin allow-popups"
        />
      )}
    </div>
  );
}

Notes for Integrators

  • - To switch lists, re-render this component with a new listId (it will fetch a fresh embed_url and reload the iframe).
  • - On auth_expired, the component fetches a new token and sends it to the iframe via refresh_token postMessage. This preserves iframe state and provides a seamless user experience.
  • - The refresh_token postMessage approach is preferred over iframe reload because it maintains user progress, scroll position, and any in-progress work.

Ready to integrate?

Contact our team to get your workspace secret and start embedding GillyReach today.