# RenderKit API

> Turn agent data into beautiful hosted HTML pages.
> Base URL: https://renderkit.live/v1

## Install (OpenClaw)

### OpenClaw Skill Install

1. Sign up for an API key:

```bash
curl -s -X POST https://renderkit.live/v1/auth/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"YOUR_EMAIL","password":"GENERATE_A_SECURE_PASSWORD"}'
```

Save the returned `api_key` — it's only shown once.

2. Create the skill directory and download the skill file:

```bash
mkdir -p ~/.openclaw/skills/renderkit
curl -s https://renderkit.live/skills/openclaw/SKILL.md \
  -o ~/.openclaw/skills/renderkit/SKILL.md
```

3. Create `~/.openclaw/skills/renderkit/skill.json`:

```json
{
  "name": "renderkit",
  "version": "1.0.0",
  "description": "Render data as beautiful web pages",
  "triggers": ["render", "visualize", "make a page", "show results"],
  "icon": "🎨"
}
```

4. Create `~/.openclaw/skills/renderkit/config.json` with your API key:

```json
{
  "api_key": "rk_live_YOUR_KEY_FROM_STEP_1",
  "api_base": "https://renderkit.live"
}
```

5. Verify it works:

```bash
curl -s https://renderkit.live/v1/account/me \
  -H "Authorization: Bearer rk_live_YOUR_KEY"
```

## Quick Start

POST /v1/render with your data. Get back a URL to a beautiful page.

```bash
curl -X POST https://renderkit.live/v1/render \
  -H "Authorization: Bearer rk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "template": "freeform",
    "context": "weekly AI news summary",
    "data": { "content": "# AI News\n\n..." },
    "format": "url"
  }'
```

## Authentication

All `/v1/render` and `/v1/account` endpoints require: `Authorization: Bearer <api_key>`

### Free Tier

Every API key gets **15 free renders**. After that, renders require payment via the x402 protocol ($0.10/render). Check your remaining free renders at `GET /v1/account/me`.

### Sign Up

Create an account and get your first API key.

```bash
curl -X POST https://renderkit.live/v1/auth/signup \
  -H "Content-Type: application/json" \
  -d '{ "email": "agent@example.com", "password": "your-password" }'
```

**Response 201:**
```json
{ "user_id": "abc123", "email": "agent@example.com", "api_key": "rk_live_XXXXXXXX..." }
```

Save the `api_key` — it's only shown once.

### Log In

```bash
curl -X POST https://renderkit.live/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{ "email": "agent@example.com", "password": "your-password" }'
```

**Response 200:**
```json
{ "user_id": "abc123", "email": "agent@example.com", "api_keys": [{ "prefix": "rk_live_XXXX", "name": "default", "created_at": "..." }] }
```

### Create Additional API Key

Requires authentication.

```bash
curl -X POST https://renderkit.live/v1/auth/keys \
  -H "Authorization: Bearer rk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "name": "my-agent" }'
```

**Response 201:**
```json
{ "api_key": "rk_live_YYYYYYYY...", "prefix": "rk_live_YYYY", "name": "my-agent" }
```

### Revoke an API Key

```bash
curl -X DELETE https://renderkit.live/v1/auth/keys/rk_live_XXXX \
  -H "Authorization: Bearer rk_live_..."
```

**Response:** 204 No Content

### Account Info

```bash
curl https://renderkit.live/v1/account/me \
  -H "Authorization: Bearer rk_live_..."
```

**Response 200:**
```json
{ "user_id": "abc123", "email": "agent@example.com", "created_at": "...", "api_keys": [...], "usage": { "total_renders": 12, "free_renders_remaining": 3, "last_30_days": 8 } }
```

### Usage History

```bash
curl "https://renderkit.live/v1/account/usage?page=1" \
  -H "Authorization: Bearer rk_live_..."
```

**Response 200:**
```json
{ "renders": [{ "render_id": "...", "template": "freeform", "status": "complete", "duration_ms": 4200, "created_at": "..." }], "total": 42, "page": 1 }
```

Paginated, 50 per page.

## Endpoints

### POST /v1/render

Creates a new rendered page.

| Field | Type | Required | Description |
|---|---|---|---|
| template | string | yes | `travel_itinerary`, `technical_spec`, `freeform` (also `product_comparison`, `technical_research` — legacy) |
| context | string | yes | Natural language description of what this content is |
| data | object | yes | Content to render. Include URLs inline — they are auto-enriched |
| format | string | no | `url` (default, hosted page), `html` (raw HTML string), `zip`, `pdf` |
| theme | object | no | `{ "mode": "light"/"dark", "colors": { "primary": "#hex" } }` |
| locale | string | no | ISO locale. Default: `en` |

**Response (sync):**
```json
{ "status": "complete", "render_id": "abc123", "slug": "sicily-adventure-7-days-abc12", "url": "https://renderkit.live/p/sicily-adventure-7-days-abc12" }
```

