ConvertKit - v1.0.0
Public Subscribe
The endpoint front-end subscribe forms POST to. The actual Kit call happens on the queue, so this always returns 202 Accepted once validation passes.
POST /convertkit/subscribers
Auth: none by default (that's the point). Per-IP throttle applied on top of any configured middleware.
Route name: convertkit.subscribers.store.
Base prefix: convertkit (configurable via CONVERTKIT_SUBSCRIBE_PREFIX).
Two modes
The endpoint accepts either a feed_id (uses that feed's Kit form + tags) or a bare kit_form_id (skips the feed indirection). Exactly one of the two must be present — sending both is a validation error.
feed_id mode
{
"feed_id": 1,
"email": "jane@example.com",
"first_name": "Jane",
"tags": [ 100 ]
}
The controller resolves the feed (must be is_active = true), pulls its kit_form_id, merges its kit_tag_ids with any request-supplied tags, and dispatches the job. Missing first_name / fields / tags are fine — they're all optional.
kit_form_id mode
{
"kit_form_id": 12345,
"email": "jane@example.com",
"first_name": "Jane",
"fields": { "company": "Acme" },
"tags": [ 7 ]
}
Skips the feed lookup entirely. Useful for one-off forms where you don't want to persist a KitFeed row.
Validation rules
| Field | Rules |
|---|---|
feed_id |
required without kit_form_id, prohibits kit_form_id, integer, min:1 |
kit_form_id |
required without feed_id, integer, min:1 |
email |
required, valid email, max:255 |
first_name |
nullable, string, max:255 |
fields |
array, max:32 entries |
fields.* |
nullable, string, max:2048 |
tags |
array, max:20 |
tags.* |
integer, min:1 |
The fields and tags caps exist for a reason — see Security.
Success response
HTTP/1.1 202 Accepted
{ "message": "Subscribe queued." }
The actual Kit call runs on the worker. Success or failure surfaces via KitSubscribed or KitSubscriptionFailed events, not the HTTP response.
Rate limit response
HTTP/1.1 429 Too Many Requests
Standard Laravel throttle middleware response, including Retry-After and X-RateLimit-* headers.
Validation failure
HTTP/1.1 422 Unprocessable Entity
{
"message": "The email field must be a valid email address.",
"errors": {
"email": [ "The email field must be a valid email address." ]
}
}
Job pipeline
Dispatches SubscribeToKit. Signature:
new SubscribeToKit(
string $email,
?string $firstName,
array $fields,
array $tagIds,
?int $kitFormId,
);
Same retry policy as ProcessKitFeed — retries KitRateLimitException / KitServerException; every other exception marks the job failed immediately.
Defense-in-depth cap SubscribeToKit::MAX_TAGS_PER_JOB = 50 on the tag-apply loop. Even if a future dispatcher forgets to bound its input, the job will not fan out more than 50 API calls per subscribe.
Security
The endpoint is public — assume everyone can hit it. Guards:
- Per-IP throttle — default 10 attempts / minute per IP. Configurable but cannot be removed by the consumer (unless the consumer supplies their own
throttle:*insubscribe.middleware, in which case the package defers to it). tagscapped at 20 — prevents an attacker from triggering thousands of Kit API calls per request via a hugetagsarray.fieldscapped at 32 entries, eachstring|max:2048— prevents queue-DB bloat via nested payloads or 10MB blobs.feed_idinactive/unknown → generic 422 — the response is indistinguishable from any other validation failure, so 202 vs 422 does not act as a feed-id enumeration oracle.is_active = falsefeeds are treated as unknown. Toggle a feed off to instantly stop accepting subs via that feed_id.
Example — curl
curl -X POST https://your-app.test/convertkit/subscribers \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"feed_id": 1, "email": "jane@example.com"}'
Recipes
Full front-end examples in Subscribe Recipes:
Tuning the throttle
Loosen for a high-volume campaign:
CONVERTKIT_SUBSCRIBE_MAX_ATTEMPTS=60
CONVERTKIT_SUBSCRIBE_DECAY_MINUTES=1
Or route the endpoint behind your own limiter (e.g. Cloudflare rate limiting rules) and add your own throttle:* entry to convertkit.subscribe.middleware. The package will notice and skip prepending its own.
When to use feed_id vs kit_form_id
- Use
feed_idwhen you want the same admin/QA workflow you use for form-driven subs: create the feed once, dry-run it, toggle it on. Front-end code just references the feed id. - Use
kit_form_idwhen the front-end is calling Kit directly and you don't need the feed indirection. Kit form ids are essentially public (they're embedded in every Kit-hosted form URL), so leaking them via client JS is not a security concern.