# Create a plan

Source: https://docs.coderhelm.com/api/reference/plans/create-plan

`POST /api/plans`

Creates a plan, with its tasks if you send them. The plan and its tasks start as `draft`; nothing runs until the plan is executed. Not idempotent: sending the same request twice makes two plans.

## Authentication

A team API key: `Authorization: Bearer chk_...`. The key needs the `plans` scope. See [Authentication](https://docs.coderhelm.com/api/authentication).

## Request body

`application/json`: The plan.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `description` | string or null | no | More about it, in Markdown. |
| `destination` | enum (`github`, `jira`) or null | no | Where a plan's tasks become tickets: `github` (GitHub issues) or `jira`. |
| `repo` | string or null | no | The repository its tasks work in, `owner/name`, unless a task names its own. |
| `tasks` | array of NewPlanTask or null | no | The plan's tasks, in order. Each starts as `draft`. |
| `tasks[].acceptance_criteria` | string or null | no | How to tell it is done. |
| `tasks[].description` | string or null | no | How, in Markdown. The coder reads it as the ticket body. |
| `tasks[].destination` | enum (`github`, `jira`) or null | no | Where a plan's tasks become tickets: `github` (GitHub issues) or `jira`. |
| `tasks[].jira_project` | string or null | no | With `jira`: the Jira project key to file the ticket in. |
| `tasks[].repo` | string or null | no | The repository for this task, `owner/name`, when not the plan's. |
| `tasks[].title` | string or null | no | What to do. Defaults to "Untitled task". |
| `title` | string | yes | What the plan is for. 1 to 500 characters. |

## Responses

### 200

The plan was made.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `plan_id` | string | yes | The new plan's id. |

Headers: `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`.

### 400

The body is not valid JSON, or the title is missing or longer than 500 characters.

### 401

The key is missing, malformed, revoked or expired, or the person who made it left the team.

### 403

The key lacks the scope this endpoint needs (`missing_scope`).

### 429

Over the key's rate limit (`rate_limited`), or, for endpoints that start work, over the team's monthly token limit (`token_limit_reached`).

Headers: `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, `Retry-After`.

### 500

Something failed on CoderHelm's side. Safe to retry reads.

## Example request

cURL:

```bash
curl -X POST 'https://api.coderhelm.com/api/plans' \
  -H "Authorization: Bearer $CODERHELM_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "description": "Brute-force attempts are showing up in the auth logs.",
  "repo": "acme/web",
  "tasks": [
    {
      "acceptance_criteria": "The 11th attempt within 15 minutes gets 429 and a Retry-After header.",
      "description": "Allow 10 attempts per IP per 15 minutes; answer 429 after that.",
      "title": "Limit login attempts per IP"
    }
  ],
  "title": "Rate-limit the login form"
}'
```

TypeScript:

```ts
const res = await fetch("https://api.coderhelm.com/api/plans", {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CODERHELM_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "description": "Brute-force attempts are showing up in the auth logs.",
      "repo": "acme/web",
      "tasks": [
        {
          "acceptance_criteria": "The 11th attempt within 15 minutes gets 429 and a Retry-After header.",
          "description": "Allow 10 attempts per IP per 15 minutes; answer 429 after that.",
          "title": "Limit login attempts per IP"
        }
      ],
      "title": "Rate-limit the login form"
    }),
});
if (!res.ok) {
  const { error, code } = await res.json();
  throw new Error(`${res.status} ${code}: ${error}`);
}
const data = await res.json();
```

Python:

```python
import os
import requests

headers = {
    "Authorization": f"Bearer {os.environ['CODERHELM_API_KEY']}",
}
body = {
    "description": "Brute-force attempts are showing up in the auth logs.",
    "repo": "acme/web",
    "tasks": [
        {
            "acceptance_criteria": "The 11th attempt within 15 minutes gets 429 and a Retry-After header.",
            "description": "Allow 10 attempts per IP per 15 minutes; answer 429 after that.",
            "title": "Limit login attempts per IP",
        },
    ],
    "title": "Rate-limit the login form",
}
res = requests.request("POST", "https://api.coderhelm.com/api/plans", headers=headers, json=body)
if not res.ok:
    raise RuntimeError(f"{res.status_code}: {res.json().get('error')}")
data = res.json()
```

## Example response

```json
{
  "plan_id": "01JB8V0K3M5P7R9T1W3Y5A7C9E"
}
```
