Using Penguin's API
This guide provides examples and code snippets for interacting with Penguin's API endpoints in various programming languages and environments.
Overview
Penguin provides a comprehensive REST API and WebSocket interface for integrating with applications, scripts, and other services. The API allows you to:
- Send messages and receive responses
- Stream responses in real-time
- Manage conversations
- Persist and control session-scoped goals
- Execute tasks
- Access context files
- Monitor token usage
Quick Start
The examples below assume a local server on the default 127.0.0.1:9000.
If web auth is enabled, include an X-API-Key header using a key from
PENGUIN_API_KEYS or the startup token printed by penguin-web.
Basic Chat Request
import requests
# Send a simple message
response = requests.post(
"http://127.0.0.1:9000/api/v1/chat/message",
headers={"X-API-Key": "your-key"},
json={
"text": "Write a Python function to calculate the Fibonacci sequence."
}
)
# Print the response
print(response.json()["response"])
JavaScript Example
// Send a message using fetch
async function sendMessage(text) {
const response = await fetch('http://127.0.0.1:9000/api/v1/chat/message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-key'
},
body: JSON.stringify({
text: text
})
});
const data = await response.json();
return data.response;
}
// Usage
sendMessage("Explain quantum computing in simple terms")
.then(response => console.log(response))
.catch(error => console.error('Error:', error));
Chat API
Regular Chat
To send a message and receive a response:
curl -X POST http://127.0.0.1:9000/api/v1/chat/message \
-H "Content-Type: application/json" \
-d '{"text": "Generate a hello world program in Rust"}'
With Conversation Context
To continue an existing conversation:
curl -X POST http://127.0.0.1:9000/api/v1/chat/message \
-H "Content-Type: application/json" \
-d '{
"text": "Explain the key components of the code you just generated",
"conversation_id": "conversation_20240330_123456"
}'
With Session-Scoped Directory
Use session_id/conversation_id plus directory to bind a session to a specific repo root.
curl -X POST http://127.0.0.1:9000/api/v1/chat/message \
-H "Content-Type: application/json" \
-d '{
"text": "Run checks in this repo",
"session_id": "repo-session-1",
"directory": "/absolute/path/to/repo"
}'
Notes:
- The first valid directory bound to a session becomes the session root.
- Rebinding that same session to a different directory returns
409. - Invalid directories return
400.
Tool Use Through Chat
Tools are invoked by the model during chat execution, not by calling tool names
directly from the web API. To let Penguin inspect or modify a repo, provide a
stable session_id and directory; the runtime binds that session to the repo
root and executes native provider tool calls there.
Tool-related behavior to expect:
- successful tool runs appear in the REST
action_resultspayload - live clients receive message/tool-part events for tool start and completion
- approval-gated tools pause until approved or denied
- native provider tool calls are preferred; ActionXML is retained only as a fallback compatibility path
- Penguin avoids executing both native tool calls and duplicate ActionXML from the same assistant turn
Streaming Responses
For real-time streaming of responses, use the WebSocket API:
// Create WebSocket connection
const socket = new WebSocket('ws://127.0.0.1:9000/api/v1/chat/stream');
// Connection opened
socket.addEventListener('open', (event) => {
socket.send(JSON.stringify({
text: "Write a step-by-step guide to installing TensorFlow.",
session_id: "stream-session-1",
directory: "/absolute/path/to/repo"
}));
});
// Listen for messages
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.event === 'start') {
console.log('Response started');
} else if (data.event === 'token') {
process.stdout.write(data.data.token);
} else if (data.event === 'complete') {
console.log('\nResponse completed');
console.log('Action results:', data.data.action_results);
}
});
Conversation Management
List Conversations
import requests
# Get all conversations
response = requests.get("http://127.0.0.1:9000/api/v1/conversations")
conversations = response.json()["conversations"]
for conv in conversations:
print(f"ID: {conv['session_id']}, Messages: {conv['message_count']}")
Create New Conversation
import requests
# Create a new conversation
response = requests.post("http://127.0.0.1:9000/api/v1/conversations/create")
conversation_id = response.json()["conversation_id"]
print(f"Created conversation: {conversation_id}")
Retrieve Conversation
import requests
# Get a specific conversation
conversation_id = "conversation_20240330_123456"
response = requests.get(f"http://127.0.0.1:9000/api/v1/conversations/{conversation_id}")
data = response.json()
# Get messages
for message in data["messages"]:
print(f"{message['role']}: {message['content']}")
Session Goals
A session goal is durable lifecycle state attached to an existing saved
session. The HTTP API separates persistence from execution: creating a goal
stores it, while POST .../goal/run starts RunMode execution. The TUI's
/goal <objective> command calls both operations for convenience.
There is one goal per session. /247 is a TUI alias for /goal; it does not
create a second goal type and does not enable automatic continuation.
The current implementation assumes one server process owns a session store. Its locks and live-run registry are process-local, so do not point multiple web workers at the same conversation files. Cross-process leases are not part of this release.
Read Goal State
curl http://127.0.0.1:9000/api/v1/session/SESSION_ID/goal \
-H "X-API-Key: your-key"
An existing session with no goal returns 200:
{"goal": null, "status": "ok"}
Create or Replace a Goal
curl -X POST http://127.0.0.1:9000/api/v1/session/SESSION_ID/goal \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{
"objective": "Ship the session-goal API with regression tests",
"token_budget": 50000,
"replace": false
}'
The objective must be non-empty. token_budget, when present, is a positive
cumulative stop threshold across runs. Replacing an unfinished goal requires
"replace": true; completed goals may be replaced without the flag. A goal
with an active run cannot be replaced until that run is stopped and released.
Pause and Resume
Pause through the goal mutation endpoint:
curl -X POST http://127.0.0.1:9000/api/v1/session/SESSION_ID/goal \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{"status": "paused"}'
Resume by setting active, then explicitly starting a run:
curl -X POST http://127.0.0.1:9000/api/v1/session/SESSION_ID/goal \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{"status": "active"}'
curl -X POST http://127.0.0.1:9000/api/v1/session/SESSION_ID/goal/run \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{"max_iterations": 8, "timeout_seconds": 600}'
max_iterations and timeout_seconds are optional positive integers with no
Penguin hard maximum. If omitted, they remain unset and do not stop execution.
Likewise, the runtime passes a token ceiling only when the goal has an explicit
cumulative token_budget. Because usage is reported after a provider response,
the last billed turn may cross that configured threshold; a valid structured
finish on that turn still wins. An exhausted explicit cumulative budget returns
budget_limited.
Goal creation and execution are separate HTTP requests. If a client loses its
connection after creation but before /goal/run is acknowledged, read the
persisted goal first: retry the same create request identifiers when available,
or call /goal/run for the already-active, unrun goal. Do not replace the goal
just to recover from an uncertain response.
Pausing does not silently rewrite a completed result from an already-running request. Goal ID, run ID, revision, and active status fence late completion so paused, cleared, or replaced state wins.
Clear a Goal
curl -X DELETE http://127.0.0.1:9000/api/v1/session/SESSION_ID/goal \
-H "X-API-Key: your-key"
Successful reads and mutations use an operation acknowledgement at the top
level; the lifecycle value remains inside goal:
{
"status": "ok",
"goal": {
"id": "goal_...",
"objective": "Ship the session-goal API with regression tests",
"status": "active",
"revision": 3,
"token_budget": 50000,
"tokens_used": 12000,
"time_used_seconds": 91.4,
"active_run_id": null
}
}
Successful /goal/run responses instead expose the resulting lifecycle status
at the top level and include the bounded RunMode result:
{
"status": "active",
"goal": { "id": "goal_...", "status": "active" },
"result": {
"status": "pending_review",
"finish_status": "partial",
"finish_summary": "Implemented the first verified slice"
}
}
Goal lifecycle values are active, paused, blocked, usage_limited,
budget_limited, and complete. A successful validated
finish_task(status="done") marks a goal complete; the deprecated
task_completed result marker is also honored when it carries an anchored
machine-readable done status. Partial progress remains
active; clarification or an explicit blocked finish leaves it blocked.
Goal API errors use these status codes:
404: session missing, or a pause/resume/run/clear operation has no goal.409: unfinished replacement lacksreplace=true, a run/session is busy, or the requested lifecycle transition conflicts with current state.422: malformed JSON fields, unknown status, empty objective, non-positive or over-limit budget/iteration value, or other schema validation failure.500: the server could not durably save/finalize goal state or initialize the scoped runtime; the run is fenced for recovery rather than reported as success.
Project and Task Management
Create Project
import requests
# Create a new project
response = requests.post(
"http://127.0.0.1:9000/api/v1/projects",
json={
"name": "Website Development",
"description": "Create a responsive website with React and FastAPI backend"
}
)
project_data = response.json()
print(f"Created project: {project_data}")
Execute Task
import requests
# Run a task
response = requests.post(
"http://127.0.0.1:9000/api/v1/tasks/execute",
json={
"name": "Generate API documentation",
"description": "Create OpenAPI documentation for all endpoints",
"continuous": False,
"time_limit": 30 # minutes
}
)
print(f"Task execution started: {response.json()}")
Utility Endpoints
Token Usage
import requests
# Get runtime/global token usage stats
response = requests.get("http://127.0.0.1:9000/api/v1/token-usage")
usage = response.json()["usage"]
print(f"Usage scope: {usage['scope']}")
# Get transcript-safe, session-scoped context-window telemetry
response = requests.get(
"http://127.0.0.1:9000/api/v1/token-usage",
params={"session_id": "example-session"},
)
usage = response.json()["usage"]
if usage["scope"] == "session":
print(f"Session context usage: {usage['current_total_tokens']} tokens")
Context Files
import requests
# List available context files
response = requests.get("http://127.0.0.1:9000/api/v1/context-files")
files = response.json()["files"]
for file in files:
print(f"File: {file['path']}, Size: {file['size']}")
# Load a context file
response = requests.post(
"http://127.0.0.1:9000/api/v1/context-files/load",
json={
"file_path": "docs/api_reference.md"
}
)
print(f"Context file loaded: {response.json()}")
Error Handling
The API uses standard HTTP status codes:
- 200: Success
- 400: Bad Request (invalid parameters)
- 404: Not Found (resource doesn't exist)
- 409: Conflict (resource lifecycle or active-run conflict)
- 422: Unprocessable Entity (request schema or value validation failed)
- 500: Server Error
Example error handling:
import requests
try:
response = requests.post(
"http://127.0.0.1:9000/api/v1/chat/message",
json={"invalid_param": "value"}
)
response.raise_for_status()
data = response.json()
print(data["response"])
except requests.exceptions.HTTPError as e:
if response.status_code == 400:
print(f"Bad request: {response.json().get('detail')}")
elif response.status_code == 500:
print(f"Server error: {response.json().get('detail')}")
else:
print(f"HTTP error: {e}")
except Exception as e:
print(f"Error: {e}")
Integrating with Other Languages
cURL
# Send a message
curl -X POST http://127.0.0.1:9000/api/v1/chat/message \
-H "Content-Type: application/json" \
-d '{"text": "What are the key features of Penguin?"}'
Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class PenguinApiExample {
public static void main(String[] args) {
try {
HttpClient client = HttpClient.newHttpClient();
String requestBody = "{\"text\": \"Explain SOLID principles\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1:9000/api/v1/chat/message"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
} catch (Exception e) {
e.printStackTrace();
}
}
}
Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
func main() {
// Create request body
requestBody, _ := json.Marshal(map[string]string{
"text": "Explain container orchestration",
})
// Send POST request
resp, err := http.Post(
"http://127.0.0.1:9000/api/v1/chat/message",
"application/json",
bytes.NewBuffer(requestBody),
)
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
// Read response
body, _ := ioutil.ReadAll(resp.Body)
// Parse JSON response
var result map[string]interface{}
json.Unmarshal(body, &result)
fmt.Println(result["response"])
}
Rate Limiting
The API currently does not implement rate limiting, but best practices include:
- Limit request frequency to avoid overloading the server
- Implement retries with exponential backoff for failed requests
- Use a single conversation for related messages to maintain context
Security Considerations
When deploying Penguin in a production environment:
- Use HTTPS for all API communication
- Implement proper authentication and authorization
- Restrict CORS settings to known domains
- Review and limit model capabilities as needed
Next Steps
- Review the API Reference Documentation for detailed endpoint specifications
- Explore the Web Interface for a browser-based experience
- Consider setting up a proxy for production deployments