Developer Guide

API documentation for Client Gallery and Drive

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&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 or product=clientGallery, 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, accountKind, plan snapshot, and state. Wrong client or secret → 401; reused or expired code → 400.

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

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.apiKey / account.driveApiKey for this user

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)

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 or drive, depending on the 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