# Quickstart (/docs/api/getting-started/quickstart)



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.

<Callout type="warn">
  **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`](/docs/api/reference/credits/get-credits)
  and top up from the dashboard if you're close to zero. See [Sandbox & test
  keys](/docs/api/getting-started/sandbox) for the full isolation story.
</Callout>

## 1. Get an API key [#1-get-an-api-key]

Get a key from the Layers dashboard at [`https://app.layers.com/settings/api-keys`](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`](/docs/api/reference/api-keys/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`.

```bash
export LAYERS_API_KEY="lp_live_01HX9Y6K7EJ4T2AB_4QZpN..."
```

<Callout>
  The key's `env` segment is `live` or `test`. Both hit the same surface and share the same org wallet — see [Sandbox & test keys](/docs/api/getting-started/sandbox) for the real isolation story.
</Callout>

## 2. Confirm the key resolves [#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.

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl https://api.layers.com/v1/whoami \
      -H "X-Api-Key: $LAYERS_API_KEY"
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    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);
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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"])
    ```
  </Tab>
</Tabs>

A healthy response:

```json
{
  "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 [#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.

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    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"
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    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();
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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()
    ```
  </Tab>
</Tabs>

Response (201 Created, truncated to the fields you'll most often use):

```json
{
  "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 [#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](/docs/api/reference/github/install-url)), then register the resulting `installationId` + `state` against your Layers org.

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    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"
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    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",
      }),
    });
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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",
        },
    )
    ```
  </Tab>
</Tabs>

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 [#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](/docs/api/operational/pricing) for the full table.

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    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"]
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    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();
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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"]
    ```
  </Tab>
</Tabs>

Response:

```json
{
  "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 [#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.

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl "https://api.layers.com/v1/jobs/$JOB_ID" \
      -H "X-Api-Key: $LAYERS_API_KEY"
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    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));
      }
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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)
    ```
  </Tab>
</Tabs>

A running response carries `progress` and a `stage` string from the workflow's vocabulary (`cloning`, `analyzing`, `generating_sdk_patch`, `opening_pr`, `finalizing`):

```json
{
  "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:

```json
{
  "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 [#whats-next]

* [Authentication](/docs/api/getting-started/authentication) — rotation, kill switch, rate limits.
* [Common patterns](/docs/api/getting-started/common-patterns) — idempotency, pagination, error shape.
* [Jobs](/docs/api/concepts/jobs) — the envelope every async call uses.
* [Onboard a customer](/docs/api/guides/onboard-customer) — the full ingest → SDK → first content flow.
