Docs

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/v1

Content 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/spaces

Creating API keys

API keys are scoped to your organization and can be created in Settings > API Keys.

Key typeScopeUse case
OrganizationAll spacesAdmin automation, CI/CD
SpaceSingle spaceScoped integrations
Read-onlyGET requests onlyMonitoring, dashboards

Key rotation

API keys can be rotated without downtime:

  1. Create a new key
  2. Update your applications to use the new key
  3. Revoke the old key

Both keys remain valid during the transition period.

Spaces

List spaces

GET /spaces

Query parameters:

ParameterTypeDescription
limitnumberMax results (default: 20, max: 100)
offsetnumberPagination offset
statusstringFilter 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 /spaces

Request body:

{
  "name": "Product Team",
  "description": "Product management workflows",
  "persona": "product-manager",
  "model": "sonnet"
}

Get space details

GET /spaces/:id

Returns full space information including integrations, persona config, and statistics.

Update a space

PATCH /spaces/:id

Updatable fields: name, description, persona, model, status

Delete a space

DELETE /spaces/:id

Initiates a 30-day grace period. The space is immediately archived and permanently deleted after 30 days.

Messages

Send a message

POST /spaces/:id/messages

Request body:

{
  "content": "Summarize all open PRs in the auth-service repo",
  "stream": true
}

Parameters:

ParameterTypeDescription
contentstringThe message to send to the agent
streambooleanEnable SSE streaming (default: false)
contextobjectAdditional 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/messages

Query parameters:

ParameterTypeDescription
limitnumberMax results (default: 50)
beforestringISO timestamp for pagination
afterstringISO timestamp for pagination

Smart Actions

Trigger an action

POST /spaces/:id/actions

Request body:

{
  "prompt": "Generate a weekly report of all merged PRs",
  "output": {
    "type": "slack",
    "channel": "#engineering-updates"
  }
}

Get action status

GET /spaces/:id/actions/:actionId

Response:

{
  "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

StatusDescription
pendingQueued, not yet started
planningLLM is generating the execution plan
runningExecuting tool calls
completedAll steps finished successfully
failedOne or more steps failed after retries
cancelledCancelled by user or timeout

Integrations

List integrations

GET /spaces/:id/integrations

Connect an integration

POST /spaces/:id/integrations

Returns an OAuth authorization URL. Redirect the user to complete the flow.

Disconnect an integration

DELETE /spaces/:id/integrations/:provider

Revokes 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

CodeHTTP StatusDescription
unauthorized401Invalid or missing API key
forbidden403Key doesn't have required scope
not_found404Resource doesn't exist
rate_limit_exceeded429Too many requests
internal_error500Server error (retry safe)
integration_error502Upstream integration failed

Rate limits

PlanRequests/minuteConcurrent actions
Standard1005
Pro1,00025
EnterpriseCustomCustom

Rate limit headers

Every response includes:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1706803200

Handling 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

EventDescription
action.completedA Smart Action finished successfully
action.failedA Smart Action failed after retries
space.updatedSpace settings changed
integration.connectedNew integration connected
integration.disconnectedIntegration removed
integration.errorIntegration token refresh failed