Zelko API
REST API reference for the Zelko platform.
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 include a Supabase JWT access token in the Authorization header. Server-side routes use the Supabase service role key to read and write data on behalf of the authenticated user.
POST /api/generate-image
Authorization: Bearer <supabase_access_token>
Content-Type: multipart/form-dataGet a token from the Supabase client:
import { supabase } from "@/lib/supabase";
const { data: { session } } = await supabase.auth.getSession();
const token = session?.access_token;Generate Image
/api/generate-imageTransforms a property photo using fal.ai Flux Pro Kontext. Accepts multipart/form-data with the source image and transformation parameters.
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| file | File | required | Source room or exterior photo (JPG/PNG, max 10 MB) |
| operation | string | required | staging | style_swap | exterior_redesign | sky_replacement | color_change | furniture_removal |
| orgId | string | required | Your organization UUID (found in Dashboard → Settings) |
| style | string | optional | Design style (Modern, Luxury, Farmhouse…) or color name for color_change |
| roomType | string | optional | living_room | bedroom | kitchen | bathroom | dining_room. Default: living_room |
| projectId | string | optional | Project UUID to associate the generated asset with |
| customPrompt | string | optional | Override the auto-generated prompt entirely |
| preserveStructure | boolean | optional | Append structure-preservation instructions to prompt. Default: true |
| maskFile | File | optional | White-on-black PNG mask for furniture_removal inpainting |
| referenceFile | File | optional | Reference style photo for style_swap / exterior_redesign. Upgrades model to kontext/max |
Response
{
"assetId": "a1b2c3d4-...",
"outputUrl": "https://fal.media/files/...",
"creditsUsed": 2
}Operations & credit costs
| Operation | Model | Credits | Notes |
|---|---|---|---|
| staging | flux-pro/kontext | 2 | Adds furniture to empty rooms |
| style_swap | flux-pro/kontext | 2 | Upgrades to kontext/max with referenceFile |
| exterior_redesign | flux-pro/kontext | 2 | Upgrades to kontext/max with referenceFile |
| color_change | flux-pro/kontext | 2 | Pass color name in style param |
| sky_replacement | flux-pro/kontext | 1 | No room type or structure preservation |
| furniture_removal | flux-pro/v1/fill | 2 | Pass white-on-black mask in maskFile |
Example — Virtual Staging
const formData = new FormData();
formData.append("file", roomPhoto);
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();Example — Style Swap with reference photo
formData.append("operation", "style_swap");
formData.append("referenceFile", referencePhoto);
// Automatically upgrades to fal-ai/flux-pro/kontext/maxGenerate Copy
/api/generate-copyGenerates 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)
| Parameter | Type | Required | Description |
|---|---|---|---|
| type | string | required | listing | social | taglines |
| orgId | string | required | Your organization UUID (found in Dashboard → Settings) |
| propertyDetails | object | optional | { address, bedrooms, bathrooms, sqft, type, notes } |
| imageUrl | string | optional | Property photo URL for vision-aware listing copy |
| assetId | string | optional | Existing asset to attach the copy to |
Response
// type: "listing"
{ "description": "Welcome to this stunning Modern masterpiece...", "compliance": { "compliance_check": "PASS" } }
// type: "social"
{ "instagram": "✨ Just listed!...", "facebook": "NEW LISTING...", "linkedin": "Excited to present..." }
// type: "taglines"
{ "taglines": ["Where Modern Luxury Meets Timeless Comfort", ...] }Example
const res = await fetch("/api/generate-copy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "listing",
orgId: "your-org-uuid",
propertyDetails: {
address: "123 Sunset Blvd, Los Angeles, CA",
bedrooms: "4", bathrooms: "3", sqft: "2400",
type: "Single Family",
notes: "Renovated kitchen 2024, rooftop deck, mountain views",
},
}),
});
const { description, compliance } = await res.json();Integrations API
Manage third-party integrations (Zapier, Follow Up Boss, HubSpot). Credentials are stored per-org and fired automatically when an asset completes.
Connect an integration
/api/integrations/[provider]| Parameter | Type | Required | Description |
|---|---|---|---|
| orgId | string | required | Organization UUID |
| webhook_url | string | optional | Zapier only — https://hooks.zapier.com/... |
| api_key | string | optional | Follow Up Boss only |
| access_token | string | optional | HubSpot only — Private App token |
// 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/..." }) });
// 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" }) });
// 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
/api/integrations/[provider]await fetch("/api/integrations/zapier", { method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ orgId: "your-org-uuid" }) });Check status
/api/integrations/[provider]?orgId=...{ "connected": true }Webhooks (Zapier)
When Zapier is connected, Zelko POSTs a payload to your webhook URL every time an image generation completes — use it to trigger CRM updates, Slack alerts, Google Sheets logging, etc.
Payload schema
{
"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
- 1Trigger: Webhooks by Zapier → Catch Hook
- 2Copy the webhook URL from Zapier
- 3Paste it into Dashboard → Integrations → Zapier → Connect
- 4Generate an image to send a test payload
- 5Add your Action step (HubSpot, Google Sheets, Slack, etc.)
Data Models
Asset
type Asset = {
id: string;
project_id: string | null;
org_id: string;
type: "image" | "video" | "text" | "flyer" | "social";
subtype: string | null;
input_url: string | null;
output_url: string | null;
generation_params: Record<string, unknown> | null;
model_used: string | null;
credits_cost: number;
status: "pending" | "processing" | "completed" | "failed";
created_at: string;
};Organization
type Organization = {
id: string;
name: string;
owner_id: string;
plan: "free" | "starter" | "professional" | "agency" | "enterprise";
credits_remaining: number;
stripe_customer_id: string | null;
created_at: string;
};Project
type Project = {
id: string;
org_id: string;
address: string;
property_type: "residential" | "commercial" | "land" | "multi-family" | null;
status: "active" | "archived";
thumbnail_url: string | null;
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.
-- Atomic credit deduction
select deduct_org_credits(p_org_id := 'org-uuid', p_amount := 2);
-- Add credits (Stripe webhook)
select add_org_credits(p_org_id := 'org-uuid', p_amount := 50);Credit costs
| Parameter | Type | Required | Description |
|---|---|---|---|
| Image transformation | 2 credits | optional | staging, style_swap, exterior_redesign, color_change, furniture_removal |
| Sky replacement | 1 credit | optional | sky_replacement |
| Video (10s) | 8 credits | optional | Coming Q2 2026 — Kling AI |
| Flyer PDF | 1 credit | optional | Coming Q2 2026 |
| Listing copy | FREE | optional | generate-copy endpoint — Claude Sonnet |
| Social captions | FREE | optional | generate-copy endpoint — Claude Haiku |
Error Codes
| Status | Error | Cause |
|---|---|---|
| 400 | Missing required fields: file, operation, orgId | Missing required form fields |
| 400 | Unknown operation: {op} | Invalid operation key |
| 401 | Invalid credentials | Integration API key/token failed validation |
| 402 | Insufficient credits | org.credits_remaining < creditCost |
| 500 | Failed to create asset record | Supabase insert failed |
| 500 | fal.ai returned no output image | fal.ai returned empty images array |
| 500 | Generation failed | Unexpected error — check server logs |