URLs use a human-readable slug derived from the title: `{slugified-title}-{first 5 chars of render_id}`. The `render_id` (nanoid) is still returned for PATCH/status calls. Both the slug and the render_id work in all endpoints (`/p/{id}`, `/v1/render/{id}`, `/v1/render/{id}/status`).

**Response (async — when URLs need enrichment):**
```json
{ "status": "processing", "render_id": "abc123", "slug": "sicily-adventure-7-days-abc12", "url": "https://renderkit.live/p/sicily-adventure-7-days-abc12", "poll_url": "https://renderkit.live/v1/render/abc123/status" }
```

### PATCH /v1/render/{id}

Updates an existing page **without changing its URL**. Use this for iterative work — expanding research, adding sections, or refining content that was already rendered. The `{id}` parameter accepts either a `render_id` or a `slug`.

| Field | Type | Required | Description |
|---|---|---|---|
| strategy | string | no | `"replace"` (swap all data, default) or `"merge"` (deep-merge new data into existing) |
| context | string | no | Updated description of the content |
| data | object | no | New or updated content |
| theme | object | no | Updated theme config |

**When to use PATCH vs POST:**
- **PATCH** — the user wants to refine, expand, or continue content that already has a page. The URL stays the same.
- **POST** — the user wants a brand-new page for a different topic.

```bash
curl -X PATCH https://renderkit.live/v1/render/abc123 \
  -H "Authorization: Bearer rk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "strategy": "merge",
    "context": "added restaurant recommendations to Paris trip",
    "data": {
      "restaurants": [
        { "name": "Le Comptoir", "link": "https://maps.google.com/..." }
      ]
    }
  }'
```

**Response 200:**
```json
{ "status": "complete", "render_id": "abc123", "slug": "sicily-adventure-7-days-abc12", "url": "https://renderkit.live/p/sicily-adventure-7-days-abc12" }
```

Returns `404` if the render doesn't exist, `409` if it's still processing.

### GET /v1/render/{id}/status

Poll for async render completion.

## Template: travel_itinerary

Best for: trip plans, day-by-day itineraries.
Auto-enriches: hotel/Airbnb links, Google Maps places, restaurants, flights.
Renders: day timeline, mini-maps, photo cards, budget tracker.

```json
{
  "template": "travel_itinerary",
  "context": "describe the trip",
  "data": {
    "title": "string",
    "travelers": 2,
    "dates": { "start": "YYYY-MM-DD", "end": "YYYY-MM-DD" },
    "budget": { "amount": 3000, "currency": "USD", "per": "total" },
    "days": [{
      "date": "YYYY-MM-DD",
      "theme": "day title",
      "items": [{
        "type": "flight|accommodation|activity|restaurant|transport",
        "title": "string",
        "time": { "start": "HH:MM", "end": "HH:MM" },
        "link": "any URL — auto-enriched",
        "price": { "amount": 0, "currency": "USD", "per": "night" },
        "note": "optional"
      }]
    }],
    "tips": ["string"],
    "packing_notes": "string"
  }
}
```

## Template: technical_spec

Best for: architecture docs, technical plans, specs, Claude plans, markdown-heavy planning documents.
Renders: section headers, code blocks with syntax highlighting, comparison tables, collapsible sections, todo lists, pros/cons, quote blocks.

