Joyful Docs

API

Build integrations and automate workflows. Pro feature.

Getting started

The Joyful API lets you manage publications, newsletters, subscribers, and more programmatically.

  1. Go to Settings → API Keys
  2. Click Create API Key and give it a name
  3. Copy the key immediately — you won't see it again

Authentication

All requests require a Bearer token in the Authorization header:

curl -H "Authorization: Bearer jf_your_api_key" \
     https://joyful.to/api/publications

API keys start with jf_ and provide full access to your account.

Keep your keys secure. Don't expose them in client-side code or public repositories.

Rate limits

Each API key is limited to 1,000 requests per hour. Rate limit info is included in response headers:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1702425600

If you exceed the limit, you'll get a 429 Too Many Requests response.

Headless CMS

Use Joyful as a headless CMS to power your own website or app.

Setup

  1. Go to Publication Settings → Advanced
  2. Set Visibility to "API Only" to hide content from joyful.to
  3. Optionally set a Canonical Domain (e.g., myblog.com/posts) for SEO

Fetching content

List newsletters for a publication:

curl -H "Authorization: Bearer jf_your_api_key" \
     https://joyful.to/api/publications/your-pub-slug/newsletters

Response includes metadata for each newsletter:

{
  "newsletters": [
    {
      "id": "nl_xyz789",
      "slug": "my-post",
      "title": "My Newsletter Post",
      "description": "A brief summary...",
      "coverImage": "https://...",
      "published": true,
      "sentAt": "2024-01-20T..."
    }
  ],
  "pagination": { "total": 42, "limit": 50, "offset": 0, "hasMore": false }
}

Get full content for a single newsletter:

curl -H "Authorization: Bearer jf_your_api_key" \
     https://joyful.to/api/newsletters/nl_xyz789

Response includes rendered HTML:

{
  "newsletter": {
    "id": "nl_xyz789",
    "slug": "my-post",
    "title": "My Newsletter Post",
    "html": "<p>Your rendered content...</p>",
    ...
  }
}

Canonical URLs

When you set a canonical domain, newsletter pages on joyful.to include:

<link rel="canonical" href="https://your-domain.com/your-newsletter-slug" />

This tells search engines your site is the primary source, preventing duplicate content issues.

Endpoints

Publications

MethodEndpointDescription
GET/api/publicationsList all publications
POST/api/publicationsCreate a publication
GET/api/publications/:idGet a publication
PUT/api/publications/:idUpdate a publication
DELETE/api/publications/:idDelete a publication
GET/api/publications/:id/newslettersList newsletters for a publication
GET/api/publications/:id/tagsList tags for a publication

Newsletters

MethodEndpointDescription
GET/api/newslettersList all newsletters
POST/api/newslettersCreate a newsletter
GET/api/newsletters/:idGet a newsletter
PUT/api/newsletters/:idUpdate a newsletter
DELETE/api/newsletters/:idDelete a newsletter

Subscribers

MethodEndpointDescription
GET/api/subscribersList all subscribers
POST/api/subscribersAdd a subscriber
GET/api/subscribers/:idGet a subscriber
PUT/api/subscribers/:idUpdate a subscriber
DELETE/api/subscribers/:idRemove a subscriber

Templates

MethodEndpointDescription
GET/api/templatesList all templates
POST/api/templatesCreate a template
GET/api/templates/:idGet a template
DELETE/api/templates/:idDelete a template

Stats

MethodEndpointDescription
GET/api/statsOverall statistics
GET/api/stats/:newsletterIdStats for a newsletter

Examples

JavaScript

const API_KEY = 'jf_your_api_key';

// List all subscribers
const response = await fetch('https://joyful.to/api/subscribers', {
  headers: {
    'Authorization': `Bearer ${API_KEY}`
  }
});
const data = await response.json();
console.log(data.subscribers);

// Add a subscriber
const newSubscriber = await fetch('https://joyful.to/api/subscribers', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    email: 'reader@example.com',
    firstName: 'Jane',
    publicationId: 'pub_abc123'
  })
});

Python

import requests

API_KEY = 'jf_your_api_key'
BASE_URL = 'https://joyful.to/api'

headers = {
    'Authorization': f'Bearer {API_KEY}',
    'Content-Type': 'application/json'
}

# List all subscribers
response = requests.get(f'{BASE_URL}/subscribers', headers=headers)
subscribers = response.json()['subscribers']

# Add a subscriber
new_subscriber = requests.post(
    f'{BASE_URL}/subscribers',
    headers=headers,
    json={
        'email': 'reader@example.com',
        'firstName': 'Jane',
        'publicationId': 'pub_abc123'
    }
)

Responses

All responses are JSON. Success responses contain the requested data:

{
  "publications": [
    {
      "id": "pub_abc123",
      "name": "My Newsletter",
      "slug": "my-newsletter"
    }
  ]
}

Errors include a message:

{
  "error": "Rate limit exceeded"
}