Developer Guide

API documentation for Client Gallery, Drive, Messaging, and embed customer sign-in

gellhub Developer Console

Register integration apps, rotate secrets, and view connect analytics — similar to Stripe Dashboard or Meta Developer tools. Sign in with a gellhub account used by your engineering team (not your end customers).

Plans & pricing API

Fetch published packages and prices without authentication. Use these endpoints to show gellhub plans on your own site or to obtain planId for POST /api/register or POST /api/billing/checkout-session. When you pass your integration client_id, results are limited to the product line you chose for that app in the Developer Console (Client Gallery or Drive).

No authentication required
GET/api/public/plans?type=clientGallery

Returns a JSON array of Client Gallery plans: id, planType, name, description, features, storageLimitBytes, price, billingPeriod (monthly | annual), clientGalleryEnabled.

No authentication required
GET/api/public/plans?type=drive

Same shape as above for Drive plans (planType is drive).

No authentication required
GET/api/public/plans?client_id=YOUR_CLIENT_ID

Returns { allowedProductTypes, plans } — plans are only for the single product line (Client Gallery or Drive) bound to that app. Optional &type=clientGallery|drive must match the app (403 otherwise).

No authentication required
GET/api/public/plans/by-id?id=PLAN_CUID&client_id=YOUR_CLIENT_ID

Returns one plan by id. With client_id, returns 403 if the plan’s product line is not allowed for that app (omit client_id for a generic public lookup).

Base URL examples: https://gellhub.com/api/public/plans?type=drive · https://gellhub.com/api/public/plans?client_id=YOUR_CLIENT_ID

Embedded connect (external sites & add-ons)

Use this when your product sends users to gellhub to sign up or subscribe; after checkout they return to your registered redirect URL with a one-time code. Only your backend can exchange the code, using your client id and client secret (OAuth-style confidential client). Never log the code or secret in the browser.

1. Partner app credentials

Your engineering team signs in to the gellhub Developer Console and creates one app per product environment. Each app is tied to one product line: Client Gallery or Drive (more lines may appear later). Register each exact callback URL you will use as return_url, then copy the client_id and one-time client_secret to your backend immediately: the secret is shown only once in the Console (yellow box) after create or after "Rotate secret" — gellhub stores only a hash, so the list view shows client_id only. End users of your product do not use the Console. Platform staff can moderate apps from admin Partner apps.

2. Security model

return_url must match a registered URI for that client_id. The exchange endpoint requires code + client_id + client_secret; codes are bound to the app that started the flow.

No authentication required
GET/api/integrations/connect/start?client_id=YOUR_CLIENT_ID&return_url=https://yourapp.com/oauth/gellhub/callback&state=RANDOM&plan=PLAN_ID&product=drive|clientGallery|messaging&next=register|login

Starts the flow: looks up your app, requires return_url to exactly match a registered redirect URI, sets httpOnly cookies, redirects to /register or /login. The product line must be allowed for this client_id: pass product=drive, product=clientGallery, or product=messaging, or pass plan= only (product is inferred from the plan and must still be allowed for the app).

No authentication required
GET/api/integrations/connect/context

Returns { active: true } when connect cookies are present (so your in-app UI can adjust copy). No secrets.

No authentication required
POST/api/integrations/connect/finalize

Browser session only. After signup/login (and when no Stripe redirect is pending), returns { redirectUrl } to your return_url with ?code=…&state=…. Clears connect cookies.

{}
No authentication required
POST/api/integrations/connect/exchange

Partner backend only: redeem the code once with your app credentials. Returns profile id, email, studioId, driveId, apiKey, driveApiKey, messagingApiKey (auto-issued when messaging subscription exists), hasMessagingSubscription, accountKind, plan snapshot, and state. Wrong client or secret → 401; reused or expired code → 400. See also Partner verification below for ongoing checks.

{
  "code": "from_return_url_query",
  "client_id": "same as start",
  "client_secret": "from server env only"
}

After sign-in or registration, gellhub returns the user to your return_url with a one-time code — not to the gellhub dashboard — when embedded connect cookies are active. Email login returns connectRedirect from POST /api/login; Google sign-in and Stripe success use server redirects to the same partner URL. Paid gallery/drive plans may open Stripe first; success lands on GET /integrations/connect/stripe-return then your callback.

Paid plans: with connect cookies active, Stripe Checkout success is routed to GET /integrations/connect/stripe-return?session_id=… which verifies the Checkout Session and then redirects to your return_url with the same code pattern. You can still pass successUrl on POST /api/register if you build the URL yourself (must include {CHECKOUT_SESSION_ID}).

// 1) Redirect user to gellhub (client_id from Developer Console → Applications; return_url must match registered URIs)
window.location.href =
  'https://gellhub.com/api/integrations/connect/start?' +
  'client_id=' + encodeURIComponent('YOUR_CLIENT_ID') +
  '&return_url=' + encodeURIComponent('https://yourapp.com/api/gellhub/callback') +
  '&state=' + encodeURIComponent(csrfState) +
  '&plan=' + planId; // product inferred from plan; must match app’s allowed services in Console

// 2) User finishes on gellhub → browser lands on your return_url?code=...&state=...

// 3) Your server only: exchange (never from the browser)
const r = await fetch('https://gellhub.com/api/integrations/connect/exchange', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    code,
    client_id: process.env.GELLHUB_CLIENT_ID,       // server env
    client_secret: process.env.GELLHUB_CLIENT_SECRET,
  }),
});
const account = await r.json();
// store account.profileId + keys for this merchant in your database

