# POST /v1/projects/:projectId/sdk-apps (/docs/api/reference/sdk-apps/create-sdk-app)



<Endpoint method="POST" path="/v1/projects/:projectId/sdk-apps" auth="Bearer" scope="projects:write" phase="1" />

Creates an SDK app and creates its ingest API key. An SDK app is the binding between a platform build (iOS bundle, Android package, web domain) and the Layers event pipeline — once installed, the client SDK sends events to `in.layers.com/l/events` authenticated with the key returned here.

<Callout type="error">
  The API key in the response is **shown once**. Store it immediately — Layers does not display it again. Rotation is available via [`PATCH /v1/projects/:projectId/sdk-apps/:appId`](/docs/api/reference/sdk-apps/patch-sdk-app).
</Callout>

<Callout type="warn">
  When the GitHub ingest flow [`POST /v1/projects/:id/ingest/github`](/docs/api/reference/github/ingest-github) completes, it already provisioned an SDK app for you — use this endpoint only when the user is installing the SDK by hand, via a second platform, or when you need a new key.
</Callout>

<Parameters
  title="Path"
  rows="[
  { name: 'projectId', type: 'string', required: true, description: 'Project ID.' },
]"
/>

<Parameters
  title="Headers"
  rows="[
  { name: 'Idempotency-Key', type: 'string (UUID)', description: 'Replays within 24h.' },
]"
/>

<Parameters
  title="Body"
  rows="[
  { name: 'appId', type: 'string', description: 'Partner-created SDK app ID. If omitted, the server generates one.' },
  { name: 'name', type: 'string', required: true, description: 'Human-readable app name, 1–128 chars.' },
  { name: 'platform', type: 'string', required: true, description: 'Target platform.', enum: ['ios', 'android', 'web', 'react-native', 'flutter', 'expo'] },
  { name: 'bundleId', type: 'string', description: 'iOS bundle identifier, required when platform is ios.' },
  { name: 'androidPackage', type: 'string', description: 'Android package name, required when platform is android.' },
  { name: 'webDomain', type: 'string', description: 'Apex domain for web installs, required when platform is web.' },
]"
/>

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl https://api.layers.com/v1/projects/9cb958b5-11b5-4e30-8675-5d075d52da7c/sdk-apps \
      -H "Authorization: Bearer lp_live_01HX9Y6K7EJ4T2_4QZpN..." \
      -H "Idempotency-Key: 2f0e1c88-4b1d-4ac1-bc0a-5e9f6d8a7b10" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Acme Coffee iOS",
        "platform": "ios",
        "bundleId": "com.acmecoffee.ios"
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    const app = await layers.sdkApps.create(
      "9cb958b5-11b5-4e30-8675-5d075d52da7c",
      {
        name: "Acme Coffee iOS",
        platform: "ios",
        bundleId: "com.acmecoffee.ios",
      },
      { idempotencyKey: crypto.randomUUID() }
    );
    // Store app.apiKey now — it won't be shown again.
    ```
  </Tab>

  <Tab value="Python">
    ```python
    app = layers.sdk_apps.create(
        project_id="9cb958b5-11b5-4e30-8675-5d075d52da7c",
        name="Acme Coffee iOS",
        platform="ios",
        bundle_id="com.acmecoffee.ios",
        idempotency_key=str(uuid.uuid4()),
    )
    # Store app["apiKey"] now — it won't be shown again.
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="201" description="Created">
  ```json
  {
    "appId": "app_8ffb9410eb0eb848264f8a65",
    "name": "Acme Coffee iOS",
    "platform": "ios",
    "bundleId": "com.acmecoffee.ios",
    "androidPackage": null,
    "webDomain": null,
    "ingestEndpoint": "https://in.layers.com/l/events",
    "capi": {},
    "createdAt": "2026-04-18T19:25:22Z",
    "lastRotatedAt": "2026-04-18T19:25:22Z",
    "lastEventAt": null,
    "apiKey": "sk_app_8eRq...k2P"
  }
  ```

  `capi` is an empty object until you enable per-platform forwarding via `PATCH`. `lastRotatedAt` is set to `createdAt` on first creation.
</Response>

<Callout type="warn">
  `apiKey` is returned only on creation and rotation. If you lose it, rotate via `PATCH` to create a new one — the previous key is invalidated immediately.
</Callout>

## Errors [#errors]

| Status | Code              | When                                                                                                                                 |
| ------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| 422    | `VALIDATION`      | `:projectId` is not a UUID, `platform` unknown, or missing platform-specific identifier (`bundleId`, `androidPackage`, `webDomain`). |
| 401    | `UNAUTHENTICATED` | Missing or invalid key.                                                                                                              |
| 403    | `FORBIDDEN_SCOPE` | Key lacks `projects:write`.                                                                                                          |
| 404    | `NOT_FOUND`       | Project does not exist in the key's organization.                                                                                    |
| 409    | `CONFLICT`        | SDK app with the supplied `appId` or `(platform, bundleId/androidPackage/webDomain)` already exists.                                 |
| 429    | `RATE_LIMITED`    | Write budget exhausted.                                                                                                              |

## See also [#see-also]

* [`GET /v1/projects/:projectId/sdk-apps/:appId/install-spec`](/docs/api/reference/sdk-apps/install-spec) — deterministic install snippet
* [`PATCH /v1/projects/:projectId/sdk-apps/:appId`](/docs/api/reference/sdk-apps/patch-sdk-app) — rotate key, update CAPI
* [SDK events](/docs/api/reference/telemetry/events) — read what the SDK sends
