Add B2B prospecting and lead generation to your platform with our simple iframe integration.
postMessage events.workspace_iduser_emaillist_id (optional)should_confirm (boolean)host_originscopesexpPOST/iframe/embed_tokens
Auth: Authorization: Bearer <workspace_secret>
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
}'
{
"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.
GET/iframe/get_lists
Auth: Embed token
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
}'
{
"lists": [
{
"list_id": "lst_123",
"name": "Denver GCs",
"created_at": "2025-01-12T18:22:00Z",
"counts": { "companies": 1200, "contacts": 3400 }
}
],
"next_index": 11
}
Note:
GET/iframe/get_list_with_data
Auth: Embed token
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"
}'
{
"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:
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>
Content-Security-Policy: frame-ancestors <allowed host origins>Outbound events include source and type.
{
"source": "gillyreach",
"type": "event_name",
"request_id": "uuid",
"payload": {}
}
All messages sent from the host to the iframe follow this general format:
{
"type": "event_type",
"request_id": "uuid",
"payload": {}
}
confirm_requiredWhen 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 }
}
auth_expiredWhen 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>"
}
}
readyEmitted 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_completeEmitted 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_requiredSent 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_expiredEmitted 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_fundsEmitted 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.
This section shows an example of how to embed GillyReach into your product.
GR_WORKSPACE_SECRET and returns { embed_url } to your frontend.embed_urlStore 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"));
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>
);
}
listId (it will fetch a fresh embed_url and reload the iframe).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.refresh_token postMessage approach is preferred over iframe reload because it maintains user progress, scroll position, and any in-progress work.Contact our team to get your workspace secret and start embedding GillyReach today.