---
title: Create a managed repo by purpose, optionally from a template
description: Creates a private GitHub repository for the workspace under the Wiblo organisation and registers it.
api_method: POST
api_path: "/v1/workspaces/{workspaceId}/repos"
canonical_url: https://wiblo.app/docs/developers/api/repos/create-workspace-repo
last_updated: 2026-10-11T19:02:53+02:00
md_url: https://wiblo.app/docs/developers/api/repos/create-workspace-repo.md
---

# Create a managed repo by purpose, optionally from a template

`POST /v1/workspaces/{workspaceId}/repos`

Creates a private GitHub repository for the workspace under the Wiblo organisation and registers it. The `purpose` is required and decides the template rule: `base` (the workspace's one base repo, display name fixed to "Base", always from the base template) and `site` (a website, from the default `starter-site` template unless another site template is named) require a template; `app` and `other` start as an empty repository unless a template of that purpose is named. A named template must belong to the purpose. The registry row is claimed first (`provisioning`), the GitHub repo is created as the App (template generate, or an empty repo with an initial commit), default-branch protection is applied best-effort, and the row settles `ready` with its clone URL, immediately cloneable through the credential broker. A `failed` row is re-driven by creating again with the same purpose and name. For a `site`, the create also makes the website: the slug (`slug`, or the workspace name folded to lowercase letters and digits) is checked and claimed before the repository exists, then the Vercel project named by it is created in `region` (`iad1` by default), and the answer carries `site` with its live URL. A refused slug or region makes no repository. Needs an owner or admin; a run's workspace key carries its initiator's role. Per-user rate-limited at 10/60s in its own bucket.

## Path parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `workspaceId` | `string` | Yes |  |

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `purpose` | `"base" \| "site" \| "app" \| "other"` | Yes |  |
| `display_name` | `string` | No |  |
| `template` | `string` | No |  |
| `slug` | `string` | No | For a site only: the site's public name. Omitted means the server folds it from the workspace name. |
| `region` | `string` | No | For a site only: where its functions run (iad1, sfo1, cpt1, syd1, lhr1). Omitted means iad1. |

## Request

**curl**

```bash
curl https://api.wiblo.app/v1/workspaces/{workspaceId}/repos \
  -X POST \
  -H "Authorization: Bearer $WIBLO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "purpose": "base"
}'
```

**TypeScript**

```ts
import { createSdk, createWorkspaceRepo } from "@workspace/sdk"

const sdk = createSdk({ baseUrl: "https://api.wiblo.app" })

const { data, error } = await createWorkspaceRepo({
  client: sdk,
  path: { workspaceId: "..." },
  body: {
    "purpose": "base"
  },
})
```

## Responses

**`201`** — The repo was created and registered. For a `site`, `site` carries the website: its `id`, `slug`, `region`, `status`, `live_url`, and `latest_version`. The create still answers 201 when Vercel refused the project, with `site.status` set to `failed`. `site` is null for every other purpose. Returns `WorkspaceRepoSummary`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `string` | Yes |  |
| `display_name` | `string` | Yes |  |
| `purpose` | `"base" \| "site" \| "app" \| "other"` | Yes |  |
| `template_slug` | `string \| null` | Yes |  |
| `status` | `"provisioning" \| "ready" \| "failed" \| "archived"` | Yes |  |
| `default_branch` | `string` | Yes |  |
| `clone_url` | `string \| null` | Yes |  |
| `site` | `object \| null` | Yes |  |
| `site.id` | `string` | Yes |  |
| `site.slug` | `string` | Yes |  |
| `site.region` | `"iad1" \| "sfo1" \| "cpt1" \| "syd1" \| "lhr1"` | Yes |  |
| `site.status` | `"provisioning" \| "ready" \| "failed" \| "archived"` | Yes |  |
| `site.failure` | `"address_taken" \| "provider" \| null` | Yes | Why the site is `failed`, and null for every other status. `address_taken`: the deploy provider already holds a project named by the slug that no site owns, so the same create refuses again; rename the site with `PATCH .../sites/{siteId}` (`wiblo sites rename`). `provider`: Vercel refused or did not answer; the same create again, or the next build, retries. |
| `site.live_url` | `string \| null` | Yes |  |
| `site.latest_version` | `object \| null` | Yes | The newest version that is `queued`, `building`, `ready`, or `error`, with its `number` and its `status`. The `number` is null until that version is first ready. An `error` version whose deployment is gone keeps the number it had. On the create answer, this is version one as the create left it. It is null there when version one could not be asked for or started, or Vercel refused the project. On the list answer, it is null when the site has no such version. |
| `created_at` | `string` | Yes |  |
| `updated_at` | `string` | Yes |  |

**`400`** — Body failed validation (`VALIDATION_FAILED`): unknown `purpose`, a `display_name` given for `base`, a `display_name` missing for any other purpose, a malformed `template` slug, or `slug` or `region` sent with a purpose other than `site`. Returns `ApiErrorEnvelope`.

**`401`** — No valid session, `wbl_*` bearer, or run token was present. Returns `ApiErrorEnvelope`.

**`403`** — The caller is a member, not an owner or admin (`ADMIN_REQUIRED`), or a machine key with no run to attribute (`INSUFFICIENT_PRIVILEGE`). Returns `ApiErrorEnvelope`.

**`404`** — Workspace not visible to the caller (`WORKSPACE_NOT_FOUND`). Returns `ApiErrorEnvelope`.

**`409`** — The workspace already has its base repo (`BASE_REPO_EXISTS`, `details.repo_id` names it), a live repo already uses this display name (`REPO_NAME_TAKEN`), or the site's slug is taken (`SLUG_TAKEN`). A `slug` sent in the body is taken when any site holds it, in this workspace or another. A slug derived from the workspace name takes a numbered form instead, such as `driftwoodphysio-2`. With no `slug` sent, `SLUG_TAKEN` also answers when a site create is still running in this workspace on the derived slug: try again in five minutes, or send a `slug`. Returns `ApiErrorEnvelope`.

**`422`** — Path param failed UUID validation (`INVALID_PARAMS`), the template is unknown or belongs to another purpose (`TEMPLATE_NOT_FOUND`), or a site's slug or region is refused (`SLUG_INVALID`, `SLUG_RESERVED`, `REGION_INVALID`). Returns `ApiErrorEnvelope`.

**`429`** — Rate limit exceeded. The body's `error.code` is `RATE_LIMITED` and `error.details.retry_after` is the same number of seconds as the `Retry-After` header. Returns `ApiErrorEnvelope`.

**`502`** — The git control plane or the deploy provider is not configured, or the host refused the create (`UPSTREAM_UNAVAILABLE`). Returns `ApiErrorEnvelope`.
