---
title: "Link a GitLab project (push to deploy)"
method: POST
path: "/app/{id}/link-gitlab"
tags: ["apps", "gitlab"]
---

# Link a GitLab project (push to deploy)

`POST /app/{id}/link-gitlab`

## Request

- Base URL: `https://api.faable.com`
- URL: `https://api.faable.com/app/{id}/link-gitlab`
- Auth: one of:
  - OAuth 2 (authorization code, authorization URL `https://faable.auth.faable.link/authorize`, token URL `https://faable.auth.faable.link/oauth/token`)
  - HTTP bearer
  - OAuth 2 (client credentials, token URL `https://faable.auth.faable.link/oauth/token`)
  - HTTP bearer
  - HTTP basic
  - API key in query parameter `apikey`

Links a gitlab.com project to the app and creates a project webhook so every push to the branch deploys. Runs with the calling user's GitLab token; they must be Maintainer of the project.

## Path parameters

- `id` string, required

## Request body

- object
  - `project_id` string, required
  - `branch` string, nullable

## Response `200`

App

- App: App
  - `id` string, required: App ID
  - `team` string, required
  - `name` string, required
  - `description` string, required
  - `slug` string, required
  - `url` string, required
  - `active` boolean, required
  - `disabled_reason` string: Reason the app was administratively disabled. Null when the app is active.
  - `runtime` 'node' | 'php' | 'python', required
  - `runtime_strategy` string, required
  - `detected` object
    - `buildpack` string, required
    - `framework` string
    - `runtime` object, required
      - `name` string, required
      - `version` string
      - `source` string
    - `dockerfile_ignored` boolean
    - `process_type` 'worker'
  - `runtime_pin` object
    - `name` string, required
    - `version` string, required
    - `pinned_at` string, date-time, required
    - `source` union, required
      - 'first_build'
      - 'backfill'
      - 'user_upgrade'
      - 'retirement'
  - `mode` 'standard' | 'coexistence', required
  - `build_mode` 'local' | 'remote'
  - `deploy_output` 'image' | 'artifact'
  - `root_dir` string
  - `gitlab_ci_project_id` string
  - `git_provider` union
    - 'github'
    - 'gitlab'
  - `gitlab_project_id` string
  - `gitlab_project_path` string
  - `gitlab_branch` string
  - `coexistence_routing` 'cookie' | 'path'
  - `coexistence_keep_versions` number: K: how many versions stay live under mode=coexistence (active + K-1 draining)
  - `coexistence_drain_hours` number: Max hours a demoted version keeps serving pinned sessions before GC
  - `instance` string: Compute catalogue row the app runs on (computeinstance id). Replaces instance_type.
  - `instance_type` string, required: Faable instance types comprise varying combinations of CPU, memory, storage, and networking capacity. Name of `instance`; kept for existing clients.
  - `instance_resources` object: The most CPU and memory the app can use on its instance (the limits). Read-only, derived from `instance`.
    - `cpu` string, required: Kubernetes quantity, e.g. '600m'
    - `memory` string, required: Kubernetes quantity, e.g. '512Mi'
  - `instance_upgrades` object[]: Each plan's default instance that gives more than this app's, smallest first. Read-only.
    - `plan` string, required: free | hobby | pro
    - `instance` string, required
    - `cpu` string, required
    - `memory` string, required
  - `region` 'gui1' | 'poz1' | 'ams1', required: High-level summary of where the Database is in its lifecycle
  - `notify_deploy_success` boolean: Success deploy emails (first deploy + every promote). Absent/null = enabled; false = muted. Failure emails are never gated by this.
  - `http3` boolean: Whether HTTP/3 is advertised for this app. Absent/null = advertised; false = not advertised. Note it controls the advertisement, not the listener: clients that already discovered HTTP/3 may keep using it until the advertisement expires, and the server does not refuse it.
  - `status` AppStatus, required
    - `phase` 'UNKNOWN' | 'QUEUED' | 'BUILDING' | 'ERROR' | 'INITIALIZING' | 'READY' | 'CANCELED' | 'TERMINATING', required: High-level summary of where the Database is in its lifecycle
    - `deployment` union, required
      - string: Deployment id for this app
      - object: Full Deployment object, present only when expanded via `?expand=status.deployment`
    - `deployment_previous` union, required
      - string: Previous deployment id for this app
      - object: Full Deployment object, present only when expanded via `?expand=status.deployment`
    - `last_commited_purge` string, nullable, required: Last commited cache Purge
    - `last_commited_purge_key` string, nullable, required: Last commited cache Purge key
    - `last_commited_purge_service` string, nullable, required: Last commited cache Purge key
    - `versions` object[], required: Live versions of the app: the promoted one plus any draining (coexistence) versions
      - `deployment` string, required: Deployment id of this version
      - `role` union, required: promoted = current production; coexisting = draining
        - 'promoted'
        - 'coexisting'
      - `promoted_at` string, required: ISO date this version was promoted
      - `drain_until` string, required: GC-eligibility deadline for a coexisting version (demotion + drain window). Null on the promoted entry.
    - `cold_state` union: Scale-to-zero sub-state of the promoted deployment (admin-only)
      - 'asleep'
      - 'waking'
    - `secrets_revision` string, nullable, required: Opaque revision token; changes whenever the app secrets change
    - `resource_health` object: Resource-limit breach observed at runtime
      - `state` union, required: Which limit was hit
        - 'memory_exceeded': The runtime exceeded its memory limit (OOMKilled)
        - 'disk_exceeded': The pod was evicted for exceeding its disk (ephemeral-storage) limit
      - `deployment` string, required: Deployment whose pod tripped the limit
      - `at` string, required: ISO time the limit was last observed being hit
      - `container` string, nullable: Container that was OOMKilled
    - `runtime_health` object: Runtime crash loop observed on the live version
      - `state` 'crash_loop', required: The container keeps exiting and being restarted (CrashLoopBackOff)
      - `deployment` string, required: Deployment whose pod is crash-looping
      - `at` string, required: ISO time the crash loop was last observed
      - `container` string, nullable: Container that is crash-looping
      - `restarts` integer, nullable: Restart count when observed
      - `exit_code` integer, nullable: Last exit code, when reported
      - `reason` string, nullable: K8s waiting/terminated reason
  - `repository` string
  - `github_branch` string
  - `github_installation_id` string
  - `github_installation_state` string: Platform GitHub access health, maintained by installation lifecycle webhooks. Absent/null = ok; 'deleted' | 'suspended' | 'repo_removed' = access broken until re-linked.
  - `github_repo_hidden` string: The linked repository stopped being readable after the app was created, per the GitHub `repository` webhook. Absent/null = visible; 'deleted' | 'privatized' | 'archived' = the source was pulled after deploying (the analyzer re-scores the promoted deployment against our own build artifact).
  - `github_disconnect_reason` string: Why the platform cleared the repository link automatically. Absent/null = never auto-disconnected (or re-linked since); 'push_flood' = the repo exceeded the hourly push limit and must be re-linked by the user.
  - `deploy_trigger` string: Push-to-deploy trigger. Absent/null = 'workflow' (committed GitHub Action); 'webhook' = the push webhook creates deployments server-side (deploy v4).
  - `deploy_gate` string: Wait-for-CI gate for webhook triggers. Absent/null = deploy on push; 'ci' = deploy only after the commit's check suites complete green.
  - `metadata` object, required
  - `createdAt` string, required: App creation date
  - `updatedAt` string: App updated date

