ConvertKit (Kit)

Lightweight, dependency-free, in-memory fake of the ConvertKit (Kit) v3 HTTP REST API for testing application code that talks to ConvertKit directly with the axios HTTP client (the documented integration path). Speaks the exact plain-JSON wire protocol the real service uses, with zero cost and zero side effects. State is in-memory, ephemeral, and resettable.

Default port: 4667

Quick start

Start the server:

import { ConvertkitServer } from "./services/convertkit/src/server.js";

const server = new ConvertkitServer(4667);
await server.start();
// ... run your app / tests ...
await server.stop();

Point the real axios client at it. ConvertKit's v3 API lives under /v3 and authenticates with an api_key (public) or api_secret (private), supplied either as a query-string parameter or a JSON body field — exactly as the real API accepts them:

import axios from "axios";

const ck = axios.create({
  baseURL: "http://127.0.0.1:4667/v3", // point at the parlel fake
  headers: { "Content-Type": "application/json" },
});

// Public endpoint: subscribe an email to a form (api_key in the body)
const { data } = await ck.post(`/forms/${formId}/subscribe`, {
  api_key: process.env.CONVERTKIT_API_KEY,
  email: "ada@parlel.test",
  first_name: "Ada",
  fields: { city: "London" },
});
// data.subscription.subscriber.id => a generated subscriber id

// Private endpoint: list subscribers (api_secret as a query param)
const res = await ck.get("/subscribers", {
  params: { api_secret: process.env.CONVERTKIT_API_SECRET },
});
// res.data.total_subscribers, res.data.subscribers[]

Authentication

CredentialUsed forSupplied as
api_key (public)Listing forms/sequences/tags/custom fields, subscribe endpointsquery param or JSON body field
api_secret (private)Subscriber data, broadcasts, purchases, webhooks, tag/custom-field writes, subscriptions listsquery param or JSON body field

Default credentials (from manifest.json):

A missing/wrong credential returns 401 with { "error": "Authorization Failed", "message": "..." }. Supplying only api_key to a secret-only endpoint also returns 401.

Implemented operations

Account (api_secret)

Forms

Sequences (a.k.a. Courses)

Tags

Subscribers

Custom Fields

Broadcasts (api_secret)

Webhooks (api_secret)

Purchases (api_secret)

parlel control / inspection (unauthenticated)

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

FeatureStatus
Forms list + subscribe + subscriptions✅ Supported
Sequences (courses) list + subscribe + subscriptions✅ Supported
Tags CRUD (create/list/subscribe/unsubscribe/subscriptions)✅ Supported
Subscribers list/get/update/unsubscribe/tags✅ Supported
Custom fields CRUD✅ Supported
Broadcasts CRUD + stats✅ Supported
Webhooks (automations/hooks) create + delete✅ Supported
Purchases create/list/get✅ Supported
Account / creator profile / growth stats✅ Supported
api_key / api_secret auth (query param or body)✅ Supported
Email-based subscriber upsert across all subscribe paths✅ Supported
Pagination envelopes (page, total_pages, total_*)✅ Supported (single-page, deterministic)
Real email delivery / sending broadcasts⟳ Roadmap — Not supported (no side effects)
OAuth 2.0 / Kit v4 API surface⟳ Roadmap — Not supported (this fake targets v3)
Real webhook delivery to target_url⟳ Roadmap — Not supported (stored only)
Rate limiting / 429 throttling✓ By design — Never throttles — local tests run at full speed, zero cost
Stats analytics (real open/click rates)✓ By design — Intentional for a local, zero-cost test emulator

Error codes & shapes

Errors use the ConvertKit error envelope:

{ "error": "<Title>", "message": "<detail>" }
StatusWhen
200Successful read/subscribe/update
201Resource created (tags, custom fields, broadcasts, purchases)
204Successful delete / custom-field update (no body)
400Bad Request — invalid email, blank name/label, missing target_url, malformed JSON
401Authorization Failed — missing/invalid api_key or api_secret
404Not Found — unknown resource id or path
405Method Not Allowed — unsupported method on a known resource
422Unprocessable Entity — invalid purchase payload (missing transaction_id, invalid email)
500Internal Server Error — unexpected failure

All responses set Content-Type: application/json; charset=utf-8 and permissive CORS headers, and carry a server: parlel-convertkit header.

<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

CONVERTKIT_API_KEY=parlel_test_public_api_key
CONVERTKIT_API_SECRET=parlel_test_secret_api_key
CONVERTKIT_BASE_URL=http://parlel-bridge:4667
CONVERTKIT_API_BASE_URL=http://parlel-bridge:4667/v3
<!-- parlel:testenv:end -->