```json
{
  "template": "technical_spec",
  "context": "technical architecture plan for a new authentication system",
  "data": {
    "title": "Auth System Architecture",
    "content": "# Overview\n\nThis document describes the authentication architecture...\n\n## API Endpoints\n\n| Method | Path | Description |\n|--------|------|-------------|\n| POST | /auth/login | User login |\n\n## Implementation\n\n```typescript\nasync function authenticate(token: string) {\n  // ...\n}\n```\n\n## Decision\n\n> Use JWT over session cookies for stateless scaling.\n\n## Trade-offs\n\n**Pros:** Stateless, scalable\n**Cons:** Token revocation complexity"
  }
}
```

The `data.content` field accepts a markdown string. The AI will parse headings, code fences, tables, and callouts into the appropriate visual components (SectionHeader, CodeBlock, ComparisonTable, QuoteBlock, etc.).

## Template: freeform

Best for: anything. AI picks the best layout.

```json
{
  "template": "freeform",
  "context": "describe what this content is",
  "data": {
    "content": "markdown string with inline URLs"
  }
}
```

Or use structured blocks:
```json
{
  "data": {
    "blocks": [
      { "type": "text", "content": "markdown" },
      { "type": "image", "url": "https://...", "caption": "..." },
      { "type": "quote", "text": "...", "attribution": "..." },
      { "type": "code", "language": "python", "content": "..." },
      { "type": "table", "columns": ["..."], "rows": [["..."]] }
    ]
  }
}
```

## Link Enrichment

Include URLs anywhere in your data. The API automatically:
1. Finds all URLs
2. Fetches metadata (images, ratings, prices)
3. Embeds rich previews in the output

Supported: Google Maps, GitHub, YouTube, and generic OG tags for everything else.

## Pay-per-render (x402)

RenderKit uses the [x402 payment protocol](https://www.x402.org/) for pay-per-render after the free tier.

- **Free tier:** Every API key gets 15 free renders. No payment required.
- **After free tier:** Authenticated requests that have exceeded the free limit get `402 Payment Required`. Include the `X-PAYMENT` header with a valid x402 payment to proceed.
- **No API key:** Unauthenticated requests always get `402 Payment Required`.
- **Static API keys:** Always exempt from payment.

### How it works

1. Make a `POST /v1/render` request without an API key
2. Receive a `402` response with payment requirements (price, network, wallet address)
3. Construct an x402 payment and include it in the `X-PAYMENT` header
4. Retry the request — the render proceeds as normal

### Pricing

The per-render price is configured server-side (default: `$0.10` USDC). No code changes are needed to adjust pricing.

## Skill Versioning

Every render and update response includes a `skill_version` field (e.g. `"1.4.0"`). Agents that cache the skill file (SKILL.md) should compare this version against the version in their cached frontmatter. If the API returns a newer version, re-fetch the skill file to get the latest instructions.

### GET /v1/skills/version

Public endpoint (no auth required). Returns the current skill version and the URL to fetch the latest skill file.

```bash
curl https://renderkit.live/v1/skills/version
```

**Response 200:**
```json
{ "version": "1.4.0", "skill_url": "https://renderkit.live/skills/openclaw/SKILL.md" }
```

## Forms

RenderKit also supports creating hosted forms for data collection. Forms share the same API key, auth, and quota as renders.

### POST /v1/forms

Creates a new hosted form. Provide either `fields` (explicit schema) or `prompt` (AI generates the fields).

| Field | Type | Required | Description |
|---|---|---|---|
| title | string | yes | Form title (max 200 chars) |
| description | string | no | Form description (max 2000 chars) |
| fields | array | either | Array of field definitions (1-50 fields). See Field Types below |
| prompt | string | either | Natural language description — AI generates the fields |
| multi_response | boolean | no | Allow multiple submissions (default: false) |
| expires_in | number | no | Seconds until form expires (60-2592000, default: 604800 = 7 days) |
| webhook_url | string | no | URL to POST responses to |
| webhook_secret | string | no | Secret for HMAC-SHA256 webhook signatures (min 16 chars) |
| branding | object | no | `{ "accent_color": "#hex", "logo_url": "https://...", "background_color": "#hex", "font_family": "..." }` |
| success_message | string | no | Custom thank-you message (max 500 chars) |
| redirect_url | string | no | Redirect after submission instead of showing thank-you page |

**Response 201:**
```json
{
  "form_id": "abc123",
  "slug": "event-rsvp-abc12",
  "url": "https://renderkit.live/f/event-rsvp-abc12",
  "title": "Event RSVP",
  "fields": [...],
  "ai_generated": false,
  "multi_response": false,
  "expires_at": "2025-01-15T00:00:00.000Z",
  "status": "active",
  "skill_version": "1.4.0"
}
```

#### AI-Generated Forms

Instead of defining fields manually, provide a `prompt`:

```bash
curl -X POST https://renderkit.live/v1/forms \
  -H "Authorization: Bearer rk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Customer Feedback",
    "prompt": "Create a feedback form for a restaurant. Include rating, food quality, service quality, and comments."
  }'
```

### GET /v1/forms/{id}

Get form metadata (owner only).

### DELETE /v1/forms/{id}

Close a form (soft delete). The form page shows "Form Closed" to visitors.

### GET /v1/forms/{id}/status

Poll for new submissions. Returns total and new response count since last poll.

### GET /v1/forms/{id}/responses

List all responses (paginated). Query params: `page` (default 1), `per_page` (default 50, max 100).

### GET /v1/forms/{id}/responses/{rid}

Get a single response.

### Field Types

17 field types are supported:

| Type | Description |
|---|---|
| text | Single-line text input |
| email | Email address with validation |
| number | Numeric input with min/max |
| tel | Phone number |
| url | URL with validation |
| textarea | Multi-line text |
| select | Dropdown (requires `options`) |
| multi_select | Multi-select dropdown (requires `options`) |
| checkbox | Single checkbox |
| radio | Radio buttons (requires `options`) |
| date | Date picker |
| time | Time picker |
| datetime | Date and time picker |
| file | File upload (10MB max, common MIME types) |
| rating | Star rating (1-5) |
| slider | Range slider with min/max/step |
| hidden | Hidden field with default value |

### Webhooks

When `webhook_url` is set, each submission POSTs a JSON payload to that URL with:
- `X-RenderKit-Event: response.created`
- `X-RenderKit-Signature: <HMAC-SHA256 hex>` (if `webhook_secret` is set)
- 3 retries with exponential backoff (0s, 2s, 8s)

Verify signatures:
```python
import hmac, hashlib
expected = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest()
assert expected == request.headers['X-RenderKit-Signature']
```

## Errors

```json
{ "error": "error_code", "message": "Human-readable description" }
```

HTTP codes: 200 success, 201 created, 400 bad request, 401 unauthorized, 402 payment required, 409 conflict, 429 rate limited, 500 server error.