Partner verification

After Embedded connect, your SaaS stores a mapping between your tenant id (store, studio, merchant) and the gellhub profileId returned from POST /api/integrations/connect/exchange. Use the endpoints below to confirm the merchant is still registered, still linked to your app, and still subscribed to the product line you sell.

What gellhub does not provide

  • No lookup by email — the merchant must complete connect first.
  • No knowledge of your internal store ids — you map yourStoreId ↔ profileId.
  • Do not call verify or exchange from the browser — server + client_secret only.
No authentication required
POST/api/integrations/connect/verify

Check that a merchant profile is still registered on gellhub and linked to your client_id. Returns registered, linked, email, displayName, accountKind, hasMessagingSubscription, and plan snapshot (scope: gallery | drive | messaging). 404 when profile_id was never linked to your app.

{
  "profile_id": "from exchange",
  "client_id": "same as connect/start",
  "client_secret": "from server env only"
}
Header: x-api-key: YOUR_API_KEY
GET/api/billing/status

Authoritative subscription check for a connected merchant. Call from your backend with the merchant's stored API key in x-api-key (sk_… gallery, drive key, or msg_… messaging). Returns scope, plan id/name/price, stripeStatus, currentPeriodEnd. Treat price=0 as eligible; for paid plans require stripeStatus active, trialing, or past_due.

Typical SaaS flow: on connect callback → exchange → save profileId + product key → enable the add-on. On a schedule or before sensitive actions → billing/status with the stored key; optionally connect/verify to confirm the link is still valid without exposing keys.

// After connect: store profileId + messagingApiKey (or apiKey / driveApiKey) per merchant

// Later — is this merchant still linked to our app?
const link = await fetch('https://gellhub.com/api/integrations/connect/verify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    profile_id: merchant.gellhubProfileId,
    client_id: process.env.GELLHUB_CLIENT_ID,
    client_secret: process.env.GELLHUB_CLIENT_SECRET,
  }),
});
if (link.status === 404) { /* never connected or unlinked */ }

// Later — is their Messaging subscription still active?
const bill = await fetch('https://gellhub.com/api/billing/status', {
  headers: { 'x-api-key': merchant.messagingApiKey },
});
const j = await bill.json();
const eligible =
  j.plan &&
  (j.plan.price <= 0 ||
    ['active', 'trialing', 'past_due'].includes(String(j.stripeStatus || '').toLowerCase()));

Authentication

Use only the API key. The platform derives your account from the key — no need to pass studioId or driveId in requests. Use apiKey (Client Gallery) or driveApiKey (Drive). Your Studio ID and Drive ID are shown in the dashboard for reference.

Required header (key only)

Send your API key in the x-api-key header. No account ID in the request.

x-api-key: sk_your_api_key_here

POST /api/register

{
  "email": "studio@example.com",
  "password": "your-password",
  "displayName": "My Studio",
  "accountType": "studio",
  "planId": "optional_plan_id_from_GET_/api/public/plans",
  "successUrl": "https://yourapp.com/billing/done?sid={CHECKOUT_SESSION_ID}",
  "cancelUrl": "https://yourapp.com/billing/cancel"
}

// successUrl / cancelUrl: optional absolute URLs for Stripe return (omit to use platform dashboard defaults).
// planId must match accountType (clientGallery plan + studio, or drive plan + drive).

// Response includes keys + billing:
// - apiKey / driveApiKey: use in x-api-key for API calls
// - requiresPayment: true if plan has price > 0
// - checkoutUrl: open in browser to pay (same Stripe Checkout as the website)
// - checkoutError: if payment required but session failed — retry POST /api/billing/checkout-session with x-api-key

Drive API

File storage with folders. Send driveApiKey in the x-api-key header — no Drive ID in the request. External apps and large or batched uploads should use /api/drive/upload/presign then PUT to Wasabi, then /api/drive/upload/complete (same header on all three steps). Direct /api/drive/upload streams the whole file through this server and often times out on big files; your bucket CORS must allow PUT from the browser or server origin that performs the upload.

Partner apps (Developer Console): after the user completes POST /api/integrations/connect/exchange, uploads may include header x-gellhub-integration-client-id: YOUR_CLIENT_ID (or JSON field integrationClientId on presign/complete). gellhub creates a top-level Drive folder named like your app and stores files there; the Drive web UI treats that tree as read-only while your backend keeps full API access when the same header is sent. Omit the header for normal user uploads to My Drive root or a folder they own.

Header: x-api-key: YOUR_API_KEY
GET/api/drive?parentId= (optional) &includeIntegrationAppFolders=1 (optional; API keys always see app root folders at root)

List folders and files. Browser dashboard hides partner app root folders unless includeIntegrationAppFolders=1.

Header: x-api-key: YOUR_API_KEY
POST/api/drive

Create folder

{
  "name": "My Folder",
  "parentId": null
}
Header: x-api-key: YOUR_API_KEY
POST/api/drive/upload

Upload file (to root or folder; server proxies to storage)

FormData: folderId (optional), file
Header: x-api-key: YOUR_API_KEY
POST/api/drive/upload/presign

Presigned PUT with x-api-key. With integration client id (header or body), files go under that app’s folder after connect exchange linked the account.

