For developers

Next.js integration

Wire your Ryterr account into a Next.js App Router site. Posts you schedule in Ryterr show up on your /blog page within 60 seconds, or instantly with webhook revalidation.

App Router · ~15 min · runs on Node, Edge, and Cloudflare Workers

Overview

Your /blog reads posts from the Content API (cached with ISR), and a webhook route revalidates the affected pages the instant a post publishes. Add a schedule and it runs itself.

1

Create an API key

In your Ryterr dashboard, open Settings > API keys and click New API key.

For the recommended setup, choose Webhook. Add your receiver URL, click Create key, then copy the key Ryterr shows you into your site as RYTERR_API_KEY. Ryterr generates the key for you and displays it once, so copy it before closing the dialog. The key reads posts through the Content API and verifies webhook signatures for this connection.

If your site only polls Ryterr and does not need instant revalidation, choose Content API instead. Lost a key? Use Rotate to issue a new one; the old value stops working immediately.

2

Install the SDK

The official @ryterr/client package provides TypeScript types and helpers for the Content API and webhook verification.

terminal·Shell
pnpm add @ryterr/client
# or npm install @ryterr/client
# or yarn add @ryterr/client

The SDK is published on npm and requires no special account to install. Plain fetch works too if you prefer zero dependencies.

Set it up with an AI agent (Cursor, Claude, v0, ...)

Paste this into your coding agent. It outputs the receiver, env, and fetch code for a complete Next.js setup.

ryterr-setup-prompt.md·markdown
You are an expert Next.js (App Router) developer. Integrate Ryterr (https://ryterr.com), an AI blog platform that auto-publishes research-backed, fact-checked posts, into the user's site so new posts appear on /blog automatically: instantly via webhook, with the Content API as the on-demand source.

Dashboard setup (have the user do this once):
- Ryterr -> Settings -> API keys -> New API key -> choose "Webhook". Set the receiver URL to https://THEIR-SITE/api/ryterr-webhook. Copy the key (shown once) into env as RYTERR_API_KEY. The same key also authenticates the Content API.

Env (.env.local and production):
  RYTERR_API_KEY=rytk_live_...
  RYTERR_API_BASE=https://ryterr.com

Create these files (output complete, production-ready code with error handling):
1. app/api/ryterr-webhook/route.ts - call verifyWebhook(req, process.env.RYTERR_API_KEY) from "@ryterr/client". On post.published / post.updated / post.deleted, revalidatePath("/blog") and the affected /blog/SLUG. Return 401 on InvalidSignatureError.
2. app/blog/page.tsx - fetch RYTERR_API_BASE + "/api/v1/content/posts?limit=20" with header "Authorization: Bearer " + RYTERR_API_KEY, set "export const revalidate = 60", and list the posts (response shape: { data: [...] }).
3. app/blog/[slug]/page.tsx - fetch "/api/v1/content/posts/" + slug. Render post.html (already sanitized server-side by Ryterr) with dangerouslySetInnerHTML, featuredImageUrl above it. 404 when missing.

Install: pnpm add @ryterr/client  (or use plain fetch + Web Crypto for zero dependencies).

Verify: use Ryterr's "Send test event" button on the connection and confirm the receiver returns 2xx and the page revalidates.

Use only the public Content API contract: cursor-paginated list, single post by slug or id, payload has sanitized html + markdown + metadata. Full guide: https://ryterr.com/integrations/nextjs
3

Add the API key to .env

Make sure .env.local is already in your .gitignore.

.env.local·Env
RYTERR_API_KEY=rytk_live_...
RYTERR_API_BASE=https://ryterr.com
4

Build the blog index

Fetch posts published to this connection. ISR caches for 60 seconds; the webhook in step 6 invalidates that cache on demand.

app/blog/page.tsx·TypeScript
// app/blog/page.tsx
const RYTERR_API_BASE = process.env.RYTERR_API_BASE ?? "https://ryterr.com"

// ISR: refresh every 60 seconds. The webhook below makes this
// instant when it fires successfully.
export const revalidate = 60

export default async function BlogIndex() {
  const res = await fetch(`${RYTERR_API_BASE}/api/v1/content/posts?limit=20`, {
    headers: {
      Authorization: `Bearer ${process.env.RYTERR_API_KEY!}`,
    },
    next: { revalidate: 60 },
  })
  if (!res.ok) throw new Error("Failed to load Ryterr posts")

  const { data: posts } = await res.json()

  return (
    <main>
      <h1>Blog</h1>
      <ul>
        {posts.map((p) => (
          <li key={p.id}>
            <a href={`/blog/${p.slug}`}>{p.title}</a>
            <p>{p.excerpt}</p>
          </li>
        ))}
      </ul>
    </main>
  )
}
5

Build the post detail page

Fetch one post by slug. post.html includes body HTML and inline images; post.body is raw markdown if you prefer to render it yourself.

app/blog/[slug]/page.tsx·TypeScript
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation"

const RYTERR_API_BASE = process.env.RYTERR_API_BASE ?? "https://ryterr.com"

export const revalidate = 60
export const dynamicParams = true

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const res = await fetch(
    `${RYTERR_API_BASE}/api/v1/content/posts/${encodeURIComponent(slug)}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.RYTERR_API_KEY!}`,
      },
      next: { revalidate: 60 },
    }
  )
  if (res.status === 404) notFound()
  if (!res.ok) throw new Error("Failed to load Ryterr post")

  const { data: post } = await res.json()
  if (!post) notFound()

  return (
    <article>
      <h1>{post.title}</h1>
      {post.featuredImageUrl ? (
        <img
          src={post.featuredImageUrl}
          alt={post.featuredImageAlt ?? post.title}
        />
      ) : null}
      {/* post.html is sanitized server-side by Ryterr (scripts, on*
          handlers, and javascript: URLs are stripped), so it is safe
          to render directly. Add your own sanitizer for defense in
          depth if you like. */}
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  )
}

