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

# Gateway lifecycle

Panicly's gateway is not a thin proxy. It is an auth layer, quota layer, rules layer, control layer, logging layer, and provider-forwarding layer.

### Request

POST [https://panicly.vercel.app/v1/chat/completions](https://panicly.vercel.app/v1/chat/completions)

```curl Allowed request
curl -X POST https://panicly.vercel.app/v1/chat/completions \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{
  "model": "openrouter/auto",
  "messages": [
    {
      "role": "user",
      "content": "Write one sentence about protected AI spend."
    }
  ],
  "max_tokens": 64
}'
```

```python Allowed request
import requests

url = "https://panicly.vercel.app/v1/chat/completions"

payload = {
    "model": "openrouter/auto",
    "messages": [
        {
            "role": "user",
            "content": "Write one sentence about protected AI spend."
        }
    ],
    "max_tokens": 64
}
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

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

print(response.json())
```

```javascript Allowed request
const url = 'https://panicly.vercel.app/v1/chat/completions';
const options = {
  method: 'POST',
  headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
  body: '{"model":"openrouter/auto","messages":[{"role":"user","content":"Write one sentence about protected AI spend."}],"max_tokens":64}'
};

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

```go Allowed request
package main

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

func main() {

	url := "https://panicly.vercel.app/v1/chat/completions"

	payload := strings.NewReader("{\n  \"model\": \"openrouter/auto\",\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"Write one sentence about protected AI spend.\"\n    }\n  ],\n  \"max_tokens\": 64\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 Allowed request
require 'uri'
require 'net/http'

url = URI("https://panicly.vercel.app/v1/chat/completions")

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  \"model\": \"openrouter/auto\",\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"Write one sentence about protected AI spend.\"\n    }\n  ],\n  \"max_tokens\": 64\n}"

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

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

HttpResponse<String> response = Unirest.post("https://panicly.vercel.app/v1/chat/completions")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"model\": \"openrouter/auto\",\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"Write one sentence about protected AI spend.\"\n    }\n  ],\n  \"max_tokens\": 64\n}")
  .asString();
```

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

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://panicly.vercel.app/v1/chat/completions', [
  'body' => '{
  "model": "openrouter/auto",
  "messages": [
    {
      "role": "user",
      "content": "Write one sentence about protected AI spend."
    }
  ],
  "max_tokens": 64
}',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
  ],
]);

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

```csharp Allowed request
using RestSharp;

var client = new RestClient("https://panicly.vercel.app/v1/chat/completions");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"model\": \"openrouter/auto\",\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"Write one sentence about protected AI spend.\"\n    }\n  ],\n  \"max_tokens\": 64\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift Allowed request
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = [
  "model": "openrouter/auto",
  "messages": [
    [
      "role": "user",
      "content": "Write one sentence about protected AI spend."
    ]
  ],
  "max_tokens": 64
] as [String : Any]

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

let request = NSMutableURLRequest(url: NSURL(string: "https://panicly.vercel.app/v1/chat/completions")! 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()
```

## Request sequence

The client sends a request to `/v1/chat/completions`, `/v1/responses`, `/v1/embeddings`, `/v1/completions`, `/v1/models`, or a provider namespace such as `/v1/openrouter/*`.

The gateway accepts a key through `Authorization` or `x-panicly-key`, then verifies it against stored key prefix and SHA-256 hash.

The gateway checks workspace plan tier, included monthly volume, credit-backed capacity, and any configured usage offset.

Network Controls, Region Rules, model rules, Sentry Mode, burst protection, abuse detection, token guard, and loop guard can block before upstream spend.

Approved traffic is forwarded to the connected provider with Panicly auth headers stripped and provider auth headers inserted.

Panicly attempts to record route, provider, model, country, decision, reason, tokens, cost estimate, and timestamp.

## Core endpoint contract

### Response (200)

```json
{
  "id": "chatcmpl_panicly_example",
  "object": "chat.completion",
  "created": 1779993600,
  "model": "openrouter/auto",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Panicly blocks risky model traffic before it can create upstream spend."
      },
      "finish_reason": "stop"
    }
  ]
}
```

### Schema (`request.body`)

```yaml
openapi: 3.1.0
info:
  title: API
  version: 1.0.0
paths:
  /v1/chat/completions:
    post:
      operationId: create-chat-completion
      summary: Create a chat completion
      description: >
        Sends an OpenAI-compatible chat completion request through Panicly.

        Panicly authenticates the project key, evaluates controls, records a
        decision, and then forwards approved traffic upstream.
      tags:
        - subpackage_gateway
      responses:
        '200':
          description: Provider response returned through Panicly.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatCompletionResponse'
        '401':
          description: Missing or invalid Panicly API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '402':
          description: Workspace has no remaining included or credit-backed capacity.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Panicly policy blocked the request before provider forwarding.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChatCompletionRequest'
servers:
  - url: https://panicly.vercel.app
  - url: http://localhost:3000
components:
  schemas:
    ChatCompletionRequestMessagesItemsRole:
      type: string
      enum:
        - system
        - user
        - assistant
        - tool
      title: ChatCompletionRequestMessagesItemsRole
    ChatCompletionRequestMessagesItems:
      type: object
      properties:
        role:
          $ref: '#/components/schemas/ChatCompletionRequestMessagesItemsRole'
        content:
          type: string
      required:
        - role
        - content
      title: ChatCompletionRequestMessagesItems
    ChatCompletionRequest:
      type: object
      properties:
        model:
          type: string
          description: Provider model identifier.
        messages:
          type: array
          items:
            $ref: '#/components/schemas/ChatCompletionRequestMessagesItems'
        max_tokens:
          type: integer
          description: Maximum tokens requested from the provider.
        temperature:
          type: number
          format: double
      required:
        - model
        - messages
      title: ChatCompletionRequest
    ChatCompletionResponse:
      type: object
      properties:
        id:
          type: string
        object:
          type: string
        created:
          type: integer
        model:
          type: string
        choices:
          type: array
          items:
            type: object
            additionalProperties:
              description: Any type
      title: ChatCompletionResponse
    ErrorResponseError:
      type: object
      properties:
        type:
          type: string
        reason:
          type: string
        message:
          type: string
      required:
        - type
        - message
      title: ErrorResponseError
    ErrorResponse:
      type: object
      properties:
        error:
          $ref: '#/components/schemas/ErrorResponseError'
      title: ErrorResponse

```

## Authentication fields

Use `Bearer pk_live_...` or `Bearer pk_test_...`.

Alternative direct key header for applications that cannot easily set a bearer token.

Provider model identifier, such as `openrouter/auto` or a connected provider-specific model.

The repository currently has gateway logic in both `apps/api/src/routes/gateway.ts` and `apps/web/src/app/v1/[...path]/route.ts`. Treat behavior as duplicated until the active deployment path is verified.