Documentation
REST API
On this page
Overview
The wisp REST API provides programmatic access to create, update, and delete blog posts on your blog without using the web interface. This is perfect for:
- Automating content publishing workflows
- Integrating with external CMS or publishing tools
- Building custom content management applications
- Syncing content from other platforms
Paid Feature
REST API access is only available on paid plans. Free plan users will need to upgrade their subscription to use API keys.
Getting Started
1. Create an API Key
To use the REST API, you'll first need to create an API key:
- Navigate to Settings → API Keys in your wisp dashboard
- Click "New Key" button
- Enter a descriptive name (e.g., "Production Key", "CI/CD Pipeline")
- Click "Create Key"
- Copy your API key - it will be displayed in full and can be copied anytime
Security Best Practices
- • Keep your API keys secure and never commit them to version control
- • Use environment variables to store API keys in your applications
- • Create separate keys for different environments (development, staging, production)
- • Regularly rotate API keys and delete unused keys
2. Authentication
All REST API requests must include your API key in the Authorization header:
Authorization: Bearer wisp_your_api_key_here
Base URL
https://www.wisp.blog/api/rest/v1
Endpoints
Blog Posts
Create a Post
Creates a new blog post.
Endpoint: POST /posts
Request Body:
{
"title": "My New Blog Post",
"content": "<p>This is the content of my blog post with HTML formatting.</p>",
"slug": "my-new-blog-post",
"description": "A brief description of the post",
"publishedAt": "2025-12-10T00:00:00Z",
"image": "https://example.com/image.jpg",
"metadata": "{\"author\": \"John Doe\"}",
"tagIds": ["tag-id-1", "tag-id-2"]
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | Yes | The title of the blog post |
content | string | Yes | HTML content of the post |
slug | string | Yes | URL-friendly identifier for the post |
description | string | No | Short description/excerpt |
publishedAt | string (ISO 8601) | No | Publication date. If null, post is a draft |
image | string | No | URL of the featured image |
metadata | string (JSON) | No | Additional metadata as JSON string (max 512 chars) |
tagIds | array of strings | No | Array of tag IDs to associate with the post |
Example Request:
curl -X POST https://www.wisp.blog/api/rest/v1/posts \
-H "Authorization: Bearer wisp_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"title": "Getting Started with wisp",
"content": "<h2>Introduction</h2><p>Welcome to wisp!</p>",
"slug": "getting-started-with-wisp",
"description": "Learn the basics of wisp",
"publishedAt": "2025-12-10T00:00:00Z"
}'
Response (201 Created):
{
"id": "clx123456789",
"title": "Getting Started with wisp",
"content": "<h2>Introduction</h2><p>Welcome to wisp!</p>",
"slug": "getting-started-with-wisp",
"description": "Learn the basics of wisp",
"image": null,
"metadata": null,
"authorId": "usr_123456789",
"teamId": "team_123456789",
"publishedAt": "2025-12-10T00:00:00.000Z",
"createdAt": "2025-12-10T10:30:00.000Z",
"updatedAt": "2025-12-10T10:30:00.000Z"
}
List Posts
Retrieves a paginated list of blog posts.
Endpoint: GET /posts
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number for pagination |
limit | number | 10 | Number of posts per page |
tag | string | - | Filter by tag name (can be specified multiple times) |
query | string | - | Search query for title or content |
Example Request:
curl -X GET "https://www.wisp.blog/api/rest/v1/posts?page=1&limit=20" \
-H "Authorization: Bearer wisp_your_api_key_here"
Response (200 OK):
{
"posts": [
{
"id": "clx123456789",
"title": "Getting Started with wisp",
"slug": "getting-started-with-wisp",
"description": "Learn the basics of wisp",
"image": null,
"publishedAt": "2025-12-10T00:00:00.000Z",
"createdAt": "2025-12-10T10:30:00.000Z",
"updatedAt": "2025-12-10T10:30:00.000Z",
"author": {
"name": "John Doe",
"email": "john@example.com"
},
"tags": [
{
"tag": {
"id": "tag_123",
"name": "Tutorial"
}
}
]
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 50,
"totalPages": 3
}
}
Get a Post
Retrieves a single blog post by slug.
Endpoint: GET /posts/:slug
Example Request:
curl -X GET "https://www.wisp.blog/api/rest/v1/posts/getting-started-with-wisp" \
-H "Authorization: Bearer wisp_your_api_key_here"
Response (200 OK):
{
"id": "clx123456789",
"title": "Getting Started with wisp",
"content": "<h2>Introduction</h2><p>Welcome to wisp!</p>",
"slug": "getting-started-with-wisp",
"description": "Learn the basics of wisp",
"image": null,
"metadata": null,
"publishedAt": "2025-12-10T00:00:00.000Z",
"createdAt": "2025-12-10T10:30:00.000Z",
"updatedAt": "2025-12-10T10:30:00.000Z",
"author": {
"name": "John Doe",
"email": "john@example.com"
},
"tags": [
{
"tag": {
"id": "tag_123",
"name": "Tutorial"
}
}
]
}
Update a Post
Updates an existing blog post.
Endpoint: PUT /posts/:slug
Request Body: Same as Create Post
Example Request:
curl -X PUT "https://www.wisp.blog/api/rest/v1/posts/getting-started-with-wisp" \
-H "Authorization: Bearer wisp_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"title": "Getting Started with wisp (Updated)",
"content": "<h2>Introduction</h2><p>Welcome to wisp! This is updated content.</p>",
"slug": "getting-started-with-wisp",
"description": "Updated description",
"publishedAt": "2025-12-10T00:00:00Z"
}'
Response (200 OK): Same as Get Post response
Delete a Post
Deletes a blog post by slug.
Endpoint: DELETE /posts/:slug
Example Request:
curl -X DELETE "https://www.wisp.blog/api/rest/v1/posts/getting-started-with-wisp" \
-H "Authorization: Bearer wisp_your_api_key_here"
Response (200 OK):
{
"success": true
}
Image Uploads
Get Signed Upload URL
Retrieves a signed URL for uploading images to wisp's storage.
Endpoint: POST /uploads/signed-url
Request Body:
{
"path": "my-image.jpg"
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | Yes | The filename/path for the image (e.g., "blog-image.jpg") |
Example Request:
curl -X POST "https://www.wisp.blog/api/rest/v1/uploads/signed-url" \
-H "Authorization: Bearer wisp_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"path": "hero-image.jpg"}'
Response (200 OK):
{
"uploadURL": "https://upload.imagedelivery.net/..."
}
Upload Flow:
- Call the signed URL endpoint to get an upload URL
- POST your image file to the returned
uploadURL - Extract the image URL from the upload response
- Use that URL in your blog post's
imagefield
Complete Example:
# Step 1: Get signed upload URL
SIGNED_URL=$(curl -X POST "https://www.wisp.blog/api/rest/v1/uploads/signed-url" \
-H "Authorization: Bearer wisp_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"path": "hero.jpg"}' | jq -r '.uploadURL')
# Step 2: Upload image to signed URL
IMAGE_RESPONSE=$(curl -X POST "$SIGNED_URL" \
-F "file=@./hero.jpg" \
-H "accept: application/json")
# Step 3: Extract image URL
IMAGE_URL=$(echo $IMAGE_RESPONSE | jq -r '.result.variants[0]')
# Step 4: Create post with image
curl -X POST "https://www.wisp.blog/api/rest/v1/posts" \
-H "Authorization: Bearer wisp_your_api_key_here" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"Post with Image\",
\"content\": \"<p>Content here</p>\",
\"slug\": \"post-with-image\",
\"image\": \"$IMAGE_URL\"
}"
Error Handling
The API returns standard HTTP status codes:
| Status Code | Description |
|---|---|
200 | Success |
201 | Created successfully |
400 | Bad request - Invalid input |
401 | Unauthorized - Invalid or missing API key |
403 | Forbidden - Free plan or restricted account |
404 | Not found - Resource doesn't exist |
409 | Conflict - Duplicate slug |
500 | Internal server error |
Error Response Format:
{
"error": "Error message describing what went wrong",
"details": {} // Optional additional error details
}
Common Errors:
- Invalid API key: Check that your API key is correct and included in the Authorization header
- Free plan restriction: Upgrade to a paid plan to use the REST API
- Duplicate slug: The slug already exists for another post in your blog
- Invalid metadata: Ensure metadata is valid JSON if provided
Rate Limits
API requests are currently limited to:
- 100 requests per minute per API key
If you exceed the rate limit, you'll receive a 429 Too Many Requests response.
Best Practices
1. Use Descriptive Slugs
Slugs should be URL-friendly and descriptive:
- ✅
getting-started-with-nextjs - ❌
post-1or123abc
2. Handle Errors Gracefully
Always check response status codes and handle errors appropriately:
const response = await fetch("https://www.wisp.blog/api/rest/v1/posts", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
});
if (!response.ok) {
const error = await response.json();
console.error("API Error:", error.error);
// Handle error appropriately
}
const post = await response.json();
3. Use Environment Variables
Store API keys securely:
// .env
WISP_API_KEY = wisp_your_api_key_here;
// In your code
const apiKey = process.env.WISP_API_KEY;
4. Draft Before Publishing
Create posts as drafts first by omitting publishedAt, then update them to publish:
// Create draft
const draft = await createPost({
title,
content,
slug,
publishedAt: null,
});
// Publish later
await updatePost(slug, {
...draft,
publishedAt: new Date().toISOString(),
});
SDK Examples
Node.js / TypeScript
import fetch from "node-fetch";
const WISP_API_KEY = process.env.WISP_API_KEY;
const BASE_URL = "https://www.wisp.blog/api/rest/v1";
async function createPost(data: {
title: string;
content: string;
slug: string;
description?: string;
publishedAt?: string;
}) {
const response = await fetch(`${BASE_URL}/posts`, {
method: "POST",
headers: {
Authorization: `Bearer ${WISP_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
return await response.json();
}
async function listPosts(page = 1, limit = 10) {
const response = await fetch(
`${BASE_URL}/posts?page=${page}&limit=${limit}`,
{
headers: {
Authorization: `Bearer ${WISP_API_KEY}`,
},
},
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
return await response.json();
}
// Usage
const newPost = await createPost({
title: "My New Post",
content: "<p>Hello World</p>",
slug: "my-new-post",
publishedAt: new Date().toISOString(),
});
const posts = await listPosts(1, 20);
Python
import requests
import os
from typing import Optional, Dict, Any
WISP_API_KEY = os.getenv('WISP_API_KEY')
BASE_URL = 'https://www.wisp.blog/api/rest/v1'
def create_post(
title: str,
content: str,
slug: str,
description: Optional[str] = None,
published_at: Optional[str] = None
) -> Dict[str, Any]:
response = requests.post(
f'{BASE_URL}/posts',
headers={
'Authorization': f'Bearer {WISP_API_KEY}',
'Content-Type': 'application/json',
},
json={
'title': title,
'content': content,
'slug': slug,
'description': description,
'publishedAt': published_at,
}
)
response.raise_for_status()
return response.json()
def list_posts(page: int = 1, limit: int = 10) -> Dict[str, Any]:
response = requests.get(
f'{BASE_URL}/posts',
headers={'Authorization': f'Bearer {WISP_API_KEY}'},
params={'page': page, 'limit': limit}
)
response.raise_for_status()
return response.json()
# Usage
new_post = create_post(
title='My New Post',
content='<p>Hello World</p>',
slug='my-new-post',
published_at='2025-12-10T00:00:00Z'
)
posts = list_posts(page=1, limit=20)
Use Cases
Automated Publishing
Schedule and publish content automatically:
// Publish scheduled posts
async function publishScheduledPosts() {
const drafts = await listPosts();
const now = new Date();
for (const post of drafts.posts) {
if (!post.publishedAt && shouldPublish(post)) {
await updatePost(post.slug, {
...post,
publishedAt: now.toISOString(),
});
console.log(`Published: ${post.title}`);
}
}
}
Content Migration
Import content from another platform:
// Migrate posts from another CMS
async function migrateFromOtherCMS(posts) {
for (const oldPost of posts) {
try {
await createPost({
title: oldPost.title,
content: convertToHTML(oldPost.body),
slug: sanitizeSlug(oldPost.slug),
description: oldPost.excerpt,
publishedAt: oldPost.published_date,
});
console.log(`Migrated: ${oldPost.title}`);
} catch (error) {
console.error(`Failed to migrate: ${oldPost.title}`, error);
}
}
}
CI/CD Integration
Publish documentation from your codebase:
# .github/workflows/publish-docs.yml
name: Publish Documentation
on:
push:
branches: [main]
paths:
- "docs/**"
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Publish to wisp
env:
WISP_API_KEY: ${{ secrets.WISP_API_KEY }}
run: |
node scripts/publish-docs.js