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 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.
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;Demo mode: Pass orgId=demo to bypass auth for local testing. Credits are not deducted and Supabase writes are skipped.
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 | Organization UUID. Pass 'demo' for unauthenticated testing. |
| 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-...", // null in demo mode
"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); // 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 imageExample — Style Swap with reference photo
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
/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 | Organization UUID or 'demo' |
| propertyDetails | object | optional | { address, bedrooms, bathrooms, sqft, type, notes } — used for listing and taglines |
| imageUrl | string | optional | Property photo URL for vision-aware listing copy (Claude sees the image) |
| assetId | string | optional | Existing asset to attach the copy to |
Response
// 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
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
/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 |
// 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
/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 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
{
"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. Create a new Zap → Trigger: Webhooks by Zapier → Catch Hook
- 2. Copy the webhook URL (e.g.
https://hooks.zapier.com/hooks/catch/123456/abcdef/) - 3. Paste it into Dashboard → Integrations → Zapier → Connect
- 4. Generate an image to send a test payload to Zapier
- 5. Add your Action step (HubSpot, Google Sheets, Slack, etc.)
Data Models
Asset
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
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
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
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.
-- 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/refundsCredit costs by operation
| 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 |