Zelko API

REST API reference for the Zelko platform.

Base URL
https://yourdomain.com/api
Auth
Supabase JWT (Bearer token)
Content-Type
multipart/form-data or application/json
Response
application/json

The Zelko API gives you programmatic access to AI image transformation, listing copy generation, credit management, and third-party integrations. All endpoints are Next.js App Router route handlers and run on Vercel Edge.

Authentication

All API calls from the mobile app include a Supabase JWT access token in the Authorization header. Server-side API routes use the Supabase service role key (bypasses RLS) to read and write data on behalf of the authenticated user.

http
POST /api/generate-image
Authorization: Bearer <supabase_access_token>
Content-Type: multipart/form-data

Get a token from the Supabase client:

typescript
import { supabase } from "@/lib/supabase";

const { data: { session } } = await supabase.auth.getSession();
const token = session?.access_token;

Demo mode: Pass orgId=demo to bypass auth for local testing. Credits are not deducted and Supabase writes are skipped.

Generate Image

POST/api/generate-image

Transforms a property photo using fal.ai Flux Pro Kontext. Accepts multipart/form-data with the source image and transformation parameters.

Request parameters

ParameterTypeRequiredDescription
fileFilerequiredSource room or exterior photo (JPG/PNG, max 10 MB)
operationstringrequiredstaging | style_swap | exterior_redesign | sky_replacement | color_change | furniture_removal
orgIdstringrequiredOrganization UUID. Pass 'demo' for unauthenticated testing.
stylestringoptionalDesign style (Modern, Luxury, Farmhouse…) or color name for color_change
roomTypestringoptionalliving_room | bedroom | kitchen | bathroom | dining_room. Default: living_room
projectIdstringoptionalProject UUID to associate the generated asset with
customPromptstringoptionalOverride the auto-generated prompt entirely
preserveStructurebooleanoptionalAppend structure-preservation instructions to prompt. Default: true
maskFileFileoptionalWhite-on-black PNG mask for furniture_removal inpainting
referenceFileFileoptionalReference style photo for style_swap / exterior_redesign. Upgrades model to kontext/max

Response

json
{
  "assetId": "a1b2c3d4-...",     // null in demo mode
  "outputUrl": "https://fal.media/files/...",
  "creditsUsed": 2
}

Operations & credit costs

OperationModelCreditsNotes
stagingflux-pro/kontext2Adds furniture to empty rooms
style_swapflux-pro/kontext2Upgrades to kontext/max with referenceFile
exterior_redesignflux-pro/kontext2Upgrades to kontext/max with referenceFile
color_changeflux-pro/kontext2Pass color name in style param
sky_replacementflux-pro/kontext1No room type or structure preservation
furniture_removalflux-pro/v1/fill2Pass white-on-black mask in maskFile

Example — Virtual Staging

typescript
const formData = new FormData();
formData.append("file", roomPhoto);           // File object
formData.append("operation", "staging");
formData.append("style", "Modern");
formData.append("roomType", "living_room");
formData.append("orgId", session.user.org_id);
formData.append("preserveStructure", "true");

const res = await fetch("/api/generate-image", {
  method: "POST",
  headers: { Authorization: `Bearer ${session.access_token}` },
  body: formData,
});

const { outputUrl, creditsUsed } = await res.json();
// outputUrl → CDN URL of the staged room image

Example — Style Swap with reference photo

typescript
formData.append("operation", "style_swap");
formData.append("referenceFile", referencePhoto);  // upgrades to kontext/max
// Model automatically switches to fal-ai/flux-pro/kontext/max
// Prompt: "Transform room in first image to match style in second image"

Generate Copy

POST/api/generate-copy

Generates MLS listing descriptions, social captions, and taglines using Claude. Runs a Fair Housing compliance check on all output. Free on all plans (0 credits).

Request body (JSON)

ParameterTypeRequiredDescription
typestringrequiredlisting | social | taglines
orgIdstringrequiredOrganization UUID or 'demo'
propertyDetailsobjectoptional{ address, bedrooms, bathrooms, sqft, type, notes } — used for listing and taglines
imageUrlstringoptionalProperty photo URL for vision-aware listing copy (Claude sees the image)
assetIdstringoptionalExisting asset to attach the copy to

Response

json
// type: "listing"
{
  "description": "Welcome to this stunning Modern masterpiece...",
  "compliance": {
    "compliance_check": "PASS",
    "violations": [],
    "suggestions": []
  }
}

// type: "social"
{
  "instagram": "✨ Just listed! This beautifully staged...",
  "facebook": "NEW LISTING at 123 Sunset Blvd...",
  "linkedin": "Excited to present this exceptional property..."
}

// type: "taglines"
{
  "taglines": [
    "Where Modern Luxury Meets Timeless Comfort",
    "Your Dream Home Awaits on Sunset Blvd",
    "Elevated Living in the Heart of the City"
  ]
}

Example

typescript
const res = await fetch("/api/generate-copy", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    type: "listing",
    orgId: "demo",
    propertyDetails: {
      address: "123 Sunset Blvd, Los Angeles, CA",
      bedrooms: "4",
      bathrooms: "3",
      sqft: "2400",
      type: "Single Family",
      notes: "Newly renovated kitchen, pool, mountain views",
    },
  }),
});

const { description, compliance } = await res.json();
// compliance.compliance_check === "PASS" | "FAIL"

Integrations API

Manage third-party integrations (Zapier, Follow Up Boss, HubSpot). Credentials are stored per-org in the org_integrations table and fired automatically when an asset completes.

