> This page is for Gateway.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.merge.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.merge.dev/_mcp/server.

# Create Project

POST https://api-gateway.merge.dev/v1/projects
Content-Type: application/json

Create a project, optionally with its own routing policy and a spending
budget. Without a routing policy, the project routes with the
organization's default.

Reference: https://docs.merge.dev/merge-gateway/management-api/projects/create-project

## Authentication

- `Authorization` header (bearer token, required) — A management key (`mgmt_...`), sent as `Authorization: Bearer <key>`. Reading /v1/routing-policies also accepts the organization's gateway API keys; writing there needs a management key.

## Request

### Body (application/json)

This endpoint expects a ProjectV1Create.

- `name` (string, required) — The project's name.
- `budget_config` (ProjectBudgetConfigV1, optional, nullable) — A spending budget for the project.
- `description` (string, optional, nullable) — What the project is for.
- `routing_policy` (ProjectPolicyV1, optional, nullable) — The project's own routing policy. Send it with `uses_organization_default_routing` false.
- `uses_organization_default_routing` (boolean, optional, default: true) — true routes the project with the organization's default policy. false needs `routing_policy`, which becomes the project's own policy.

## Response

### 201

The created project

- `budget_config` (ProjectBudgetConfigV1Response, required, nullable) — The project's spending budget; null when it has none.
- `created_at` (datetime, required) — When the project was created.
- `description` (string, required, nullable) — What the project is for.
- `id` (string, required) — The project's id.
- `is_active` (boolean, required) — false while the project's prompt injection, DLP and controls overrides are switched off.
- `name` (string, required) — The project's name.
- `organization_id` (string, required) — The organization the project belongs to.
- `routing_policy` (PolicyV2Response, required, nullable) — The project's own routing policy; null when it uses the organization default.
- `slug` (string, required) — Derived from the name at creation and unique in the organization. It doesn't change when the project is renamed.
- `updated_at` (datetime, required) — When the project last changed.
- `uses_organization_default_routing` (boolean, required) — true when the project routes with the organization's default policy, having no policy of its own.

## Errors

### 400 Bad Request Error

The request names a model the organization can't use: restricted, vendor-gated, or another organization's self-hosted model.

- `detail` (ErrorResponseDetail, required) — A message, or an object with a `code` to branch on and a `message`.

### 401 Unauthorized Error

The key is missing or invalid.

- `detail` (ErrorResponseDetail, required) — A message, or an object with a `code` to branch on and a `message`.

### 403 Forbidden Error

The key lacks the scope this route needs, or the change is not allowed.

- `detail` (ErrorResponseDetail, required) — A message, or an object with a `code` to branch on and a `message`.

### 422 Unprocessable Entity Error

Validation Error

- `detail` (list of ValidationError, optional)

### 502 Bad Gateway Error

The gateway could not be updated. Retrying the same request is safe.

- `detail` (ErrorResponseDetail, required) — A message, or an object with a `code` to branch on and a `message`.

## Types

### ProjectBudgetConfigV1

A spending budget for the project.

- `amount` (double, required) — Budget amount in USD.
- `enforcement_mode` (enum, required) — `hard_limit` alerts, then refuses requests with `402 project_budget_exceeded` until the period resets or the budget is raised. `soft_limit` only alerts.
  - Allowed values: `soft_limit`, `hard_limit`
- `period` (enum, required) — How often the budget resets.
  - Allowed values: `daily`, `weekly`, `monthly`, `quarterly`, `yearly`
- `alert_thresholds` (list of integer, optional) — Alert percentages, like [80, 90].
- `notification_emails` (list of string, optional) — Addresses emailed when a threshold is reached. Recipients don't need a Merge account.
- `notify_org_admins` (boolean, optional, default: true) — Email every organization admin alongside `notification_emails`. Naming recipients adds to the audience rather than replacing it; set false to email only the named recipients. While `notification_emails` is empty, one admin is notified.

### ProjectPolicyV1

The project's own routing policy. Same fields as an organization routing policy, except that `name` is optional.

- `strategy` (enum, required) — `PRIORITY` tries `priority_order` in turn; `INTELLIGENT` picks among `allowed_models` per request, optimizing for `axis`.
  - Allowed values: `PRIORITY`, `INTELLIGENT`