{
  "fileName": "",
  "contentType": "",
  "sizeBytes": 0,
  "folderId": null,
  "integrationClientId": "optional; or header x-gellhub-integration-client-id"
}
Header: x-api-key: YOUR_API_KEY
POST/api/drive/upload/complete

Register Drive file after successful PUT; send the same integration header/body as presign when used.

{
  "storageKey": "",
  "fileName": "",
  "contentType": "",
  "sizeBytes": 0,
  "folderId": null,
  "integrationClientId": "optional; match presign"
}
Header: x-api-key: YOUR_API_KEY
GET/api/drive/items/[id]?type=file|folder

JSON metadata; for files includes url and mediaToken for /api/drive/media (used by partner backends to resolve playback URL)

Header: x-api-key: YOUR_API_KEY
PATCH/api/drive/items/[id]?type=file|folder

Rename or move file/folder

{
  "name": "New name",
  "targetFolderId": "folder_id"
}
Header: x-api-key: YOUR_API_KEY
DELETE/api/drive/items/[id]?type=file|folder

Move to Recycle Bin (soft delete)

Header: x-api-key: YOUR_API_KEY
GET/api/drive/trash

List items in Recycle Bin

Header: x-api-key: YOUR_API_KEY
POST/api/drive/trash

Restore items from Recycle Bin

{
  "fileIds": [],
  "folderIds": []
}
Header: x-api-key: YOUR_API_KEY
DELETE/api/drive/trash

Permanently delete from Recycle Bin

{
  "fileIds": [],
  "folderIds": []
}
Header: x-api-key: YOUR_API_KEY
GET/api/drive/storage

Get used bytes and limit (subscription plan)

Header: x-api-key: YOUR_API_KEY
GET/api/drive/media?t=TOKEN

Stream Drive file (use token from files response)

Messaging API

For registered GellHub accounts with an active Messaging plan. Sign in, open Dashboard → Messaging, generate a msg_… key, and send it as x-api-key on the endpoints below. Use this to connect WhatsApp bridges, mobile apps, or your own backend while keeping bot training in the dashboard.

GellHub AI engine. Inference runs on the platform — you do not supply OpenAI or other provider API keys. Each agent stores a modelKey from your messaging plan's model catalog (e.g. openai:gpt-4o-mini). Chat turns use that agent's model; credit cost varies by model.

SaaS partners can onboard Messaging via Embedded connect with product=messaging and a plan id from the public plans API.

SaaS onboarding flow

  1. Register a Developer app with allowedProductTypes: ["messaging"] in the Console.
  2. Connect each merchant via Embedded connect (product=messaging).
  3. Merchant generates msg_… in Dashboard → Messaging → API key (or your UI calls dashboard session endpoints).
  4. Your backend calls GET /api/messaging/v1/models, then POST /api/messaging/v1/agents with a modelKey from that list, then POST /api/messaging/v1/sources to upload knowledge for that agent, then PUT /api/messaging/v1/embed with the merchant's key (set embedAuthMode and your embedAuthClientId if shoppers must sign in).
  5. Paste embedCodeFloating on the tenant site or share embedShareLink in ads. Use Customer connect when your app needs the signed-in shopper's identity.
  6. Route customer messages through POST /api/messaging/v1/chat (WhatsApp bridge, app, etc.).
  7. Enable Dynamic skills so booking/product questions call your webhook (0 AI credits).

Account & chat API

Dashboard-only (cookie session)

