AI
Every Feeef AI feature under /v1/ai — text, code, HTML landing pages, images, image landing pages, voice and studios. All of it runs on Feeef Cloud.
Every AI endpoint lives under /v1/ai/*. Feeef never calls an AI provider directly: each
request runs on Feeef Cloud, which owns models, prompts for
Cloud products, provider routing and the exact usage receipt. Feeef adds your store and product
data, keeps history, and charges your wallet from the receipt.
| Area | Endpoints | Result |
|---|---|---|
| Catalog | models, collections/{id} | Sync |
| Raw Cloud access | requests, streams | Async job / SSE |
| Text | text/* | Sync draft (nothing saved) |
| Code | code/* | Sync code or page structure |
| HTML landing pages | landing-pages | Async job → a ProductLandingPage |
| Images | images, images/sync, image-generations, image-prompt-templates | Async record, or sync |
| Image landing pages | image-landing-pages | Async → one long poster image |
| Voice | voiceovers, speech, voices, voice-projects | Sync audio |
| Studios | brand-assets, post-assets, ui-assets, *-studios | Async image records |
Image landing pages and HTML landing pages are different products. An image landing page
is a single tall image (a mobile ad poster). An HTML landing page is a builder page
(ProductLandingPage) with sections and settings.
Conventions
Auth and scopes
All routes need a Bearer token except the public reads (image-generations/gallery,
image-prompt-templates). Tokens need ai.read for GET and ai.generate for everything
else; first-party tokens (*) always pass. Otherwise the API answers:
// 403, WWW-Authenticate: Bearer error="insufficient_scope", scope="ai.generate"
{ "error": { "code": "insufficient_scope", "message": "Explicit ai.generate access is required" } }Store-scoped calls also check your member permissions on storeId.
Idempotency
Every paid request needs an Idempotency-Key header (1–200 printable ASCII characters; voice
calls use a UUID). Persist the key before sending and reuse it on retry:
| Situation | Response |
|---|---|
| First call | Runs and is billed once |
| Same key, same body, finished | The original status and body, replayed — no new charge |
| Same key, still running | 503 ai_workflow_pending — retry later with the same key |
| Same key, different body | 409 idempotency_conflict |
| A paid step is still reconciling | 503 ai_recovery_required — retry with the same key and body |
| Missing key | 400 idempotency_key_required |
A replay returns exactly what the first call returned, including success: false bodies: use a
new key to try again after a failure. The SDKs manage keys for you
(CloudAiWorkflowIntents in Dart, ff.withAiWorkflow(key, …) in JavaScript).
Async results
Long jobs return immediately and finish in the background:
- Image records (
images,image-generations,image-landing-pages, studio assets) create anImageGenerationwithstatuspending→processing→completed(orfailed, reason inerror). Follow it on the realtime channelsimage_generations/{id},users/{userId}/image_generationsorstores/{storeId}/image_generations(created,updated,deletedevents), or pollGET /v1/ai/image-generations/{id}. - HTML landing pages return a
jobId; pollGET /v1/ai/landing-pages/jobs/{jobId}. - Raw requests return a job; poll
GET /v1/ai/requests/{id}.
Billing
AI is postpaid. When Cloud returns the final receipt (charged_micros, in USD micros), Feeef
converts it once — micros / 1,000,000 × exchange rate × retail markup, rounded up to 0.001 DZD —
and records one ai_generation transfer from your wallet. There is no hold and no balance
pre-check; a wallet can go negative and must be topped up. Failed steps that already ran are
billed. Admission limits per user: 4 active jobs and 20 new jobs per minute, otherwise
429 feeef_ai_admission_limit (nothing started, nothing charged).
Responses and errors
- Text, code and sync image/voice endpoints answer 200 with
{ "success": true | false, "message", ... }. AI or permission failures aresuccess: falsewitherror. - Cloud errors keep their status:
{ "error": { "code", "message", "requestId" } }. - Uploads are limited to 20 MiB per request (
413 ai_body_too_large).
Catalog
| Method | Path | Description |
|---|---|---|
| GET | /v1/ai/models | Cloud models you can use, with pricing, request_constraints and feeef_image (image options, or null) |
| GET | /v1/ai/collections/{id} | Curated model ladder, cheapest first, e.g. image-landing-page. 404 collection_not_found when not configured |
Pass storeId to use the store's Cloud workspace; without it your personal workspace is used.
A collection rung is { rank, label, description, model } — show it as a quality slider.
Raw Cloud access
Direct access to any Cloud operation (chat/completions, responses, embeddings,
images/*, audio/*, videos, google/*, anthropic/messages) with Feeef billing.
| Method | Path | Description |
|---|---|---|
| POST | /v1/ai/requests | { endpoint, body, storeId? } → 202 job. Multipart files only for videos and `audio/transcriptions |
| GET | /v1/ai/requests | Your last 50 jobs; ?key= finds the job of an idempotency key |
| GET | /v1/ai/requests/{id} | Job with result once succeeded |
| GET | /v1/ai/requests/{id}/content | Binary result (audio, image, video) |
| POST | /v1/ai/streams | Same body with stream: true for chat/completions / responses; relays SSE |
body.model must come from /v1/ai/models. A job is
{ id, status, endpoint, cloudRequestId, imageGenerationId, billing: { currency: "DZD", chargedMicros, chargeDzd, settled }, error, result }.
Text
Natural language → a validated draft. Nothing is saved; your app applies the draft. All take
storeId, input, optional attachments (image, url, audio, product, store) and
modelId.
| Method | Path | Draft | Permission |
|---|---|---|---|
| POST | /v1/ai/text/product-draft | product (`mode: create | update, productId` for update) |
| POST | /v1/ai/text/shipping-price-draft | shippingPrice (shippingPriceId for update) | shipping_prices.write |
| POST | /v1/ai/text/order-draft | order (pass the current form as order) | orders.write |
| POST | /v1/ai/text/list-filter | filter — flat or filterator; send the client schema | any list read |
| POST | /v1/ai/text/date-preset | preset (date-range recipe); send the client schema | any list read |
| POST | /v1/ai/text/custom-fields-draft | fields + message — the complete next list (contract v2: every type, options, visibility rules); pass currentFields to edit, kept ids are preserved. Save it yourself with PATCH /v1/stores/{id}/integrations/customFields | integrations.write |
curl -X POST "https://api.feeef.org/v1/ai/text/product-draft" \
-H "Authorization: Bearer $FEEEF_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"storeId":"STORE_ID","input":"Black leather wallet, 2500 DZD, 3 colors"}'Code
Storefront code and builder structures. See Templates → AI for how results
plug into TemplateData.
| Method | Path | Produces |
|---|---|---|
| POST | /v1/ai/code/snippet | data: HTML, CSS, JavaScript or Markdown (language, `mode: create |
| POST | /v1/ai/code/custom-component | code + propsSchema + props (+ slots). Edit with current* fields |
| POST | /v1/ai/code/template-page-edit | Full page (page, globalPropsPatch) or a scoped proposal (pageId, mode, prompt/attachments) |
| POST | /v1/ai/code/landing-page-template-data | data.pages.landing_page + schema from a landing template (templateId, productId, prompt/attachments) |
Template code needs store_templates.write, template_components.write,
product_landing_pages.write or pages.write on the store.
HTML landing pages
| Method | Path | Description |
|---|---|---|
| POST | /v1/ai/landing-pages | { storeId, productId, name?, prompt?/attachments? } → { success, jobId } |
| GET | /v1/ai/landing-pages/jobs/{jobId} | status: pending, completed (with landingPageId) or failed (with error) |
The job writes the page copy and design, then creates the ProductLandingPage and sends a push
notification. Poll every ~10 seconds.
Images
| Method | Path | Description |
|---|---|---|
| POST | /v1/ai/images | Image studio: generate or edit with any Cloud image model. 202 ImageGeneration |
| POST | /v1/ai/images/sync | Generate or edit and wait; returns imageUrl |
| GET | /v1/ai/image-generations | Your records, or a store's with storeId; filter tags |
| POST | /v1/ai/image-generations | Prompt/template-driven generation (multipart). 200 record, pending |
| GET | /v1/ai/image-generations/{id} | One record (generatedImageUrl, configs, generations[] history, error) |
| PATCH | /v1/ai/image-generations/{id} | Rename, or `status: published |
| DELETE | /v1/ai/image-generations/{id} | Soft delete |
| POST | /v1/ai/image-generations/{id}/regenerate | Run again on the same record (latest run wins; 409 while running) |
| GET | /v1/ai/image-generations/gallery | Public published images |
| GET | /v1/ai/image-prompt-templates[/{id}] | Public prompt presets for the image studio |
| POST/PATCH/DELETE | /v1/ai/image-prompt-templates[/{id}] | Platform admins only |
POST /v1/ai/images (JSON or multipart) takes model (a Cloud image model), prompt,
options (from the model's feeef_image choices: aspect ratio, size, quality, background,
format), attachments (image URLs on Feeef storage, store, product), optional imageFile
and maskFile (PNG with alpha), id to append to an existing record, and storeId.
POST /v1/ai/image-generations takes prompt, storeId, model, aspectRatio,
imageSize (1K/2K/4K), resolution, attachments (image, store, product, audio),
imageFile, background, quality, outputFormat, googleSearch, imageSearch.
final gen = await ff.ai.generateImage(/* model, prompt, options … */);
// then listen on users/{userId}/image_generations, or:
final record = await ff.imageGenerations.find(gen['id']);Image landing pages
One tall product poster image (default 9:32), built by Cloud in two steps: a text model writes
the copy deck from your product data and photos, then an image model renders it. The result is an
ImageGeneration tagged image_landing_page.
| Method | Path | Description |
|---|---|---|
| POST | /v1/ai/image-landing-pages | Start. { success, id }; follow the image record |
| POST | /v1/ai/image-landing-pages/questions | Optional wizard questions (billed, sync) |
| GET | /v1/ai/image-landing-pages/templates | Platform templates + the store's own (?storeId=) |
| POST | /v1/ai/image-landing-pages/templates | Create a store template (?storeId= required) |
| PATCH | /v1/ai/image-landing-pages/templates/{id} | Update a store template |
| DELETE | /v1/ai/image-landing-pages/templates/{id} | Delete a store template |
| GET | /v1/ai/collections/image-landing-page | Image model ladder for the quality slider |
Start — text (the brief), attachments (image, product, store), storeId (its Cloud
workspace owns templates and the job), templateId, answers (keyed by question key),
imageModel (a ladder rung), aspectRatio, imageSize, background, sectionsCount (2–12),
loadProductImage (default true; send false when you pass the photos yourself).
Templates are visual direction plus optional questions:
{
"name": "Gold Ramadan",
"description": "Matte gold, white marble, soft light",
"prompt": "Palette: matte gold on white marble. Headline: {{headline}}.",
"preview_image_url": "https://storage.feeef.net/…/preview.jpg",
"fields": [
{ "key": "headline", "type": "text", "label": "Headline" },
{ "key": "mood", "type": "select", "label": "Mood", "options": ["Calm", "Festive", "Luxury"] }
]
}Field types: text, textarea, select, multiselect, toggle, color, number. All
answers are optional; {{key}} in the prompt takes the answer and other answers are passed as
labelled notes. Platform templates are read-only.
Questions — { storeId?, text?, attachments?, templateId?, locale: ar|fr|en } with an
Idempotency-Key → { summary, templateFields, fields, billing: { chargeDzd } }.
final ladder = await ff.ai.collection('image-landing-page', storeId: storeId);
final templates = await ff.ai.imageLandingPages.templates(storeId: storeId);
final res = await ff.actions.generateImageLandingPage(
text: 'Ramadan offer, free delivery',
attachments: [ProductAttachment(value: productId, label: 'Wallet')],
storeId: storeId,
templateId: templates.first.id,
imageModel: ladder?.rungs.last.model.id,
);Voice
| Method | Path | Description |
|---|---|---|
| POST | /v1/ai/voiceovers | Script → speech in one call: optional script enhancement, style, two-speaker dialogue. Returns audioUrl |
| POST | /v1/ai/speech | Voice studio TTS from blocks (voice, text, style; ≤24 blocks, ≤2 voices). Returns audioUrl |
| POST | /v1/ai/speech/script-edit | Rewrite the blocks with a text model (modelId, prompt, blocks, voices) |
| GET | /v1/ai/speech/script-edit/quote | Text models available for script editing |
| GET | /v1/ai/speech/pending | Unfinished paid voice jobs (with retryKey) |
| POST | /v1/ai/speech/retry | Resume a pending job by id |
| GET | /v1/ai/voices | Voice library: built-in, platform and your custom voices (search, pageToken) |
| POST | /v1/ai/voices | Create a custom voice: prompted (designed) or replicated (cloned with consent) |
| POST | /v1/ai/voices/manage | Rename or delete custom voices (ids, operation) |
| GET/POST | /v1/ai/voice-projects | Saved speech drafts (list / create) |
| GET/PUT/DELETE | /v1/ai/voice-projects/{id} | One draft; PUT needs the current revision (409 revision_conflict when stale) |
| GET | /v1/ai/voice-projects/{id}/takes/{takeId}/audio | Audio of a generated take |
Voice calls are synchronous (up to ~170 s). If a call ends while the job is still running you get
503 with the job id — call again with the same Idempotency-Key (a UUID) or use
/speech/retry. Passing projectId to /speech records the take in that project.
curl -X POST "https://api.feeef.org/v1/ai/voiceovers" \
-H "Authorization: Bearer $FEEEF_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"storeId":"STORE_ID","text":"عرض خاص هذا الأسبوع — توصيل مجاني!","voiceName":"Fenrir","enhanceScript":true}'Studios
Brand, social-post and UI mockup studios. Asset calls return immediately with an
ImageGeneration id (follow it like any image record).
| Method | Path | Description |
|---|---|---|
| POST | /v1/ai/brand-assets | logoName, description, colors, assetKind (identity, logo, icon, avatar, graphic_elements, landing), brandStudioId or createStudio |
| POST | /v1/ai/post-assets | name, brief, assetKind (feed, story, square, wide), regionStyle, postStudioId or createStudio |
| POST | /v1/ai/ui-assets | name, brief, assetKind (web, mobile, dashboard, landing, settings), style, uiStudioId or createStudio |
| GET/POST | /v1/ai/{brand,post,ui}-studios | List / create studio projects |
| GET/PATCH/DELETE | /v1/ai/{brand,post,ui}-studios/{id} | One project, with primaryImages and generationCounts |
| GET | /v1/ai/{brand,post,ui}-studios/{id}/generations | Assets of one kind |
| PATCH | /v1/ai/{brand,post,ui}-studios/{id}/primaries | Pick the primary asset of a kind (kind, generationId) |
SDK map
| Area | Dart (ff) | JavaScript (ff) |
|---|---|---|
| Catalog | ff.ai.models(), ff.ai.collection(id) | ff.ai.models() |
| Raw Cloud | ff.ai.submit/submitFile/stream/request/content | ff.ai.submit/submitFile/stream/request |
| Text & code | ff.actions.updateProductUsingAi, generateSimpleCode, generateCustomComponentCode, … | — |
| Images | ff.ai.generateImage, ff.imageGenerations.*, ff.actions.editOrGenerateSimpleImage | ff.ai.generateImage, ff.imageGenerations.* |
| Image landing pages | ff.actions.generateImageLandingPage, ff.ai.imageLandingPages.* | — |
| Voice | ff.actions.generateVoiceover | — |
| Studios | ff.actions.generateBrandAsset/PostAsset/UiAsset, ff.brandStudios/postStudios/uiStudios | same |
The generated API reference lists every field of every route.