post.html is sanitized server-side before Ryterr sends it: <script>, inline event handlers, and javascript: URLs are stripped, so it is safe to render directly. If you prefer defense in depth, run it through your own sanitizer too.

6

Add the webhook receiver

Without this, new posts take up to 60 seconds (the ISR window) to appear. With it, Ryterr sends a POST to your endpoint the instant a post is ready and your blog refreshes within a request.

app/api/ryterr-webhook/route.ts·TypeScript
// app/api/ryterr-webhook/route.ts
import { verifyWebhook, InvalidSignatureError } from "@ryterr/client"
import { revalidatePath } from "next/cache"

export async function POST(req: Request) {
  try {
    const event = await verifyWebhook(req, process.env.RYTERR_API_KEY!)

    if (event.type === "post.published") {
      revalidatePath("/blog")
      revalidatePath(`/blog/${event.data.post.slug}`)
    }
    if (event.type === "post.updated") {
      revalidatePath("/blog")
      revalidatePath(`/blog/${event.data.post.slug}`)
      if (event.data.previousSlug) {
        revalidatePath(`/blog/${event.data.previousSlug}`)
      }
    }
    if (event.type === "post.deleted") {
      revalidatePath("/blog")
      revalidatePath(`/blog/${event.data.slug}`)
    }

    return Response.json({ ok: true })
  } catch (err) {
    if (err instanceof InvalidSignatureError) {
      return new Response("bad signature", { status: 401 })
    }
    throw err
  }
}

In Ryterr, your Webhook key should point at https://your-site.com/api/ryterr-webhook and use the same API key. Open Settings > Connections and click Send test event to verify your receiver works before relying on real posts.

7

Deploy + schedule

Ship your site (Vercel, Netlify, VPS, anywhere Next.js runs).

In Ryterr, go to Settings > Schedules and create a schedule with auto-publish targeted at your webhook connection. Pick a cadence and a queue of topics. That is it. Your blog now updates itself.

Delivery guarantees

Built like a payments API. The boring details that keep your blog in sync.

  • At-least-once delivery with exponential backoff: 1m, 5m, 30m, 2h, 12h.
  • HMAC signature verification with timestamp replay protection.
  • Idempotency keys on every event. Receivers de-dupe automatically.
  • 10-second timeout per attempt with replay protection (5-min timestamp window).
  • Dead-letter notifications if your endpoint stays down too long.
  • 100 req/min rate limit per API key with proper Retry-After headers.
  • Cached responses (60s s-maxage) on the Content API for hot blog pages.

Next steps

Other runtimes, sample receivers, and the full API reference.