POST /v1/projects/:id/ingest/github
Clone a repo, analyze it, and open a PR that instruments the codebase with the Layers SDK.
/v1/projects/:id/ingest/github- Auth
- Bearer
- Scope
- ingest:write
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 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.
idstringrequiredProject ID to attach the resulting SDK app to.
Idempotency-Keystring (UUID)optionalReplays within 24h. Recommended — ingest is expensive.
repoFullNamestringrequiredRepo identifier in owner/name form, e.g. acme-coffee/ios-app.branchstringoptionaldefault: repository default branchBranch to fork from.openPRbooleanoptionaldefault: trueIf false, the workflow stops after the patch is generated and does not open a PR.platformsstring[]optionalShorthand alias for `sdkConfig.includePlatforms` — restrict instrumentation to specific platforms.sdkConfigobjectoptionalOverrides for SDK app name, platforms, and event allowlist. See below.sdkConfig.appNamestringoptionalOverrides the inferred SDK app name.sdkConfig.includePlatformsstring[]optionalRestrict instrumentation to specific platforms. One of ios, android, web, react-native, flutter, expo.sdkConfig.eventAllowliststring[]optionalIf provided, only these event names are forwarded to CAPI.prTitlestringoptionaldefault: "chore: instrument with Layers SDK"Override the PR title.prBodystringoptionalAdditional markdown appended to the PR body.
The body is strict — keys outside the list above are rejected with 422 VALIDATION.
Example request
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"]
}
}'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);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"])Response
{
"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"
}Terminal job payload once GET /v1/jobs/:jobId reports "status": "completed":
{
"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
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
GET /v1/jobs/:jobId— poll for progress and final resultGET /v1/projects/:id/sdk-apps/:appId— inspect the SDK app the ingest created- Jobs — the 202 → poll pattern
- Onboard a customer — full walkthrough