ConvertKit - v1.0.0

Job Pipeline

ProcessKitFeed is the job the forms integration dispatches, one per feed per submission. It's the actual thing that hits Kit.

The public subscribe endpoint dispatches a different job, SubscribeToKit — same shape, slightly different responsibilities. This page covers ProcessKitFeed.

Constructor signature

new ProcessKitFeed(
    KitFeed $feed,
    array $payload,        // ['email_address' => ..., 'first_name' => ?, 'fields' => [...]]
    array $tagIds,         // int|string ids to apply
    int $submissionId,     // for event payloads / logging
);

The job is dispatchable via the Dispatchable trait:

ProcessKitFeed::dispatch( $feed, $payload, $tagIds, $submissionId );

Retry policy

  • public int $tries = 3;
  • public int $backoff = 30; (seconds)

Only retryable exceptions are re-thrown. Every other failure is marked terminal immediately via $this->fail($e) — retrying would just burn quota.

Exception Behavior
KitRateLimitException Re-thrown → queue worker retries after backoff.
KitServerException (5xx) Re-thrown → retried.
KitAuthException (bad key) Terminal — failed() fires.
KitValidationException (bad payload) Terminal.
KitNotFoundException (bad kit_form_id) Terminal.
Any other Throwable Terminal.

handle()

Two paths, chosen by whether the feed has a Kit form set:

With kit_form_id

$convertKit->forms()->subscribe(
    $this->feed->kit_form_id,
    $email,
    $fields,
    $this->tagIds,
);

One Kit API call. Tags are applied server-side as part of the subscribe. KitSubscribed fires on success.

Without kit_form_id

$subscriber = $convertKit->subscribers()->create( $email, $firstName, $fields );

foreach ( $this->tagIds as $tagId ) {
    if ( is_numeric( $tagId ) ) {
        $convertKit->subscribers()->tag( $subscriber->id, (int) $tagId );
    }
}

1 + N calls (create + one per tag). Non-numeric tag ids are skipped silently — they can't be a legal Kit tag id.

failed()

Fires KitSubscriptionFailed with the feed, payload, submission id, and exception:

public function failed( Throwable $exception ): void
{
    KitSubscriptionFailed::dispatch( $this->feed, $this->payload, $this->submissionId, $exception );
}

By design, KitSubscriptionFailed fires only from failed(), never inline from handle(). That way subscribers see exactly one event per terminal failure — no duplicates from retries.

Queue routing

The job reads convertkit.forms_integration.queue_connection and queue at construction time. This is baked into the dispatch payload — set once at boot, applies to every dispatch.

To route Kit work to a dedicated queue:

CONVERTKIT_QUEUE_CONNECTION=redis
CONVERTKIT_QUEUE=convertkit

Then run a dedicated worker: php artisan queue:work redis --queue=convertkit.

Choosing sync vs. database/redis/sqs

  • sync — the API call runs inside the HTTP request that dispatched it. If Kit is slow or down, your form submission is slow or down. Avoid in production for public-facing forms.
  • database — persistent, works everywhere Laravel does, no extra infra. Good starter choice.
  • redis / sqs — higher throughput. Recommended once you're doing more than a few subs a minute.

The retry policy is identical across drivers — retries live in the queue payload, not the driver.

Empty payloads

Defense in depth: if the payload arrives with an empty email_address, the job throws KitException: ProcessKitFeed received a payload with no email address and failed() fires immediately without calling Kit. In practice, FieldMapper catches this at map time and the listener never dispatches — but the job double-checks.

Events dispatched

  • KitSubscribed — on successful subscribe.
  • KitSubscriptionFailed — on terminal failure.

Neither is fired by the listener; both come from the job (or from failed()). See Events.

The public subscribe job

For completeness: SubscribeToKit (used by the public subscribe endpoint) has the same structure as ProcessKitFeed but takes primitives instead of a KitFeed. It also enforces a MAX_TAGS_PER_JOB = 50 internal cap on the tag-apply loop as defense in depth against a buggy dispatcher.