Connect an integration

POST/api/integrations/[provider]
ParameterTypeRequiredDescription
orgIdstringrequiredOrganization UUID
webhook_urlstringoptionalZapier only — https://hooks.zapier.com/...
api_keystringoptionalFollow Up Boss only
access_tokenstringoptionalHubSpot only — Private App token
typescript
// Connect Zapier
await fetch("/api/integrations/zapier", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    orgId: "your-org-uuid",
    webhook_url: "https://hooks.zapier.com/hooks/catch/123456/abcdef/",
  }),
});

// Connect Follow Up Boss
await fetch("/api/integrations/fub", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ orgId: "your-org-uuid", api_key: "fub_xxxxxxxxxx" }),
});

// Connect HubSpot
await fetch("/api/integrations/hubspot", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ orgId: "your-org-uuid", access_token: "pat-na1-xxx" }),
});

Disconnect

DELETE/api/integrations/[provider]
typescript
await fetch("/api/integrations/zapier", {
  method: "DELETE",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ orgId: "your-org-uuid" }),
});

Check status

GET/api/integrations/[provider]?orgId=...
json
{ "connected": true }

Webhooks (Zapier)

When Zapier is connected, Zelko POSTs a JSON payload to your webhook URL every time an image generation completes successfully. Use this to trigger any downstream automation — CRM updates, Slack notifications, Google Sheets logging, email alerts, etc.

Payload schema

json
{
  "event":        "asset.completed",
  "asset_id":     "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "output_url":   "https://fal.media/files/generated-room.jpg",
  "operation":    "staging",
  "style":        "Modern",
  "room_type":    "living_room",
  "credits_used": 2,
  "org_id":       "org-uuid-here",
  "timestamp":    "2026-03-14T12:34:56.789Z"
}

Setup in Zapier

  1. 1. Create a new Zap → Trigger: Webhooks by ZapierCatch Hook
  2. 2. Copy the webhook URL (e.g. https://hooks.zapier.com/hooks/catch/123456/abcdef/)
  3. 3. Paste it into Dashboard → Integrations → Zapier → Connect
  4. 4. Generate an image to send a test payload to Zapier
  5. 5. Add your Action step (HubSpot, Google Sheets, Slack, etc.)

Data Models

Asset

typescript
type Asset = {
  id:                 string;          // UUID
  project_id:         string | null;   // FK → projects.id
  org_id:             string;          // FK → organizations.id
  type:               "image" | "video" | "text" | "flyer" | "social";
  subtype:            string | null;   // operation key, e.g. "staging"
  input_url:          string | null;   // original uploaded photo
  output_url:         string | null;   // generated result (fal.media CDN)
  generation_params:  Record<string, unknown> | null;
  model_used:         string | null;   // e.g. "fal-ai/flux-pro/kontext"
  credits_cost:       number;
  status:             "pending" | "processing" | "completed" | "failed";
  created_at:         string;          // ISO 8601
};

Organization

typescript
type Organization = {
  id:                 string;
  name:               string;
  owner_id:           string;          // FK → auth.users.id
  plan:               "free" | "starter" | "professional" | "agency" | "enterprise";
  credits_remaining:  number;
  stripe_customer_id: string | null;
  white_label_config: Record<string, unknown> | null;
  created_at:         string;
};

Project

typescript
type Project = {
  id:             string;
  org_id:         string;
  address:        string;
  city:           string | null;
  property_type:  "residential" | "commercial" | "land" | "multi-family" | null;
  status:         "active" | "archived";
  thumbnail_url:  string | null;
  created_at:     string;
  updated_at:     string;
};

OrgIntegration

typescript
type OrgIntegration = {
  id:         string;
  org_id:     string;
  provider:   "zapier" | "fub" | "hubspot";
  config:     {
    webhook_url?:   string;  // Zapier
    api_key?:       string;  // Follow Up Boss
    access_token?:  string;  // HubSpot
  };
  enabled:    boolean;
  created_at: string;
  updated_at: string;
};

Credit System

Credits are tracked per organization and deducted atomically using a PostgreSQL stored procedure to prevent race conditions from concurrent requests.

Free
10 lifetime
Starter
50/month
Professional
200/month
Agency
600/month
sql
-- Atomic credit deduction (prevents overdraft under concurrent load)
select deduct_org_credits(p_org_id := 'org-uuid', p_amount := 2);

-- Adds credits (Stripe webhook on purchase)
select add_org_credits(p_org_id := 'org-uuid', p_amount := 50);

-- Every deduction is logged in credits_ledger
-- amount is negative for usage, positive for purchases/refunds

Credit costs by operation

ParameterTypeRequiredDescription
Image transformation2 creditsoptionalstaging, style_swap, exterior_redesign, color_change, furniture_removal
Sky replacement1 creditoptionalsky_replacement
Video (10s)8 creditsoptionalComing Q2 2026 — Kling AI
Flyer PDF1 creditoptionalComing Q2 2026
Listing copyFREEoptionalgenerate-copy endpoint — Claude Sonnet
Social captionsFREEoptionalgenerate-copy endpoint — Claude Haiku

Error Codes

StatusErrorCause
400Missing required fields: file, operation, orgIdMissing required form fields
400Unknown operation: {op}Invalid operation key
401Invalid credentialsIntegration API key/token failed validation
402Insufficient creditsorg.credits_remaining < creditCost
500Failed to create asset recordSupabase insert failed
500fal.ai returned no output imagefal.ai returned empty images array
500Generation failedUnexpected error — check server logs