Skip to main content

GiGi Developer Portal

Build with the GiGi public API. v0 — partner-traffic gated until your account is flipped to live mode.

← Back to GiGi

API Reference

The GiGi Partner API. Base URL https://app.gigiguides.ai. Every endpoint lives under /api/v2/public/* and authenticates with a bearer API key. The machine-readable contract is published as an OpenAPI 3.1 spec at openapi/gigi-partner-api.yaml and is continuously verified against these route handlers in CI.

Guides

Getting started (first call in 5 minutes)

  1. Create a key on the API Keys page. Choose live or test and the scopes you need. The plaintext key is shown once — store it.
  2. Export it: export GIGI_API_KEY=gg_live_…
  3. Make your first call:
curl https://app.gigiguides.ai/api/v2/public/agents/me \
  -H "Authorization: Bearer $GIGI_API_KEY"

You'll get the agent + account that own the key. Every 2xx response carries x-ratelimit-remaining and x-gigi-environment headers.

Sending leads

Push a lead into GiGi with the POST /api/v2/public/leads call (scope write:leads). Send first_name (or a single name we split) plus at least one of email / phone.

Include an optional consent object to attest the communication consent you captured on your own form: its status (express_written, inbound_initiated, or none), the exact text_snapshot the lead saw, and captured_at. Omit it (or send status: none) and the lead is created respond-only — GiGi will answer inbound but never initiate outbound contact until express written consent is on file. A lead created with a gg_test_ key is a sandbox echo: validated and returned but never persisted, so the live pipeline never fires on it (see Test mode).

Receiving webhooks (with signature verification)

Register an HTTPS endpoint with the /api/v2/public/webhooks/subscribe call below. When a subscribed event fires we POST a signed JSON envelope to your target_url. Every delivery carries these headers:

  • x-gigi-signature: t=<unix>,v1=<hex> — the HMAC signature.
  • x-gigi-timestamp — the same unix timestamp.
  • x-gigi-event, x-gigi-event-id, x-gigi-delivery-id — event type, stable event id (idempotency key), per-attempt delivery id.

Verify every delivery: compute HMAC-SHA256(secret, "<t>.<raw_body>") over the raw request body, constant-time compare against v1, and reject deliveries whose t is outside your tolerance (5 minutes recommended). Return any 2xx to acknowledge — a non-2xx or timeout triggers up to 3 retries with exponential backoff.

Envelope

{
  "id": "evt_9c2f…",
  "type": "sentinel.fired",
  "account_id": "acc_9a…",
  "created_at": "2026-07-03T12:00:00.000Z",
  "data": {
    "firing_id": "fir_demo_01",
    "sentinel_name": "missed_followup_72h",
    "triggered_at": "2026-07-03T12:00:00.000Z",
    "action_taken": "notify_agent"
  }
}

Node (Express)

const crypto = require("crypto");

