# POST /v1/projects/:id/ingest/github (/docs/api/reference/github/ingest-github)



<Endpoint method="POST" path="/v1/projects/:id/ingest/github" auth="Bearer" scope="ingest:write" phase="1" />

Runs the SDK-install workflow end-to-end: clones the repo into an isolated sandbox, detects the target platform (27+ supported — iOS, Android, React Native, Flutter, Expo, Next.js, etc.), generates a tailored patch that wires up the Layers SDK, and opens a PR on a new branch. The PR body includes the inferred brand context that Layers writes back to the project (`appName`, `tagline`, `audience`, `brandVoice`, `keywords`, and so on).

This is an async operation. The endpoint returns `202` with a `jobId`; poll [`GET /v1/jobs/:jobId`](/docs/api/reference/jobs/get-job) for stage transitions and the final result. Stages are: `cloning` → `analyzing` → `generating_sdk_patch` → `opening_pr` → `finalizing`. Total wall time is usually 90–240 seconds depending on repo size.

<Parameters
  title="Path"
  rows="[
  { name: 'id', type: 'string', required: true, description: 'Project ID to attach the resulting SDK app to.' },
]"
/>

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

<Parameters
  title="Body"
  rows="[
  { name: 'repoFullName', type: 'string', required: true, description: 'Repo identifier in owner/name form, e.g. acme-coffee/ios-app.' },
  { name: 'branch', type: 'string', description: 'Branch to fork from.', default: 'repository default branch' },
  { name: 'openPR', type: 'boolean', description: 'If false, the workflow stops after the patch is generated and does not open a PR.', default: 'true' },
  { name: 'platforms', type: 'string[]', description: 'Shorthand alias for `sdkConfig.includePlatforms` — restrict instrumentation to specific platforms.' },
  { name: 'sdkConfig', type: 'object', description: 'Overrides for SDK app name, platforms, and event allowlist. See below.' },
  { name: 'sdkConfig.appName', type: 'string', description: 'Overrides the inferred SDK app name.' },
  { name: 'sdkConfig.includePlatforms', type: 'string[]', description: 'Restrict instrumentation to specific platforms. One of ios, android, web, react-native, flutter, expo.' },
  { name: 'sdkConfig.eventAllowlist', type: 'string[]', description: 'If provided, only these event names are forwarded to CAPI.' },
  { name: 'prTitle', type: 'string', description: 'Override the PR title.', default: '&#x22;chore: instrument with Layers SDK&#x22;' },
  { name: 'prBody', type: 'string', description: 'Additional markdown appended to the PR body.' },
]"
/>

The body is strict — keys outside the list above are rejected with `422 VALIDATION`.

## 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/ingest/github \
      -H "Authorization: Bearer lp_live_01HX9Y6K7EJ4T2_4QZpN..." \
      -H "Idempotency-Key: 9b2e4d1c-3f2a-4d66-bf31-1a2b3c4d5e60" \
      -H "Content-Type: application/json" \
      -d '{
        "repoFullName": "acme-coffee/ios-app",
        "branch": "main",
        "sdkConfig": {
          "includePlatforms": ["ios"],
          "eventAllowlist": ["app_open", "purchase_success", "subscribe_start"]
        }
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    const { jobId } = await layers.ingest.github(
      "9cb958b5-11b5-4e30-8675-5d075d52da7c",
      {
        repoFullName: "acme-coffee/ios-app",
        branch: "main",
        sdkConfig: {
          includePlatforms: ["ios"],
          eventAllowlist: ["app_open", "purchase_success", "subscribe_start"],
        },
      },
      { idempotencyKey: crypto.randomUUID() }
    );

    const result = await layers.jobs.waitForCompletion(jobId);
    console.log(result.result.prUrl);
    ```
  </Tab>

  <Tab value="Python">
    ```python
    response = layers.ingest.github(
        project_id="9cb958b5-11b5-4e30-8675-5d075d52da7c",
        repo_full_name="acme-coffee/ios-app",
        branch="main",
        sdk_config={
            "includePlatforms": ["ios"],
            "eventAllowlist": ["app_open", "purchase_success", "subscribe_start"],
        },
        idempotency_key=str(uuid.uuid4()),
    )

    result = layers.jobs.wait_for_completion(response["jobId"])
    print(result["result"]["prUrl"])
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="202" description="Accepted — ingest started">
  ```json
  {
    "jobId": "01JS5V8KX5JFK1YQ4G2YZQ4XEP",
    "kind": "project_ingest_github",
    "status": "running",
    "stage": "cloning",
    "projectId": "9cb958b5-11b5-4e30-8675-5d075d52da7c",
    "repoFullName": "acme-coffee/ios-app",
    "locationUrl": "/v1/jobs/01JS5V8KX5JFK1YQ4G2YZQ4XEP",
    "startedAt": "2026-04-18T19:20:11.243Z"
  }
  ```
</Response>

Terminal job payload once `GET /v1/jobs/:jobId` reports `"status": "completed"`:

```json
{
  "jobId": "01JS5V8KX5JFK1YQ4G2YZQ4XEP",
  "kind": "project_ingest_github",
  "status": "completed",
  "finishedAt": "2026-04-18T19:22:58Z",
  "result": {
    "prUrl": "https://github.com/acme-coffee/ios-app/pull/482",
    "prNumber": 482,
    "branch": "layers/install-sdk",
    "sdkAppId": "app_a1b2c3d4e5f6",
    "brandContext": {
      "appName": "Acme Coffee",
      "appDescription": "Pre-order your neighborhood coffee shop's daily brew.",
      "tagline": "Skip the line, keep the ritual.",
      "audience": "Urban professionals, 25–45",
      "icp": "Mobile-first coffee loyalists",
      "brandVoice": "Friendly, unfussy, a little wry",
      "keywords": ["coffee", "order ahead", "mobile payments"],
      "primaryLanguage": "en",
      "logoUrl": "https://media.layers.com/.../logo.png"
    },
    "contextPatchedFields": ["appName", "appDescription", "tagline", "audience", "keywords", "brandVoice"]
  }
}
```

## Errors [#errors]

The initial `202` can fail with:

| Status | Code              | When                                                                                                                        |
| ------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
| 422    | `VALIDATION`      | `:id` is not a UUID, body shape invalid, or GitHub installation cannot access `repoFullName` (repo not granted to the App). |
| 401    | `UNAUTHENTICATED` | Missing or invalid key.                                                                                                     |
| 403    | `FORBIDDEN_SCOPE` | Key lacks `ingest:write`.                                                                                                   |
| 404    | `NOT_FOUND`       | Project does not exist, or no GitHub installation is registered.                                                            |
| 409    | `CONFLICT`        | Idempotency replay with a different body.                                                                                   |
| 429    | `RATE_LIMITED`    | Long-running-starts budget exhausted.                                                                                       |

Terminal job failures surface as `status: "failed"` with `error.code` ∈ `CREDENTIAL_INVALID`, `PLATFORM_ERROR`, `CIRCUIT_OPEN`, `INTERNAL`. `error.data.platformCode` + `error.data.platformMessage` carry GitHub's raw error when applicable.

## See also [#see-also]

* [`GET /v1/jobs/:jobId`](/docs/api/reference/jobs/get-job) — poll for progress and final result
* [`GET /v1/projects/:id/sdk-apps/:appId`](/docs/api/reference/sdk-apps/get-sdk-app) — inspect the SDK app the ingest created
* [Jobs](/docs/api/concepts/jobs) — the 202 → poll pattern
* [Onboard a customer](/docs/api/guides/onboard-customer) — full walkthrough