## Other responses

- `400`: `validation_error` — The body, query or path failed schema validation. `details.issues` lists each failing field.
- `401`: `unauthorized` — The request carries no valid credentials. `invalid_token` — The bearer token is missing, malformed or expired.
- `403`: `gitlab_project_not_accessible` — The GitLab project does not exist or the user is not a Maintainer of it (creating the webhook needs Maintainer). `forbidden` — The credentials are valid but do not allow this operation. `user_suspended` — The account is suspended. Contact support@faable.com.
- `409`: `repository_already_linked` — The repository is already linked to another app. `details.app_id` / `details.app_name` name it.
- `412`: `gitlab_identity_missing` — The user has not connected GitLab. Send them through `connect_gitlab`. `gitlab_token_invalid` — The GitLab connection has no usable token (revoked or expired). The user must connect GitLab again.
- `429`: `too_many_requests` — Rate limit exceeded. Honour the `Retry-After` header.
- `500`: `internal_error` — Unexpected server error. Retry later; the request id is logged.
- `503`: `gitlab_not_configured` — GitLab linking is not enabled on this environment. `database_unavailable` — Faable cannot reach its database right now. Transient: retry after the `Retry-After` header.

---

[API](https://skmtc.dev/faable/apis/faablecloud-api.md) · [All operations](https://skmtc.dev/faable/apis/faablecloud-api/llms.txt) · [OpenAPI document](https://skmtc.dev/faable/apis/faablecloud-api/revisions/e2fc93da3c49?raw)
