Quickstart
Authenticate, create a project, kick off your first ingest job. Ten minutes end to end.
This walks the first six calls every integration makes: confirm your key, create a project for one of your customers, register a GitHub installation, kick off an ingest job, and poll it to completion. Every step has curl, TypeScript, and Python.
Every key bills real credits. Layers does not offer a credits-free
sandbox — lp_test_* keys and lp_live_* keys share the same org wallet,
and every generate call debits real credits against real money. Before
running end-to-end tests, check your balance with GET /v1/credits
and top up from the dashboard if you're close to zero. See Sandbox & test
keys for the full isolation story.
1. Get an API key
Get a key from the Layers dashboard at https://app.layers.com/settings/api-keys — log in with a free account and click Create key. Keys are scoped to your org; you can kill any key instantly via POST /v1/api-keys/:keyId/kill if it leaks.
Every request carries X-Api-Key: lp_<env>_<key_id>_<secret>. Keep the key in a secret store, never in source. Authorization: Bearer <key> is accepted as a fallback for clients that can't set custom headers; the primary form is X-Api-Key.
export LAYERS_API_KEY="lp_live_01HX9Y6K7EJ4T2AB_4QZpN..."The key's env segment is live or test. Both hit the same surface and share the same org wallet — see Sandbox & test keys for the real isolation story.
2. Confirm the key resolves
Hit /v1/whoami. Cheapest call on the surface. It tells you what your key is bound to: workspace, organization, rate-limit tier, and whether the kill switch is on.
curl https://api.layers.com/v1/whoami \
-H "X-Api-Key: $LAYERS_API_KEY"const res = await fetch("https://api.layers.com/v1/whoami", {
headers: { "X-Api-Key": process.env.LAYERS_API_KEY! },
});
const me = await res.json();
console.log(me.organizationId, me.organizationName);import os, requests
res = requests.get(
"https://api.layers.com/v1/whoami",
headers={"X-Api-Key": os.environ["LAYERS_API_KEY"]},
)
me = res.json()
print(me["organizationId"], me["organizationName"])A healthy response:
{
"organizationId": "2481fa5c-a404-44ed-a561-565392499abc",
"workspaceId": "2481fa5c-a404-44ed-a561-565392499abc",
"organizationName": "Acme Growth",
"scopes": [],
"rateLimitTier": "standard",
"killSwitch": false,
"apiAccessRevoked": false,
"apiKeyId": "c2037bb9-354d-4662-96b7-97a28ad6b6e1",
"creditBalance": 6000
}If killSwitch or apiAccessRevoked is true, every other call returns 503 KILL_SWITCH. Stop and message your Layers contact.
3. Create a project
A project represents one of your end-customers. Pass customerExternalId so you can look the project up later by your own internal id.
curl -X POST https://api.layers.com/v1/projects \
-H "X-Api-Key: $LAYERS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Acme Mobile",
"customerExternalId": "acme_42",
"timezone": "America/Los_Angeles",
"primaryLanguage": "en"
}'import { randomUUID } from "node:crypto";
const res = await fetch("https://api.layers.com/v1/projects", {
method: "POST",
headers: {
"X-Api-Key": process.env.LAYERS_API_KEY!,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
},
body: JSON.stringify({
name: "Acme Mobile",
customerExternalId: "acme_42",
timezone: "America/Los_Angeles",
primaryLanguage: "en",
}),
});
const project = await res.json();import os, uuid, requests
res = requests.post(
"https://api.layers.com/v1/projects",
headers={
"X-Api-Key": os.environ["LAYERS_API_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"name": "Acme Mobile",
"customerExternalId": "acme_42",
"timezone": "America/Los_Angeles",
"primaryLanguage": "en",
},
)
project = res.json()Response (201 Created, truncated to the fields you'll most often use):
{
"id": "254a4ce1-f4ca-42b1-9e36-17ca45ef3d39",
"name": "Acme Mobile",
"customerExternalId": "acme_42",
"timezone": "America/Los_Angeles",
"primaryLanguage": "en",
"organizationId": "2481fa5c-a404-44ed-a561-565392499abc",
"status": "active",
"requiresApproval": false,
"firstNPostsBlocked": null,
"currentBlockedCount": 0,
"brand": null,
"metadata": {},
"createdAt": "2026-04-18T17:02:11.000Z",
"updatedAt": "2026-04-18T17:02:11.000Z"
}Save id — every project-scoped call uses it. IDs are UUIDs; don't assume a prefix.
4. Register a GitHub installation
The ingest job clones a repo through the Layers GitHub App. Install the app on the customer's GitHub org first (install URL flow), then register the resulting installationId + state against your Layers org.
curl -X POST https://api.layers.com/v1/github/installation \
-H "X-Api-Key: $LAYERS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"installationId": 56781234,
"state": "gh_state_from_install_url_callback"
}'await fetch("https://api.layers.com/v1/github/installation", {
method: "POST",
headers: {
"X-Api-Key": process.env.LAYERS_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
installationId: 56781234,
state: "gh_state_from_install_url_callback",
}),
});requests.post(
"https://api.layers.com/v1/github/installation",
headers={
"X-Api-Key": os.environ["LAYERS_API_KEY"],
"Content-Type": "application/json",
},
json={
"installationId": 56781234,
"state": "gh_state_from_install_url_callback",
},
)One installation per Layers org covers every repo it has access to. Layers issues short-lived installation tokens per operation; nothing is stored beyond the install id.
5. Kick off an ingest job
POST /v1/projects/:projectId/ingest/github clones the repo into an ephemeral sandbox, extracts brand context, and opens a PR that wires up the Layers SDK. It returns 202 with a job envelope — the work happens in the background.
Ingest spends 0 credits today. A downstream content generate call spends ~120 credits (video_remix or ugc_remix) or ~50 credits (slideshow_remix) — see Pricing for the full table.
curl -X POST "https://api.layers.com/v1/projects/$PROJECT_ID/ingest/github" \
-H "X-Api-Key: $LAYERS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"repoFullName": "acme/acme-mobile",
"branch": "main",
"openPR": true,
"platforms": ["ios"]
}'const res = await fetch(
`https://api.layers.com/v1/projects/${projectId}/ingest/github`,
{
method: "POST",
headers: {
"X-Api-Key": process.env.LAYERS_API_KEY!,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
},
body: JSON.stringify({
repoFullName: "acme/acme-mobile",
branch: "main",
openPR: true,
platforms: ["ios"],
}),
},
);
const { jobId, locationUrl } = await res.json();res = requests.post(
f"https://api.layers.com/v1/projects/{project_id}/ingest/github",
headers={
"X-Api-Key": os.environ["LAYERS_API_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"repoFullName": "acme/acme-mobile",
"branch": "main",
"openPR": True,
"platforms": ["ios"],
},
)
envelope = res.json()
job_id = envelope["jobId"]Response:
{
"jobId": "job_01HX9Y6K7EJ4T2ABCDEF01234",
"kind": "project_ingest_github",
"status": "running",
"stage": "initializing",
"projectId": "254a4ce1-f4ca-42b1-9e36-17ca45ef3d39",
"repoFullName": "acme/acme-mobile",
"locationUrl": "/v1/jobs/job_01HX9Y6K7EJ4T2ABCDEF01234",
"startedAt": "2026-04-18T17:03:02.000Z"
}6. Poll the job
Every long-running operation uses the same envelope at GET /v1/jobs/:jobId. Poll every five to thirty seconds until status is terminal (completed, failed, or canceled). Terminal states are sticky — once a job lands in one, it stays there.
curl "https://api.layers.com/v1/jobs/$JOB_ID" \
-H "X-Api-Key: $LAYERS_API_KEY"async function waitForJob(jobId: string) {
while (true) {
const res = await fetch(`https://api.layers.com/v1/jobs/${jobId}`, {
headers: { "X-Api-Key": process.env.LAYERS_API_KEY! },
});
const job = await res.json();
if (job.status === "completed") return job.result;
if (job.status === "failed" || job.status === "canceled") {
throw new Error(`Job ${job.status}: ${job.error?.message}`);
}
await new Promise((r) => setTimeout(r, 5000));
}
}import time
def wait_for_job(job_id: str):
while True:
res = requests.get(
f"https://api.layers.com/v1/jobs/{job_id}",
headers={"X-Api-Key": os.environ["LAYERS_API_KEY"]},
)
job = res.json()
if job["status"] == "completed":
return job["result"]
if job["status"] in ("failed", "canceled"):
raise RuntimeError(f"Job {job['status']}: {job.get('error', {}).get('message')}")
time.sleep(5)A running response carries progress and a stage string from the workflow's vocabulary (cloning, analyzing, generating_sdk_patch, opening_pr, finalizing):
{
"jobId": "job_01HX9Y6K7EJ4T2ABCDEF01234",
"kind": "project_ingest_github",
"status": "running",
"progress": 0.42,
"stage": "generating_sdk_patch",
"startedAt": "2026-04-18T17:03:02.000Z"
}A completed response carries the result:
{
"jobId": "job_01HX9Y6K7EJ4T2ABCDEF01234",
"kind": "project_ingest_github",
"status": "completed",
"finishedAt": "2026-04-18T17:09:14.000Z",
"result": {
"projectId": "254a4ce1-f4ca-42b1-9e36-17ca45ef3d39",
"repoFullName": "acme/acme-mobile",
"prUrl": "https://github.com/acme/acme-mobile/pull/247",
"prNumber": 247,
"brandContext": {
"appName": "Acme Mobile",
"tagline": "Faster checkouts, fewer taps.",
"audience": "iOS shoppers, 25–44, US/UK",
"brandVoice": "direct, dry, occasionally funny",
"keywords": ["one-tap checkout", "saved cards"]
},
"sdkAppId": "app_8ffb9410eb0eb848264f8a"
}
}That's it. Your customer now has a project on Layers, a brand context profile derived from their codebase, and an open PR that wires up the SDK.
What's next
- Authentication — rotation, kill switch, rate limits.
- Common patterns — idempotency, pagination, error shape.
- Jobs — the envelope every async call uses.
- Onboard a customer — the full ingest → SDK → first content flow.
Layers Partner API
One REST surface to ingest a customer, generate content, gate approval, distribute, and measure — all from inside your product.
Quickstart (no GitHub repo)
Onboard a customer when you can't give us the codebase — iOS/Android bundle ingest, Godot/Unity apps, anything where "just install the SDK" doesn't apply.