End users manage keys in the dashboard. Integrators use the msg_… key on all /api/messaging/v1/* routes below.

No authentication required
GET/api/messaging/account

Credits balance, inbound/outbound counts, masked API key flag.

No authentication required
POST/api/messaging/api-key

Create or rotate the Messaging API key (returns full msg_… once).

Bot & chat (x-api-key)

Header: x-api-key: YOUR_API_KEY
GET/api/messaging/v1/bot

Read-only snapshot: account credits, all agents, default agent (persona + handoff), embed settings, embedShareLink, HTML install snippets.

Header: x-api-key: YOUR_API_KEY
POST/api/messaging/v1/chat

Run one AI turn. Response includes assistant content and creditsCharged. Do not send model or provider fields on chat — the model comes from the agent's modelKey (see AI models below).

{
  "channel": "api",
  "agentId": "optional-agent-cuid",
  "conversationRef": "whatsapp:+15551234567:thread-1",
  "messages": [
    {
      "role": "user",
      "content": "Hello"
    }
  ]
}

AI models & plans

Each messaging plan defines which catalog models subscribers may use. Platform admins configure this in Admin → Packages (Messaging plans → Allowed AI models). If no models are selected for a plan, all active catalog models are allowed.

When creating or updating an agent, pass modelKey from the allowed list. Omit it to use the first model on the plan. Requests with a model outside the plan return 400 (Selected model is not included in your messaging package.). Each model includes an estimated creditsPerMessage for UI pricing hints; actual chat responses return creditsCharged per turn.

Header: x-api-key: YOUR_API_KEY
GET/api/messaging/v1/models

List models allowed on the connected account's messaging plan: key, displayName, provider, creditsPerMessage.

No authentication required
GET/api/messaging/available-models

Same shape as v1/models, for dashboard cookie sessions (Messaging UI).

Agents API (x-api-key)

Create and manage bots for a connected account. Max 25 agents per profile. At least one agent must remain. Fields: name, botDisplayName, botRoleSummary, systemInstructions, modelKey (catalog key from your plan), handoff URLs, isDefault.

Header: x-api-key: YOUR_API_KEY
GET/api/messaging/v1/agents

List agents (id, name, botDisplayName, modelKey, isDefault, sourcesCount, updatedAt).

Header: x-api-key: YOUR_API_KEY
POST/api/messaging/v1/agents

Create an agent. First agent becomes default automatically. modelKey must be on the account's messaging plan (GET /api/messaging/v1/models).

{
  "name": "Sales bot",
  "botDisplayName": "Store assistant",
  "botRoleSummary": "Help with products and prices",
  "systemInstructions": "Be friendly and concise.",
  "modelKey": "openai:gpt-4o-mini",
  "humanHandoffEnabled": false,
  "isDefault": true
}
Header: x-api-key: YOUR_API_KEY
GET/api/messaging/v1/agents/{id}

Full agent record including systemInstructions, modelKey, and handoff settings.

Header: x-api-key: YOUR_API_KEY
PATCH/api/messaging/v1/agents/{id}

Partial update. Set isDefault:true to make this the default agent for chat and widget. modelKey must stay within the plan catalog.

{
  "botDisplayName": "Updated name",
  "systemInstructions": "New instructions…",
  "modelKey": "openai:gpt-4o",
  "isDefault": true
}
Header: x-api-key: YOUR_API_KEY
DELETE/api/messaging/v1/agents/{id}

Delete an agent. Fails if it is the only agent on the account.

Knowledge sources API (x-api-key)

Static knowledge for each agent: FAQs, pricing tables, policies, or free-form text. Send structured JSON — GellHub builds the bot-ready bodyText for you (no manual formatting). Use kind: faq, pricing, policy, or text (default). For live inventory, slots, or cart data, use Dynamic skills instead.

Scoped to one agentId. Limits: 40 sources per agent, 120,000 chars per source, 400,000 chars total per agent. Used in POST /api/messaging/v1/chat. Dashboard cookie sessions may use /api/messaging/sources; external apps use v1 with msg_….

Header: x-api-key: YOUR_API_KEY
GET/api/messaging/v1/sources?agentId=AGENT_CUID

List sources (id, title, kind, mimeType, bodyPreview, bodyChars, createdAt).

Header: x-api-key: YOUR_API_KEY
POST/api/messaging/v1/sources

kind "faq" + faqs[]. Title defaults to "FAQ". GellHub stores formatted text for the bot.

{
  "agentId": "agent-cuid",
  "kind": "faq",
  "faqs": [
    {
      "question": "Do you ship internationally?",
      "answer": "Yes, to EU and UK within 5–7 days."
    },
    {
      "question": "What is your return window?",
      "answer": "30 days unused, with receipt."
    }
  ]
}
Header: x-api-key: YOUR_API_KEY
POST/api/messaging/v1/sources

kind "pricing" + plans[] (name, price required; period, features optional). Title defaults to "Pricing".

{
  "agentId": "agent-cuid",
  "kind": "pricing",
  "plans": [
    {
      "name": "Starter",
      "price": "$29",
      "period": "per month",
      "features": [
        "1 agent",
        "5k messages"
      ]
    },
    {
      "name": "Pro",
      "price": "$99",
      "period": "per month",
      "features": [
        "Unlimited agents"
      ]
    }
  ]
}
Header: x-api-key: YOUR_API_KEY
POST/api/messaging/v1/sources

kind "policy" + sections[] or a single content string. Title defaults to "Policies".

{
  "agentId": "agent-cuid",
  "kind": "policy",
  "sections": [
    {
      "heading": "Shipping",
      "content": "Orders ship within 2 business days."
    },
    {
      "heading": "Returns",
      "content": "Contact support within 30 days."
    }
  ]
}
Header: x-api-key: YOUR_API_KEY
POST/api/messaging/v1/sources

kind "text" (or omit kind): supply bodyText directly. Multipart: agentId + optional title + .txt file.

{
  "agentId": "agent-cuid",
  "kind": "text",
  "title": "About us",
  "bodyText": "We are a boutique studio founded in 2018…"
}
Header: x-api-key: YOUR_API_KEY
GET/api/messaging/v1/sources/{id}

Full source including kind and bodyText.

Header: x-api-key: YOUR_API_KEY
PATCH/api/messaging/v1/sources/{id}

Update by kind: send faqs, plans, sections/content, or bodyText. Rebuilds stored text when structured fields are sent.

{
  "faqs": [
    {
      "question": "Holiday hours?",
      "answer": "Closed Dec 24–26."
    }
  ]
}
Header: x-api-key: YOUR_API_KEY
DELETE/api/messaging/v1/sources/{id}

Remove a knowledge source.

Dynamic skills webhook

For live data (booking slots, product catalog, forms). GellHub detects keywords, POSTs to your HTTPS endpoint, validates the JSON response, and renders UI in the chat widget — without asking the LLM to build the interface. Skill turns cost 0 credits.

No authentication required
GET/api/messaging/skills-config

Dashboard session: read skillsEnabled, webhook URL, keyword triggers.

No authentication required
PUT/api/messaging/skills-config

Dashboard session: save webhook + skill keyword triggers.

{
  "skillsEnabled": true,
  "skillsWebhookUrl": "https://yourstore.com/api/gellhub/skills",
  "skillsWebhookSecret": "your-hmac-secret",
  "skills": [
    {
      "key": "booking",
      "label": "Booking",
      "keywords": [
        "book",
        "appointment",
        "schedule"
      ]
    },
    {
      "key": "catalog",
      "label": "Products",
      "keywords": [
        "product",
        "price",
        "catalog"
      ]
    }
  ]
}
Header: x-api-key: YOUR_API_KEY
GET/api/messaging/v1/skills

x-api-key (msg_…): read skills config for SaaS onboarding.

Header: x-api-key: YOUR_API_KEY
PUT/api/messaging/v1/skills

x-api-key (msg_…): configure skills for a merchant programmatically.

{
  "skillsEnabled": true,
  "skillsWebhookUrl": "https://yourstore.com/api/gellhub/skills",
  "skills": [
    {
      "key": "booking",
      "label": "Booking",
      "keywords": [
        "book",
        "appointment"
      ]
    }
  ]
}

Your webhook receives (POST):

{
  "version": 1,
  "skill": "booking",
  "profileId": "…",
  "conversationRef": "web:emb_…:visitor-id",
  "visitorId": "visitor-id",
  "channel": "web",
  "message": "What times are available tomorrow?",
  "messages": [{ "role": "user", "content": "…" }]
}

Headers: X-GellHub-Timestamp, X-GellHub-Skill, optional X-GellHub-Signature (HMAC-SHA256 of timestamp + "." + body).

Respond with JSON (version 1):

{
  "version": 1,
  "message": "Available times tomorrow:",
  "ui": {
    "type": "slot_list",
    "items": [
      { "id": "10:00", "title": "10:00", "subtitle": "30 min" },
      { "id": "11:30", "title": "11:30", "subtitle": "Available" }
    ]
  }
}

Allowed ui.type values (rendered by GellHub, not the LLM):

  • slot_list — booking/time buttons
  • product_cards — title, price, imageUrl, actionUrl
  • buttons — action message or url
  • iframeurl (HTTPS), optional height for complex booking forms

Chat responses from POST /api/messaging/embed/v1/chat and POST /api/messaging/v1/chat may include an extra field ui alongside content.

Widget settings API (x-api-key)

Enable the floating chat widget, set allowed domains, accent color, greeting, launcher icon, and optional customer sign-in. Response includes embedPublicId (emb_…), embedShareLink, embedAuthMode, and ready-to-paste embedCodeFloating / embedCodeButton.

  • embedLauncherIcon — emoji (e.g. 💬) or public https://… image URL for the floating button
  • embedAllowedOrigins — hostnames only (e.g. shop.example.com)
  • embedAuthMode none | on_open | on_handoff (see Customer sign-in)
  • embedAuthClientId — optional Developer Console client_id used as default when issuing customer connect codes
  • rotateEmbedId — optional on PUT; invalidates old emb_… id
Header: x-api-key: YOUR_API_KEY
GET/api/messaging/v1/embed

Read current widget settings and install snippets.

Header: x-api-key: YOUR_API_KEY
PUT/api/messaging/v1/embed

Save widget settings. Requires at least one allowed domain when embedEnabled is true. embedAuthMode defaults to none.

{
  "embedEnabled": true,
  "embedAllowedOrigins": [
    "tenant-store.com",
    "www.tenant-store.com"
  ],
  "embedAccentColor": "#0d9488",
  "embedLauncherIcon": "💬",
  "embedShowLauncher": true,
  "embedGreeting": "Hi! How can we help?",
  "embedAuthMode": "on_handoff",
  "embedAuthClientId": "YOUR_CLIENT_ID",
  "rotateEmbedId": false
}

Website chat widget (per profile)

Each subscriber profile on GellHub can enable a hosted chat widget scoped to that account only. The browser never sees the secret msg_… key — only a public emb_… embed id plus an Origin allowlist configured in Messaging → Website widget.

SaaS apps with a msg_… key can use PUT /api/messaging/v1/embed to set accent color and floating button icon, and POST /api/messaging/v1/agents to create bots per tenant.

Direct chat link (ads)

https://gellhub.com/chat/emb_…

Full-screen hosted chat (data-mode="fullscreen"). Each page load gets a unique v_{uuid} — ad traffic never reuses a thread, even after Google sign-in on a later visit. The floating widget on a merchant site uses cust_{customerId} after sign-in so returning shoppers resume the same conversation.

Customer sign-in on ad links still works when embedAuthMode is enabled — identity is attached to the current ephemeral thread only (see Customer sign-in).

Install on merchant site

Floating bubble

<script
  src="https://gellhub.com/embed/gellhub-chat.js"
  data-embed-id="emb_…"
  data-chat-title="Store or bot name"
  data-launcher-icon="💬"
  data-launcher="true"
  async
></script>

Custom button

<script
  src="https://gellhub.com/embed/gellhub-chat.js"
  data-embed-id="emb_…"
  data-chat-title="Store or bot name"
  data-launcher="false"
  async
></script>
                  <button type="button" onclick="window.GellHubChat && GellHubChat.open()">Chat</button>

Staff replies: load gellhub-chat.js from https://gellhub.com only — not a copied file on your SaaS server. After handoff the widget opens one EventSource to GET /api/messaging/embed/v1/stream (SSE). Staff replies are pushed instantly — no polling loop.

No authentication required
GET/api/messaging/embed/v1/config?embedId=emb_…

Public CORS: widget bootstrap (chatTitle, accentColor, launcherIcon, greeting, authMode, googleSignInAvailable, maxUserMessageChars, apiBaseUrl). Origin must match allowlist or GellHub /chat/ host.

No authentication required
POST/api/messaging/embed/v1/chat

Public CORS: run one bot turn. Optional header x-gellhub-customer-token when signed in. 401 AUTH_REQUIRED if embedAuthMode is on_open and no token. Response: { role, content, handoff?, aiPaused?, conversationRef? } — no merchant API key exposed.

{
  "embedId": "emb_…",
  "visitorId": "v_uuid-or-cust_id",
  "messages": [
    {
      "role": "user",
      "content": "What are your prices?"
    }
  ]
}
No authentication required
GET/api/messaging/embed/v1/stream?embedId=emb_…&visitorId=v_uuid-or-cust_id&customerToken=JWT

Public CORS (SSE): long-lived connection after handoff. Pass customerToken query or x-gellhub-customer-token when authenticated. Events: ready, update { messages[], aiPaused }, ping.

No authentication required
GET/api/messaging/embed/v1/sync?embedId=emb_…&visitorId=v_uuid-or-cust_id&customerToken=JWT

Public CORS: one-shot thread snapshot (panel open / after send). Send customer token when signed in. Prefer SSE while aiPaused.

No authentication required
GET/api/messaging/embed-config

Dashboard session: read widget settings, embedShareLink, chatTitle, allowed domains, install snippets (cookie auth).

No authentication required
PUT/api/messaging/embed-config

Dashboard session: save widget settings (same fields as PUT /api/messaging/v1/embed, including embedAuthMode and embedAuthClientId). Set rotateEmbedId:true to invalidate an old embed id.

{
  "embedEnabled": true,
  "embedAllowedOrigins": [
    "yourstore.com"
  ],
  "embedShowLauncher": true,
  "embedAccentColor": "#0d9488",
  "embedLauncherIcon": "💬",
  "embedGreeting": "Hi! How can we help?",
  "embedAuthMode": "none",
  "embedAuthClientId": "",
  "rotateEmbedId": false
}

Customer sign-in (embed widget)

Merchants can require end-customers to sign in before chatting — without giving them a GellHub dashboard account. Configure in Dashboard → Messaging → Website widget → Customer sign-in, or programmatically via PUT /api/messaging/v1/embed. The hosted widget handles the UI; use the public embed-auth endpoints below if you build a custom chat shell.

Auth modes

  • none — anonymous chat (default). No sign-in gate.
  • on_open — customer must sign in before the first message. Chat API returns 401 with code: "AUTH_REQUIRED" until authenticated.
  • on_handoff — chat freely with the AI; sign-in is required only when the visitor requests a human. Handoff is blocked until Google (or email) sign-in completes.

Google sign-in (primary)

When GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are set on the platform, the widget shows Continue with Google. OAuth uses the shared callback /api/auth/google/callback (no extra redirect URI needed). Email/password remains available on the hosted page /embed-auth/{embedId}.

Customer session token

After sign-in the widget stores a JWT and sends it on chat, sync, and SSE requests via header x-gellhub-customer-token, Authorization: Bearer …, or query customerToken=…. Tokens are scoped to one merchant embed (emb_…) and expire after 30 days.

Visitor id & thread isolation

  • Merchant site widget — after sign-in, visitorId becomes cust_{customerId} so the same signed-in customer resumes the same thread in that browser.
  • Ad link /chat/emb_… — each page load gets a fresh v_{uuid} thread. Sign-in links the customer identity to that thread only; past ad visits are never merged, even for the same Google account.

Widget events (custom parent UI)

window.addEventListener("message", (event) => {
  const data = event.data;
  if (data?.type === "gellhub:customer-auth") {
    // { embedId, token, customer: { id, email, displayName } }
    // visitorId is cust_* on merchant sites; ad links keep ephemeral v_* on the page
  }
});
No authentication required
GET/api/messaging/embed/v1/config?embedId=emb_…

Public CORS: includes authMode (none | on_open | on_handoff) and googleSignInAvailable. Widget reads these to show the sign-in gate.

No authentication required
GET/api/embed-auth/google?embed_id=emb_…&origin=https://shop.example.com&client_id=OPTIONAL&return_url=OPTIONAL&state=OPTIONAL

Redirect to Google OAuth for embed customers. Optional client_id + return_url + state for partner Customer connect (see below).

No authentication required
GET/api/embed-auth/complete?embedId=emb_…

Public CORS: one-time read of Google OAuth finish cookie. Returns { token, customer, visitorId }. Used by /embed-auth/{embedId}/done after redirect.

No authentication required
GET/api/embed-auth/session?embedId=emb_…

Public CORS: validate customer token (header or customerToken query). Returns { authenticated, authMode, customer, visitorId }.

No authentication required
POST/api/embed-auth/register

Public CORS: create embed customer for this merchant widget. Returns { token, customer, visitorId, partnerRedirect? }. partnerRedirect is set when clientId + returnUrl match a registered Developer app URI.

{
  "embedId": "emb_…",
  "email": "customer@example.com",
  "password": "min-6-chars",
  "displayName": "Jane",
  "clientId": "optional-for-partner-connect",
  "returnUrl": "https://yourapp.com/oauth/customer/callback",
  "state": "csrf-state"
}
No authentication required
POST/api/embed-auth/login

Public CORS: email/password sign-in. Same response shape as register.

{
  "embedId": "emb_…",
  "email": "customer@example.com",
  "password": "password"
}
No authentication required
POST/api/messaging/embed/v1/chat

With authMode on_open and no customer token → 401 AUTH_REQUIRED. Send x-gellhub-customer-token when signed in. Handoff with on_handoff also requires token before pausing AI.

{
  "embedId": "emb_…",
  "visitorId": "v_uuid-or-cust_id",
  "messages": [
    {
      "role": "user",
      "content": "Hello"
    }
  ]
}

Customer connect (partner apps)

When your SaaS embeds GellHub chat on a merchant store, you can learn who signed in (email, display name, customer id) without dashboard access — similar to merchant Embedded connect but for end-customers. Register a Developer app in the Console, enable customer sign-in on the merchant widget (embedAuthModenone), then redirect shoppers through the flow below.

Security model

  • return_url must match a redirect URI registered for your client_id.
  • One-time codes (ecc_…) expire in 10 minutes and are redeemed once with client_secret on your server only.
  • GellHub does not map customers to your internal user ids — you store yourUserId ↔ customer.id after exchange.
No authentication required
GET/api/integrations/customers/connect/start?embed_id=emb_…&client_id=YOUR_CLIENT_ID&return_url=https://yourapp.com/oauth/customer/callback&state=RANDOM&next=login|register

Redirect shopper to hosted sign-in for this widget. Requires widget customer sign-in enabled. Sets connect session when state is provided.

No authentication required
POST/api/integrations/customers/connect/exchange

Partner backend only: redeem customer auth code. Returns { customer: { id, email, displayName }, merchantProfileId, embedPublicId, state }. Links customer to your app via EmbedCustomerAppLink.

{
  "code": "ecc_from_return_url",
  "client_id": "same as start",
  "client_secret": "from server env only"
}
// 1) Before opening chat, send shopper to GellHub sign-in
window.location.href =
  'https://gellhub.com/api/integrations/customers/connect/start?' +
  'embed_id=' + encodeURIComponent('emb_MERCHANT_WIDGET') +
  '&client_id=' + encodeURIComponent(process.env.GELLHUB_CLIENT_ID) +
  '&return_url=' + encodeURIComponent('https://yourstore.com/api/gellhub/customer/callback') +
  '&state=' + encodeURIComponent(csrfState);

// 2) After Google/email sign-in → browser lands on return_url?code=ecc_…&state=…

// 3) Your server only: exchange (never from the browser)
const r = await fetch('https://gellhub.com/api/integrations/customers/connect/exchange', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    code,
    client_id: process.env.GELLHUB_CLIENT_ID,
    client_secret: process.env.GELLHUB_CLIENT_SECRET,
  }),
});
const { customer, merchantProfileId, embedPublicId } = await r.json();
// Map customer.id to your storefront user; open widget with their session token if you host custom UI

Human handoff (SaaS inbox + AI pause)

When a visitor asks for a real person, GellHub pauses the AI bot for that conversation (handoffPendingAt / aiPaused), sends your handoff webhook if configured, and creates a notification in GellHub Messaging → Conversations. Staff reply from your app or GellHub; call reenableAi when done — replying alone does not turn the bot back on. If the widget uses embedAuthMode: on_handoff, the visitor must complete customer sign-in (Google or email) before handoff is accepted; dashboard notifications show the customer name when available.

Header: x-api-key: YOUR_API_KEY
PUT/api/messaging/v1/handoff

Per merchant (msg_…): set the same SaaS endpoint for all stores; route by profileId in the JSON body.

{
  "handoffWebhookUrl": "https://your-saas.com/api/gellhub/handoff",
  "handoffWebhookSecret": "your-hmac-secret"
}
Header: x-api-key: YOUR_API_KEY
GET/api/messaging/v1/handoff

Read handoff webhook URL for this merchant.

GellHub POSTs to your URL:

{
  "version": 1,
  "event": "handoff.requested",
  "profileId": "clx…",
  "conversationRef": "web:emb_…:visitor-uuid",
  "conversationId": "…",
  "visitorId": "visitor-uuid",
  "agentId": "…",
  "channel": "web",
  "message": "I need to speak with someone on your team",
  "requestedAt": "2026-05-25T12:00:00.000Z"
}

Headers: X-GellHub-Timestamp, X-GellHub-Event = handoff.requested, optional X-GellHub-Signature (HMAC-SHA256 of timestamp + "." + body).

Routing many stores: keep a table your_store_id ↔ profileId when the merchant completes Embedded connect. On webhook, lookup store and push to that tenant's team only.

Iframe embed: the widget also sends postMessage with type: "gellhub:handoff" to window.parent for instant UI badges (webhook remains the source of truth).

Respond with HTTP 200 and optional JSON { "ok": true }.

Conversations API (staff inbox, x-api-key)

Header: x-api-key: YOUR_API_KEY
GET/api/messaging/v1/conversations?handoffOnly=1

List threads. handoffOnly=1 returns conversations waiting for a human (aiPaused).

Header: x-api-key: YOUR_API_KEY
GET/api/messaging/v1/conversations/{id}

Full message history. Opening a handoff thread keeps AI paused.

Header: x-api-key: YOUR_API_KEY
POST/api/messaging/v1/conversations/{id}/reply

Staff reply (role staff). Does not re-enable AI.

{
  "content": "Thanks for waiting — a colleague will help you now."
}
Header: x-api-key: YOUR_API_KEY
PATCH/api/messaging/v1/conversations/{id}

Resume AI bot for this thread when support is finished.

{
  "reenableAi": true
}

While aiPaused is true, POST …/chat and the embed chat return a short hold message (0 credits) — no LLM or dynamic skills.

Billing & Stripe

Subscribe and pay with the same x-api-key you use for API calls (Client Gallery key or Drive key). The platform maps the key to your account and starts a Stripe Checkout session. You can also use a browser session after signing in at the dashboard.

See Plans & pricing API for GET /api/public/plans and GET /api/public/plans/by-id. Use planId on POST /api/register or POST /api/billing/checkout-session with x-api-key.

Header: x-api-key: YOUR_API_KEY
GET/api/billing/status

Current plan and Stripe subscription snapshot (gallery, drive, or messaging depending on the x-api-key)

Header: x-api-key: YOUR_API_KEY
POST/api/billing/checkout-session

Returns { url } — open in a browser to complete payment. Optional successUrl/cancelUrl for server-to-browser flows.

{
  "planId": "plan_xxx",
  "successUrl": "https://yourapp.com/done?session_id={CHECKOUT_SESSION_ID}",
  "cancelUrl": "https://yourapp.com/cancel"
}
Header: x-api-key: YOUR_API_KEY
POST/api/billing/portal

Stripe Customer Portal (payment methods, cancel plan, invoices). Requires an existing Stripe customer (after first checkout).

{
  "returnUrl": "https://yourapp.com/account"
}
Header: x-api-key: YOUR_API_KEY
GET/api/billing/invoices

Lists invoices and card charges for the customer behind this API key

Configure STRIPE_SECRET_KEY and webhook endpoint POST /api/webhooks/stripe with STRIPE_WEBHOOK_SECRET so subscriptions sync to your plan limits after payment.

Examples

cURL - List Drive files

curl -H "x-api-key: YOUR_DRIVE_API_KEY" \
  "https://gellhub.com/api/drive"

cURL - Upload to Drive (small files only)

curl -X POST "https://gellhub.com/api/drive/upload" \
  -H "x-api-key: YOUR_DRIVE_API_KEY" \
  -F "folderId=FOLDER_ID" \
  -F "file=@/path/to/image.jpg"

For external integrations, large files, or batch uploads, use presigned uploads below instead of proxy upload.

JavaScript - Drive presigned upload (recommended)

const apiKey = 'YOUR_DRIVE_API_KEY';
const file = fileInput.files[0];
const folderId = 'optional_folder_id_or_null';

// 1) Ask gellhub for presigned PUT URL
const presignRes = await fetch('https://gellhub.com/api/drive/upload/presign', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': apiKey,
  },
  body: JSON.stringify({
    fileName: file.name,
    contentType: file.type || 'application/octet-stream',
    sizeBytes: file.size,
    folderId: folderId || null,
  }),
});
if (!presignRes.ok) throw new Error(await presignRes.text());
const { uploadUrl, storageKey, headers } = await presignRes.json();

// 2) Upload directly to object storage (Wasabi)
const putRes = await fetch(uploadUrl, {
  method: 'PUT',
  headers: { 'Content-Type': headers?.['Content-Type'] || file.type || 'application/octet-stream' },
  body: file,
});
if (!putRes.ok) throw new Error('PUT failed: ' + putRes.status);

// 3) Finalize on gellhub
// If storage is not immediately visible, retry /complete a few times.
for (let attempt = 1; attempt <= 5; attempt++) {
  const completeRes = await fetch('https://gellhub.com/api/drive/upload/complete', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': apiKey,
    },
    body: JSON.stringify({
      storageKey,
      fileName: file.name,
      contentType: file.type || 'application/octet-stream',
      sizeBytes: file.size,
      folderId: folderId || null,
    }),
  });

  if (completeRes.ok) {
    const fileRecord = await completeRes.json();
    console.log('Uploaded:', fileRecord);
    break;
  }

  const err = await completeRes.json().catch(() => ({}));
  const retryable = err?.error === 'upload_incomplete_or_mismatch' &&
    err?.detail === 'object_not_visible_yet_retry_complete';
  if (!retryable || attempt === 5) throw new Error(JSON.stringify(err));

  await new Promise((r) => setTimeout(r, attempt * 1000));
}

JavaScript - Create collection (Client Gallery)

const res = await fetch('https://gellhub.com/api/collections', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'YOUR_CLIENT_GALLERY_API_KEY'
  },
  body: JSON.stringify({
    name: 'Wedding Album',
    clientName: 'Jane Smith',
    date: '2024-06-15',
    categoryId: 'cat_xxx'
  })
});
const collection = await res.json();

JavaScript - Upload to collection

const formData = new FormData();
formData.append('collectionId', collection.id);
formData.append('clientName', 'Jane Smith');
formData.append('file', fileInput.files[0]);

await fetch('https://gellhub.com/api/upload', {
  method: 'POST',
  headers: { 'x-api-key': 'YOUR_CLIENT_GALLERY_API_KEY' },
  body: formData
});

Ready to build?

Create an account and get your API credentials

Get Started