API Reference
Build on top of Moonage programmatically with the REST API.
Overview
The Moonage API allows you to interact with Spaces, trigger Smart Actions, and manage integrations programmatically. It follows REST conventions and returns JSON responses.
Base URL
https://api.moonage.ai/v1Content type
All requests and responses use application/json unless otherwise noted.
Authentication
All API requests require a Bearer token in the Authorization header:
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://api.moonage.ai/v1/spacesCreating API keys
API keys are scoped to your organization and can be created in Settings > API Keys.
| Key type | Scope | Use case |
|---|---|---|
| Organization | All spaces | Admin automation, CI/CD |
| Space | Single space | Scoped integrations |
| Read-only | GET requests only | Monitoring, dashboards |
Key rotation
API keys can be rotated without downtime:
- Create a new key
- Update your applications to use the new key
- Revoke the old key
Both keys remain valid during the transition period.
Spaces
List spaces
GET /spacesQuery parameters:
| Parameter | Type | Description |
|---|---|---|
limit | number | Max results (default: 20, max: 100) |
offset | number | Pagination offset |
status | string | Filter by status: active, paused, archived |
Response:
{
"data": [
{
"id": "sp_abc123",
"name": "Engineering",
"status": "active",
"created_at": "2025-01-15T10:30:00Z",
"integrations": ["github", "linear", "slack"]
}
],
"pagination": {
"total": 5,
"limit": 20,
"offset": 0
}
}Create a space
POST /spacesRequest body:
{
"name": "Product Team",
"description": "Product management workflows",
"persona": "product-manager",
"model": "sonnet"
}Get space details
GET /spaces/:idReturns full space information including integrations, persona config, and statistics.
Update a space
PATCH /spaces/:idUpdatable fields: name, description, persona, model, status
Delete a space
DELETE /spaces/:idInitiates a 30-day grace period. The space is immediately archived and permanently deleted after 30 days.
Messages
Send a message
POST /spaces/:id/messagesRequest body:
{
"content": "Summarize all open PRs in the auth-service repo",
"stream": true
}Parameters:
| Parameter | Type | Description |
|---|---|---|
content | string | The message to send to the agent |
stream | boolean | Enable SSE streaming (default: false) |
context | object | Additional context for the agent |
Streaming responses
When stream: true, the response is a Server-Sent Events stream:
event: progress
data: {"step": "Fetching GitHub PRs", "status": "running"}
event: progress
data: {"step": "Fetching GitHub PRs", "status": "completed", "duration_ms": 1200}
event: progress
data: {"step": "Summarizing results", "status": "running"}
event: result
data: {"content": "Here are the open PRs...", "tool_calls": [...]}
event: done
data: {"total_duration_ms": 3400}List conversation history
GET /spaces/:id/messagesQuery parameters:
| Parameter | Type | Description |
|---|---|---|
limit | number | Max results (default: 50) |
before | string | ISO timestamp for pagination |
after | string | ISO timestamp for pagination |
Smart Actions
Trigger an action
POST /spaces/:id/actionsRequest body:
{
"prompt": "Generate a weekly report of all merged PRs",
"output": {
"type": "slack",
"channel": "#engineering-updates"
}
}Get action status
GET /spaces/:id/actions/:actionIdResponse:
{
"id": "act_xyz789",
"status": "completed",
"prompt": "Generate a weekly report...",
"steps": [
{
"name": "Fetch merged PRs",
"status": "completed",
"duration_ms": 1500,
"tool": "github.list_pull_requests"
},
{
"name": "Summarize and format",
"status": "completed",
"duration_ms": 2100,
"tool": "llm.summarize"
},
{
"name": "Post to Slack",
"status": "completed",
"duration_ms": 800,
"tool": "slack.post_message"
}
],
"total_duration_ms": 4400,
"created_at": "2025-02-01T14:00:00Z"
}Action statuses
| Status | Description |
|---|---|
pending | Queued, not yet started |
planning | LLM is generating the execution plan |
running | Executing tool calls |
completed | All steps finished successfully |
failed | One or more steps failed after retries |
cancelled | Cancelled by user or timeout |
Integrations
List integrations
GET /spaces/:id/integrationsConnect an integration
POST /spaces/:id/integrationsReturns an OAuth authorization URL. Redirect the user to complete the flow.
Disconnect an integration
DELETE /spaces/:id/integrations/:providerRevokes tokens and removes indexed data for the integration.
Error handling
Error format
All errors follow a consistent format:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Too many requests. Retry after 30 seconds.",
"retry_after": 30
}
}Error codes
| Code | HTTP Status | Description |
|---|---|---|
unauthorized | 401 | Invalid or missing API key |
forbidden | 403 | Key doesn't have required scope |
not_found | 404 | Resource doesn't exist |
rate_limit_exceeded | 429 | Too many requests |
internal_error | 500 | Server error (retry safe) |
integration_error | 502 | Upstream integration failed |
Rate limits
| Plan | Requests/minute | Concurrent actions |
|---|---|---|
| Standard | 100 | 5 |
| Pro | 1,000 | 25 |
| Enterprise | Custom | Custom |
Rate limit headers
Every response includes:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1706803200Handling rate limits
When rate limited, the response includes a Retry-After header. Implement exponential backoff:
async function fetchWithRetry(url: string, options: RequestInit, retries = 3) {
for (let i = 0; i < retries; i++) {
const res = await fetch(url, options);
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get("Retry-After") || "5");
await new Promise((r) => setTimeout(r, retryAfter * 1000));
continue;
}
return res;
}
throw new Error("Max retries exceeded");
}Webhooks
Configuring webhooks
Set up webhooks to receive real-time notifications:
POST /webhooks{
"url": "https://your-app.com/webhooks/moonage",
"events": ["action.completed", "action.failed", "space.updated"],
"secret": "your-webhook-secret"
}Verifying signatures
All webhook payloads include an X-Moonage-Signature header. Verify it using HMAC-SHA256:
import { createHmac } from "crypto";
function verifyWebhook(payload: string, signature: string, secret: string) {
const expected = createHmac("sha256", secret).update(payload).digest("hex");
return signature === `sha256=${expected}`;
}Event types
| Event | Description |
|---|---|
action.completed | A Smart Action finished successfully |
action.failed | A Smart Action failed after retries |
space.updated | Space settings changed |
integration.connected | New integration connected |
integration.disconnected | Integration removed |
integration.error | Integration token refresh failed |