curl --request POST \
--url https://app.uselamina.ai/v1/apps/{appId}/runs \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"inputs": {
"Upload": "https://example.com/my-photo.jpg",
"Style": "Disney / Pixar",
"Aspect Ratio": "1:1"
}
}
'import requests
url = "https://app.uselamina.ai/v1/apps/{appId}/runs"
payload = { "inputs": {
"Upload": "https://example.com/my-photo.jpg",
"Style": "Disney / Pixar",
"Aspect Ratio": "1:1"
} }
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
inputs: {
Upload: 'https://example.com/my-photo.jpg',
Style: 'Disney / Pixar',
'Aspect Ratio': '1:1'
}
})
};
fetch('https://app.uselamina.ai/v1/apps/{appId}/runs', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.uselamina.ai/v1/apps/{appId}/runs",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'inputs' => [
'Upload' => 'https://example.com/my-photo.jpg',
'Style' => 'Disney / Pixar',
'Aspect Ratio' => '1:1'
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.uselamina.ai/v1/apps/{appId}/runs"
payload := strings.NewReader("{\n \"inputs\": {\n \"Upload\": \"https://example.com/my-photo.jpg\",\n \"Style\": \"Disney / Pixar\",\n \"Aspect Ratio\": \"1:1\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.uselamina.ai/v1/apps/{appId}/runs")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"inputs\": {\n \"Upload\": \"https://example.com/my-photo.jpg\",\n \"Style\": \"Disney / Pixar\",\n \"Aspect Ratio\": \"1:1\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.uselamina.ai/v1/apps/{appId}/runs")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"inputs\": {\n \"Upload\": \"https://example.com/my-photo.jpg\",\n \"Style\": \"Disney / Pixar\",\n \"Aspect Ratio\": \"1:1\"\n }\n}"
response = http.request(request)
puts response.read_body{
"data": {
"runId": "fc32ae7d-6840-4be3-8fb1-539a60e33fc3",
"workflowId": "80d4d454-8844-489f-b903-2ad65a414482",
"workflowName": "test sync",
"status": "queued",
"webhookUrl": "https://your-server.com/lamina-callback",
"outputs": [
{
"id": "aiDesignerNode-1773051929782-5yzqjkwpm",
"label": "AI Designer",
"type": "pending",
"value": null,
"status": "pending",
"error": null
},
{
"id": "aiDesignerNode-1773132653203-v9hn959pe",
"label": "AI Designer",
"type": "pending",
"value": null,
"status": "pending",
"error": null
}
]
}
}{
"error": "Invalid inputs",
"details": [
"\"Model Gender\": invalid option \"Other\". Must be one of: Male, Female",
"\"Location\": invalid option \"Space\". Must be one of: Studio, Urban, Park, Indoors, Beach"
]
}{
"error": "Invalid API key"
}{
"error": "App not found"
}"Too many requests from this IP, please try again in a minute"Run App
Start an asynchronous app execution. Returns immediately with an execution ID and pre-created output placeholders.
Providing inputs:
- Use the
inputsobject with parameter names as keys (from the Get App response) - For
optionsparameters, send the option label (e.g."Caucasian", not an internal value) - For
urlparameters, send a publicly accessible URL - For
textparameters, send a string value - Omit optional parameters to use their defaults
After starting:
You can poll GET /v1/runs/{runId} every 3-5 seconds, use a webhook
to receive results automatically, stream via GET /v1/runs/{runId}/stream,
or combine approaches — they all work side by side.
Webhook (optional, recommended for agents):
Pass ?webhook=https://your-server.com/callback as a query parameter. When the execution
completes, we POST the results to your URL — same structure as the polling response, signed
with ED25519 for verification. The polling endpoint still works regardless, so you can use
it as a fallback or for safety checks. See the Webhook Verification endpoint for details.
curl --request POST \
--url https://app.uselamina.ai/v1/apps/{appId}/runs \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"inputs": {
"Upload": "https://example.com/my-photo.jpg",
"Style": "Disney / Pixar",
"Aspect Ratio": "1:1"
}
}
'import requests
url = "https://app.uselamina.ai/v1/apps/{appId}/runs"
payload = { "inputs": {
"Upload": "https://example.com/my-photo.jpg",
"Style": "Disney / Pixar",
"Aspect Ratio": "1:1"
} }
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
inputs: {
Upload: 'https://example.com/my-photo.jpg',
Style: 'Disney / Pixar',
'Aspect Ratio': '1:1'
}
})
};
fetch('https://app.uselamina.ai/v1/apps/{appId}/runs', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.uselamina.ai/v1/apps/{appId}/runs",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'inputs' => [
'Upload' => 'https://example.com/my-photo.jpg',
'Style' => 'Disney / Pixar',
'Aspect Ratio' => '1:1'
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.uselamina.ai/v1/apps/{appId}/runs"
payload := strings.NewReader("{\n \"inputs\": {\n \"Upload\": \"https://example.com/my-photo.jpg\",\n \"Style\": \"Disney / Pixar\",\n \"Aspect Ratio\": \"1:1\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.uselamina.ai/v1/apps/{appId}/runs")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"inputs\": {\n \"Upload\": \"https://example.com/my-photo.jpg\",\n \"Style\": \"Disney / Pixar\",\n \"Aspect Ratio\": \"1:1\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.uselamina.ai/v1/apps/{appId}/runs")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"inputs\": {\n \"Upload\": \"https://example.com/my-photo.jpg\",\n \"Style\": \"Disney / Pixar\",\n \"Aspect Ratio\": \"1:1\"\n }\n}"
response = http.request(request)
puts response.read_body{
"data": {
"runId": "fc32ae7d-6840-4be3-8fb1-539a60e33fc3",
"workflowId": "80d4d454-8844-489f-b903-2ad65a414482",
"workflowName": "test sync",
"status": "queued",
"webhookUrl": "https://your-server.com/lamina-callback",
"outputs": [
{
"id": "aiDesignerNode-1773051929782-5yzqjkwpm",
"label": "AI Designer",
"type": "pending",
"value": null,
"status": "pending",
"error": null
},
{
"id": "aiDesignerNode-1773132653203-v9hn959pe",
"label": "AI Designer",
"type": "pending",
"value": null,
"status": "pending",
"error": null
}
]
}
}{
"error": "Invalid inputs",
"details": [
"\"Model Gender\": invalid option \"Other\". Must be one of: Male, Female",
"\"Location\": invalid option \"Space\". Must be one of: Studio, Urban, Park, Indoors, Beach"
]
}{
"error": "Invalid API key"
}{
"error": "App not found"
}"Too many requests from this IP, please try again in a minute"POST /v1/content/create instead.
Key each field in inputs by the parameter’s key (stable snake_case identifier) or name from GET /v1/apps/{appId}. Prefer key when present — it never changes, while name is a display label and matches case-sensitively.
On-brand runs
SetapplyBrand: true to make the output on-brand without hand-writing brand into every input. Lamina folds the workspace’s brand negatives and visual style onto the app’s visual generation nodes before the run. Anything you pass in inputs still wins — brand only fills fields you left unset, and never touches a field the app exposes as a parameter. Pass brandProfileId to target a specific brand in a multi-brand workspace; omit it for the workspace’s active brand.
Sandbox / test mode
Build and CI your integration without spending credits. Send the headerX-Lamina-Test: true (or "test": true in the body) on a run or on POST /v1/workflows/generate. Lamina validates the request exactly as it would for real — bad inputs still return 400 — then returns a deterministic stub (status: "test", a test_… id) without dispatching an execution, calling any model, or charging credits. Point your tests at it to exercise your request-building and response-handling for free, then drop the header to go live.
Getting results
| Method | Best for |
|---|---|
Wait — GET /v1/runs/{id}/wait | Agents. Blocks until done, no polling loop. |
Webhook — pass ?webhook=<url> | Production. Results POST to your URL on completion. |
SSE — GET /v1/runs/{id}/stream | Real-time UIs. Server-Sent Events with per-node progress. |
Poll — GET /v1/runs/{id} every 3-5s | Fallback when none of the above fit. |
Authorizations
Workspace API key. Prefix: lma_. Example: lma_abc123...
Path Parameters
The app ID (UUID) from the List Apps response
Query Parameters
URL to receive execution results when complete. We POST the same payload as the polling endpoint, signed with ED25519. Must be https or http.
Body
Parameter values keyed by parameter name (from the Get App response). Keys are case-sensitive and must match exactly.
{
"Upload": "https://example.com/my-photo.jpg",
"Style": "Disney / Pixar",
"Aspect Ratio": "1:1"
}
Advanced: per-node input overrides keyed by node id, set on specific workflow nodes directly (bypassing parameter-name mapping). Most callers use inputs.
Fold the workspace's brand (negatives + visual style) onto the app's visual generation nodes so output is on-brand, without hand-writing brand into inputs. Layered below your inputs, which always win.
Target a specific brand profile when applyBrand is set. Omit to use the workspace active brand.
Sandbox mode (or send header X-Lamina-Test: true): validate inputs and return a deterministic stub run -- no execution is dispatched and no credits are charged.
Response
Run started. Poll GET /v1/runs/{runId} or stream via SSE for results.
Returned when execution starts. All output values are null initially.
Show child attributes
Show child attributes