// Verify a GiGi webhook delivery. Pass the RAW (unparsed) request body.
function verifyGigiWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  // signatureHeader looks like: "t=1720008000,v1=9f86d0..."
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((kv) => {
      const i = kv.indexOf("=");
      return [kv.slice(0, i), kv.slice(i + 1)];
    }),
  );
  const t = Number(parts.t);
  const provided = parts.v1;
  if (!t || !provided) return false;

  // Replay protection: reject deliveries outside your tolerance window.
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`, "utf8")
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(provided);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: capture the raw body so the signature matches byte-for-byte.
const express = require("express");
const app = express();
app.post(
  "/hooks/gigi",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const ok = verifyGigiWebhook(
      req.body.toString("utf8"),
      req.get("x-gigi-signature") || "",
      process.env.GIGI_WEBHOOK_SECRET,
    );
    if (!ok) return res.status(400).send("bad signature");
    const event = JSON.parse(req.body.toString("utf8"));
    console.log("verified", event.type, event.id);
    res.sendStatus(200); // 2xx acknowledges; non-2xx triggers a retry.
  },
);

Python (Flask)

import hashlib, hmac, time, json
from flask import Flask, request, abort

app = Flask(__name__)

def verify_gigi_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
    # signature_header looks like: "t=1720008000,v1=9f86d0..."
    try:
        parts = dict(p.split("=", 1) for p in signature_header.split(","))
        t = int(parts["t"])
        provided = parts["v1"]
    except (KeyError, ValueError):
        return False

    # Replay protection: reject deliveries outside your tolerance window.
    if abs(time.time() - t) > tolerance:
        return False

    signed = f"{t}.".encode("utf-8") + raw_body
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, provided)

@app.post("/hooks/gigi")
def gigi_hook():
    ok = verify_gigi_webhook(
        request.get_data(),  # raw bytes
        request.headers.get("x-gigi-signature", ""),
        app.config["GIGI_WEBHOOK_SECRET"],
    )
    if not ok:
        abort(400)
    event = json.loads(request.get_data())
    print("verified", event["type"], event["id"])
    return "", 200  # 2xx acknowledges; non-2xx triggers a retry.

Test mode

Keys come in two environments. A gg_test_ key authenticates against the same endpoints as a gg_live_ key, but its writes are flagged sandbox: a webhook subscription created with a test key is ignored by the live event fan-out, so wiring up an integration never triggers real partner deliveries. The x-gigi-environment response header tells you which environment served each request.

Rate limits & errors

Each key is limited to 100 requests per minute (token bucket). Over the limit returns 429 with retry_after_seconds and a Retry-After header. Successful responses echo:

  • x-ratelimit-limitPer-minute cap (currently 100).
  • x-ratelimit-remainingTokens left in the current window.
  • x-ratelimit-reset-msMilliseconds until the bucket refills to full.
  • x-gigi-environment'live' or 'test' — which key environment served the request.

See the full error catalog below.

Key rotation

Rotate a key from the API Keys page. Rotation mints a successor key and puts the old key into a grace window (7 days by default) during which both keys work — swap the secret in your systems, then let the old key lapse. Keys can also carry a hard expiry (1 year by default) and an optional per-key IP allowlist. Expired or past-grace keys return 401 with a specific reason.

Endpoints

GET/api/v2/public/agents/meread:agent

Returns the agent who owns the API key and their account. A partner-safe subset — auth tokens, billing, and internal flags are never exposed.

Example response

{
  "agent": {
    "id": "3f2b…",
    "email": "agent@example.com",
    "full_name": "Jordan Rivera",
    "joined_at": "2026-01-04T18:22:10.000Z"
  },
  "account": {
    "id": "acc_9a…",
    "name": "Rivera Group",
    "activation_mode": "live"
  },
  "api_version": "v2"
}
GET/api/v2/public/coaching/insightsread:coaching

Returns GiGi's most recent coaching insights for the account, newest first. Until an account has real coaching history, a small sample set is returned (each item's `source` is `fallback`) so partners can wire integrations end-to-end; `note` explains when that happens.

Query parameters

  • limit (integer) 1–50, default 10.
  • since (string (ISO 8601)) Only insights created strictly after this timestamp.

Example response

{
  "insights": [
    {
      "id": "ins_demo_strong_week",
      "headline": "Strong week — 3 buyer consults closed.",
      "detail": "Your conversion held above 30% across the last 7 days.",
      "created_at": "2026-07-02T12:00:00.000Z",
      "source": "coaching_run"
    }
  ],
  "count": 1,
  "limit": 10,
  "note": null
}
GET/api/v2/public/sentinels/firingsread:sentinels

Returns the account's sentinel firings, newest first, with cursor pagination. Pass the returned `next_cursor` back as `cursor` to page further into the past. `next_cursor` is null once you reach the end.

Query parameters

  • limit (integer) 1–100, default 25.
  • cursor (string (ISO 8601)) Returns firings triggered strictly before this timestamp.

Example response

{
  "firings": [
    {
      "id": "fir_01…",
      "sentinel_name": "missed_followup_72h",
      "triggered_at": "2026-07-03T09:15:00.000Z",
      "trigger_data": {
        "lead_id": "ld_88",
        "hours_since_contact": 74
      },
      "action_taken": "notify_agent"
    }
  ],
  "count": 1,
  "next_cursor": "2026-07-03T09:15:00.000Z",
  "limit": 25
}
POST/api/v2/public/webhooks/subscribewrite:webhooks

Registers an HTTPS endpoint to receive signed event deliveries. The `secret` is returned exactly once — store it; you cannot fetch it again. A subscription created with a gg_test_ key is a sandbox subscription the live event fan-out ignores. Target URLs are validated against an SSRF guard at registration and again at every delivery.

Request body

{
  "target_url": "https://example.com/hooks/gigi",
  "events": [
    "agent.coaching_insight_created",
    "sentinel.fired"
  ],
  "description": "Production listener"
}

Example response

{
  "subscription": {
    "id": "whs_01…",
    "target_url": "https://example.com/hooks/gigi",
    "events": [
      "agent.coaching_insight_created",
      "sentinel.fired"
    ],
    "description": "Production listener",
    "environment": "live",
    "created_at": "2026-07-03T12:00:00.000Z"
  },
  "secret": "shown-exactly-once-64-hex-chars…",
  "signing_scheme": "X-GiGi-Signature: t=<unix>,v1=hex(hmac_sha256(secret, t.body))"
}
POST/api/v2/public/leadswrite:leads

Pushes a lead into GiGi. Provide `first_name` (or a single `name` we split) plus at least one of `email` / `phone`. The optional `consent` object is partner-attested communication consent — `status` (`express_written` | `inbound_initiated` | `none`), the exact `text_snapshot` the lead saw on your form, and when it was captured (`captured_at`, ISO 8601). Omit `consent` (or send `status: none`) and the lead is created respond-only. A lead created with a gg_test_ key is a sandbox echo — it is validated and returned but never persisted, so the live pipeline never fires on it.

Request body

{
  "first_name": "Jamie",
  "last_name": "Nguyen",
  "email": "jamie@example.com",
  "phone": "+15125550142",
  "consent": {
    "status": "express_written",
    "text_snapshot": "I agree to receive calls and texts about my inquiry. Msg & data rates may apply. Reply STOP to opt out.",
    "captured_at": "2026-07-03T15:04:05.000Z"
  }
}

Example response

{
  "lead": {
    "id": "3f2b…",
    "first_name": "Jamie",
    "last_name": "Nguyen",
    "email": "jamie@example.com",
    "phone": "+15125550142",
    "consent_status": "express_written",
    "environment": "live"
  },
  "sandbox": false,
  "api_version": "v2"
}
DELETE/api/v2/public/webhooks/{id}write:webhooks

Marks the subscription revoked. The row is retained so the delivery log keeps its history; outbound dispatch excludes it going forward. Revoking an already-revoked subscription is idempotent (200 with `already_revoked: true`).

Example response

{
  "ok": true
}

Scopes

Key scopes

  • read:agentRead the API key owner's agent + account profile.
  • read:coachingRead GiGi coaching insights for the account.
  • read:sentinelsRead the account's sentinel firing history.
  • write:webhooksRegister and revoke outbound webhook subscriptions.
  • write:leadsCreate leads in the account (partner lead-intake).

Quickstart samples

curl

Read the agent profile:

curl https://app.gigiguides.ai/api/v2/public/agents/me \
  -H "Authorization: Bearer $GIGI_API_KEY"

Read coaching insights & sentinel firings:

curl "https://app.gigiguides.ai/api/v2/public/coaching/insights?limit=5" \
  -H "Authorization: Bearer $GIGI_API_KEY"
curl "https://app.gigiguides.ai/api/v2/public/sentinels/firings?limit=10" \
  -H "Authorization: Bearer $GIGI_API_KEY"

Subscribe to & revoke a webhook:

curl -X POST https://app.gigiguides.ai/api/v2/public/webhooks/subscribe \
  -H "Authorization: Bearer $GIGI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "target_url": "https://example.com/hooks/gigi",
    "events": ["agent.coaching_insight_created", "sentinel.fired"],
    "description": "Production listener"
  }'
curl -X DELETE https://app.gigiguides.ai/api/v2/public/webhooks/<subscription_id> \
  -H "Authorization: Bearer $GIGI_API_KEY"

Node

// Node 18+ (global fetch). npm i not required.
const res = await fetch(
  "https://app.gigiguides.ai/api/v2/public/coaching/insights?limit=5",
  { headers: { Authorization: `Bearer ${process.env.GIGI_API_KEY}` } },
);
if (!res.ok) throw new Error(`GiGi API ${res.status}: ${await res.text()}`);
const { insights } = await res.json();
console.log(insights);

Python

import os, requests

res = requests.get(
    "https://app.gigiguides.ai/api/v2/public/coaching/insights",
    headers={"Authorization": f"Bearer {os.environ['GIGI_API_KEY']}"},
    params={"limit": 5},
)
res.raise_for_status()
print(res.json()["insights"])

Error catalog

Error responses

  • 401 unauthorizedMissing, malformed, unknown, revoked, or expired bearer token. Body: { error, reason }. Reasons: "missing_bearer_token", "malformed_api_key", "unknown_api_key", "api_key_revoked", "api_key_expired", "api_key_grace_expired".
  • 403 forbiddenValid key, but it lacks the required scope or the source IP is outside the key's allowlist. Body: { error, reason }. Reasons: "insufficient_scope", "ip_not_allowed".
  • 403 too_many_failed_attemptsThe source IP crossed the failed-auth threshold and is temporarily locked out. Body: { error, retry_after_seconds }; a Retry-After header is also set. Reasons: "ip_locked_out".
  • 429 rate_limitedPer-key rate limit exceeded (100 req/min). Body: { error, retry_after_seconds }; a Retry-After header is also set. Reasons: "rate_limited".
  • 400 invalid_body / invalid_target_url / no_valid_events / invalid_idRequest-shape errors on the webhook endpoints (bad JSON, missing/blocked target_url, no recognized events, or a missing subscription id). Reasons: "json_parse_failed", "events_array_empty_or_unknown", "<ssrf reason>".
  • 404 subscription_not_foundDELETE referenced a subscription id not on your account.
  • 500 internal_errorUnexpected server error. Safe to retry with backoff; no partial writes are made on the read endpoints.

Webhook events

agent.coaching_insight_created

GiGi has a new coaching insight ready for the agent to review.

Fires when: A coaching loop run completes and produces at least one new insight.

Sample payload
{
  "id": "evt_demo_01",
  "type": "agent.coaching_insight_created",
  "account_id": "<your account uuid>",
  "created_at": "2026-07-03T12:00:00Z",
  "data": {
    "insight_id": "ins_demo_01",
    "headline": "You closed 3 buyer consults this week — strong momentum.",
    "created_at": "2026-05-03T12:00:00Z"
  }
}

sentinel.fired

A platform sentinel fired for the agent's account (e.g. data drift, missed follow-up).

Fires when: Any sentinel records a firing on this account.

Sample payload
{
  "id": "evt_demo_01",
  "type": "sentinel.fired",
  "account_id": "<your account uuid>",
  "created_at": "2026-07-03T12:00:00Z",
  "data": {
    "firing_id": "fir_demo_01",
    "sentinel_name": "missed_followup_72h",
    "triggered_at": "2026-05-03T12:00:00Z",
    "action_taken": "notify_agent"
  }
}

deal.stage_changed

A deal moved between pipeline stages.

Fires when: Any deal's stage column transitions to a new value.

Sample payload
{
  "id": "evt_demo_01",
  "type": "deal.stage_changed",
  "account_id": "<your account uuid>",
  "created_at": "2026-07-03T12:00:00Z",
  "data": {
    "deal_id": "deal_demo_01",
    "from_stage": "consultation",
    "to_stage": "active_buyer",
    "changed_at": "2026-05-03T12:00:00Z"
  }
}

client.contacted

GiGi recorded a contact event with a client (call, email, or SMS).

Fires when: A contact log entry is written via the dialer, email adapter, or SMS adapter.

Sample payload
{
  "id": "evt_demo_01",
  "type": "client.contacted",
  "account_id": "<your account uuid>",
  "created_at": "2026-07-03T12:00:00Z",
  "data": {
    "contact_id": "ct_demo_01",
    "client_id": "cl_demo_01",
    "channel": "phone",
    "occurred_at": "2026-05-03T12:00:00Z"
  }
}

goal.pace_changed

The agent's goal pace crossed an alert threshold.

Fires when: The goal pacing aggregator detects the agent has moved between on-pace, behind, or ahead.

Sample payload
{
  "id": "evt_demo_01",
  "type": "goal.pace_changed",
  "account_id": "<your account uuid>",
  "created_at": "2026-07-03T12:00:00Z",
  "data": {
    "goal_id": "g_demo_01",
    "pace": "behind",
    "previous_pace": "on_pace",
    "changed_at": "2026-05-03T12:00:00Z"
  }
}

See the Versioning & Deprecation policy for our stability promise.