- `allowed_models` (list of string, optional, nullable) — INTELLIGENT only. Kept in the order sent. A model listed twice is a 422. Models with no capability tier are dropped, so the response can list fewer.
- `allowed_providers` (list of string, optional, nullable) — INTELLIGENT only, optional. Not stored: responses rebuild it from allowed_models. If sent non-empty, it must name the same providers as allowed_models (order and repeats don't matter), or the request fails with 422.
- `axis` (enum, optional, nullable) — INTELLIGENT only. What the router optimizes for: `COST`, `PERFORMANCE` or `CAPABILITY`. Omitted or null means `COST`.
  - Allowed values: `COST`, `PERFORMANCE`, `CAPABILITY`
- `description` (string, optional, nullable) — Omitted or null keeps the current description when replacing a policy. Send "" to clear it.
- `name` (string, optional, nullable) — Omitted or null: "\<project name> routing policy" for a new policy, and the current name when replacing one.
- `priority_order` (list of PriorityModel, optional, nullable) — PRIORITY only. The priority values are kept as sent; responses list the entries sorted by priority. A model listed twice is a 422.

### ProjectBudgetConfigV1Response

The project's spending budget.

- `amount` (double, required) — Budget amount in USD.
- `enforcement_mode` (enum, required) — `hard_limit` alerts, then refuses requests with `402 project_budget_exceeded` until the period resets or the budget is raised. `soft_limit` only alerts.
  - Allowed values: `soft_limit`, `hard_limit`
- `period` (enum, required) — How often the budget resets.
  - Allowed values: `daily`, `weekly`, `monthly`, `quarterly`, `yearly`
- `alert_thresholds` (list of integer, optional) — Alert percentages, like [80, 90], sorted ascending without duplicates.
- `notification_emails` (list of string, optional) — Addresses emailed when a threshold is reached, in the order sent with duplicates dropped. Recipients don't need a Merge account.
- `notify_org_admins` (boolean, optional, default: true) — Email every organization admin alongside `notification_emails`. Naming recipients adds to the audience rather than replacing it; set false to email only the named recipients. While `notification_emails` is empty, one admin is notified.

### PolicyV2Response

A routing policy: how the gateway picks the model for a request.

- `allowed_models` (list of string, required, nullable) — INTELLIGENT only. In the order they were sent.
- `allowed_providers` (list of string, required, nullable) — INTELLIGENT only. The providers of allowed_models, sorted. Always rebuilt from allowed_models; never stored.
- `axis` (enum, required, nullable) — INTELLIGENT only, else null. What the router optimizes for: `COST`, `PERFORMANCE` or `CAPABILITY`.
  - Allowed values: `COST`, `PERFORMANCE`, `CAPABILITY`
- `created_at` (datetime, required) — When the policy was created.
- `description` (string, required, nullable) — What the policy is for.
- `id` (string, required) — The policy's id. It stays the same across updates.
- `model` (string, required, nullable) — The ready-to-send model string for this policy, like "@alias/fast-experiment". Send it as the `model` in any gateway request to route with this policy.
- `modified_at` (datetime, required) — When the policy last changed.
- `name` (string, required) — The policy's name.
- `priority_order` (list of PriorityModel, required, nullable) — PRIORITY only. Sorted by priority.
- `slug` (string, required, nullable) — The policy's model alias as a bare name, like "fast-experiment". Null only when no alias could be made, such as an organization at its alias limit.
- `strategy` (enum, required) — `PRIORITY` tries `priority_order` in turn; `INTELLIGENT` picks among `allowed_models` per request, optimizing for `axis`.
  - Allowed values: `PRIORITY`, `INTELLIGENT`

### ErrorResponseDetail

A message, or an object with a `code` to branch on and a `message`.

- string
- ErrorDetail: ErrorDetail

### ValidationError

- `loc` (list of ValidationErrorLocItems, required)
- `msg` (string, required)
- `type` (string, required)
- `ctx` (ValidationErrorCtx, optional)
- `input` (any, optional)

### PriorityModel

One model in a PRIORITY policy's fallback order.

- `model` (string, required) — Canonical `provider/model` id.
- `priority` (integer, required) — Lower tries first.

### ErrorDetail

A structured error. Some carry extra fields, like `count`.

- `code` (string, required) — A stable code to branch on, like `payment_method_required`.
- `message` (string, required) — A human-readable explanation.

### ValidationErrorLocItems

- string
- integer

### ValidationErrorCtx

## Examples

**Request**

```json
{
  "name": "support-bot",
  "budget_config": {
    "amount": 500,
    "enforcement_mode": "hard_limit",
    "period": "monthly",
    "alert_thresholds": [
      50,
      80,
      90
    ],
    "notification_emails": [
      "finance@acme.com"
    ],
    "notify_org_admins": true
  },
  "description": "Customer support assistant.",
  "routing_policy": {
    "strategy": "PRIORITY",
    "description": "Claude first, GPT-5.5 as the fallback.",
    "name": "support-bot routing policy",
    "priority_order": [
      {
        "model": "anthropic/claude-sonnet-4-6",
        "priority": 1
      },
      {
        "model": "openai/gpt-5.5",
        "priority": 2
      }
    ]
  },
  "uses_organization_default_routing": false
}
```

**Response**

```json
{
  "budget_config": {
    "amount": 500,
    "enforcement_mode": "hard_limit",
    "period": "monthly",
    "alert_thresholds": [
      50,
      80,
      90
    ],
    "notification_emails": [
      "finance@acme.com"
    ],
    "notify_org_admins": true
  },
  "created_at": "2026-06-02T17:04:00Z",
  "description": "Customer support assistant.",
  "id": "8b6f2d4a-1c3e-4b5a-9f7d-2e0c6a8b4d91",
  "is_active": true,
  "name": "support-bot",
  "organization_id": "3f2b8c1e-6a4d-4e7f-9b21-5c8d0e7a4f13",
  "routing_policy": {
    "allowed_models": null,
    "allowed_providers": null,
    "axis": null,
    "created_at": "2026-06-02T17:04:00Z",
    "description": "Claude first, GPT-5.5 as the fallback.",
    "id": "c4e1a7b2-9d3f-4a6e-8b5c-1f2d3e4a5b6c",
    "model": null,
    "modified_at": "2026-06-02T17:04:00Z",
    "name": "support-bot routing policy",
    "priority_order": [
      {
        "model": "anthropic/claude-sonnet-4-6",
        "priority": 1
      },
      {
        "model": "openai/gpt-5.5",
        "priority": 2
      }
    ],
    "slug": null,
    "strategy": "PRIORITY"
  },
  "slug": "support-bot",
  "updated_at": "2026-09-14T09:30:00Z",
  "uses_organization_default_routing": false
}
```

**SDK Code**

```python
import requests

url = "https://api-gateway.merge.dev/v1/projects"

payload = {
    "name": "support-bot",
    "budget_config": {
        "amount": 500,
        "enforcement_mode": "hard_limit",
        "period": "monthly",
        "alert_thresholds": [50, 80, 90],
        "notification_emails": ["finance@acme.com"],
        "notify_org_admins": True
    },
    "description": "Customer support assistant.",
    "routing_policy": {
        "strategy": "PRIORITY",
        "description": "Claude first, GPT-5.5 as the fallback.",
        "name": "support-bot routing policy",
        "priority_order": [
            {
                "model": "anthropic/claude-sonnet-4-6",
                "priority": 1
            },
            {
                "model": "openai/gpt-5.5",
                "priority": 2
            }
        ]
    },
    "uses_organization_default_routing": False
}
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api-gateway.merge.dev/v1/projects';
const options = {
  method: 'POST',
  headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
  body: '{"name":"support-bot","budget_config":{"amount":500,"enforcement_mode":"hard_limit","period":"monthly","alert_thresholds":[50,80,90],"notification_emails":["finance@acme.com"],"notify_org_admins":true},"description":"Customer support assistant.","routing_policy":{"strategy":"PRIORITY","description":"Claude first, GPT-5.5 as the fallback.","name":"support-bot routing policy","priority_order":[{"model":"anthropic/claude-sonnet-4-6","priority":1},{"model":"openai/gpt-5.5","priority":2}]},"uses_organization_default_routing":false}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api-gateway.merge.dev/v1/projects"

	payload := strings.NewReader("{\n  \"name\": \"support-bot\",\n  \"budget_config\": {\n    \"amount\": 500,\n    \"enforcement_mode\": \"hard_limit\",\n    \"period\": \"monthly\",\n    \"alert_thresholds\": [\n      50,\n      80,\n      90\n    ],\n    \"notification_emails\": [\n      \"finance@acme.com\"\n    ],\n    \"notify_org_admins\": true\n  },\n  \"description\": \"Customer support assistant.\",\n  \"routing_policy\": {\n    \"strategy\": \"PRIORITY\",\n    \"description\": \"Claude first, GPT-5.5 as the fallback.\",\n    \"name\": \"support-bot routing policy\",\n    \"priority_order\": [\n      {\n        \"model\": \"anthropic/claude-sonnet-4-6\",\n        \"priority\": 1\n      },\n      {\n        \"model\": \"openai/gpt-5.5\",\n        \"priority\": 2\n      }\n    ]\n  },\n  \"uses_organization_default_routing\": false\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api-gateway.merge.dev/v1/projects")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"name\": \"support-bot\",\n  \"budget_config\": {\n    \"amount\": 500,\n    \"enforcement_mode\": \"hard_limit\",\n    \"period\": \"monthly\",\n    \"alert_thresholds\": [\n      50,\n      80,\n      90\n    ],\n    \"notification_emails\": [\n      \"finance@acme.com\"\n    ],\n    \"notify_org_admins\": true\n  },\n  \"description\": \"Customer support assistant.\",\n  \"routing_policy\": {\n    \"strategy\": \"PRIORITY\",\n    \"description\": \"Claude first, GPT-5.5 as the fallback.\",\n    \"name\": \"support-bot routing policy\",\n    \"priority_order\": [\n      {\n        \"model\": \"anthropic/claude-sonnet-4-6\",\n        \"priority\": 1\n      },\n      {\n        \"model\": \"openai/gpt-5.5\",\n        \"priority\": 2\n      }\n    ]\n  },\n  \"uses_organization_default_routing\": false\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api-gateway.merge.dev/v1/projects")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"name\": \"support-bot\",\n  \"budget_config\": {\n    \"amount\": 500,\n    \"enforcement_mode\": \"hard_limit\",\n    \"period\": \"monthly\",\n    \"alert_thresholds\": [\n      50,\n      80,\n      90\n    ],\n    \"notification_emails\": [\n      \"finance@acme.com\"\n    ],\n    \"notify_org_admins\": true\n  },\n  \"description\": \"Customer support assistant.\",\n  \"routing_policy\": {\n    \"strategy\": \"PRIORITY\",\n    \"description\": \"Claude first, GPT-5.5 as the fallback.\",\n    \"name\": \"support-bot routing policy\",\n    \"priority_order\": [\n      {\n        \"model\": \"anthropic/claude-sonnet-4-6\",\n        \"priority\": 1\n      },\n      {\n        \"model\": \"openai/gpt-5.5\",\n        \"priority\": 2\n      }\n    ]\n  },\n  \"uses_organization_default_routing\": false\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api-gateway.merge.dev/v1/projects', [
  'body' => '{
  "name": "support-bot",
  "budget_config": {
    "amount": 500,
    "enforcement_mode": "hard_limit",
    "period": "monthly",
    "alert_thresholds": [
      50,
      80,
      90
    ],
    "notification_emails": [
      "finance@acme.com"
    ],
    "notify_org_admins": true
  },
  "description": "Customer support assistant.",
  "routing_policy": {
    "strategy": "PRIORITY",
    "description": "Claude first, GPT-5.5 as the fallback.",
    "name": "support-bot routing policy",
    "priority_order": [
      {
        "model": "anthropic/claude-sonnet-4-6",
        "priority": 1
      },
      {
        "model": "openai/gpt-5.5",
        "priority": 2
      }
    ]
  },
  "uses_organization_default_routing": false
}',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api-gateway.merge.dev/v1/projects");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"name\": \"support-bot\",\n  \"budget_config\": {\n    \"amount\": 500,\n    \"enforcement_mode\": \"hard_limit\",\n    \"period\": \"monthly\",\n    \"alert_thresholds\": [\n      50,\n      80,\n      90\n    ],\n    \"notification_emails\": [\n      \"finance@acme.com\"\n    ],\n    \"notify_org_admins\": true\n  },\n  \"description\": \"Customer support assistant.\",\n  \"routing_policy\": {\n    \"strategy\": \"PRIORITY\",\n    \"description\": \"Claude first, GPT-5.5 as the fallback.\",\n    \"name\": \"support-bot routing policy\",\n    \"priority_order\": [\n      {\n        \"model\": \"anthropic/claude-sonnet-4-6\",\n        \"priority\": 1\n      },\n      {\n        \"model\": \"openai/gpt-5.5\",\n        \"priority\": 2\n      }\n    ]\n  },\n  \"uses_organization_default_routing\": false\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = [
  "name": "support-bot",
  "budget_config": [
    "amount": 500,
    "enforcement_mode": "hard_limit",
    "period": "monthly",
    "alert_thresholds": [50, 80, 90],
    "notification_emails": ["finance@acme.com"],
    "notify_org_admins": true
  ],
  "description": "Customer support assistant.",
  "routing_policy": [
    "strategy": "PRIORITY",
    "description": "Claude first, GPT-5.5 as the fallback.",
    "name": "support-bot routing policy",
    "priority_order": [
      [
        "model": "anthropic/claude-sonnet-4-6",
        "priority": 1
      ],
      [
        "model": "openai/gpt-5.5",
        "priority": 2
      ]
    ]
  ],
  "uses_organization_default_routing": false
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api-gateway.merge.dev/v1/projects")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```