Layers
Partner APIGetting started

Quickstart

Authenticate, create a project, kick off your first ingest job. Ten minutes end to end.

View as Markdown

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

On this page