PHPackages                             reachweb/statamic-keila-integration - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. reachweb/statamic-keila-integration

ActiveLibrary

reachweb/statamic-keila-integration
===================================

v1.0.0(yesterday)01↑2900%PHPPHP ^8.3

Since Jun 22Pushed yesterdayCompare

[ Source](https://github.com/reachweb/statamic-keila-integration)[ Packagist](https://packagist.org/packages/reachweb/statamic-keila-integration)[ RSS](/packages/reachweb-statamic-keila-integration/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (2)Versions (2)Used By (0)

Statamic Keila Integration
==========================

[](#statamic-keila-integration)

Subscribe Statamic form submitters to a self-hosted [Keila](https://www.keila.io) newsletter instance via Keila's HTTP API.

When a mapped form is submitted with its opt-in toggle accepted, the addon creates a new Keila contact or updates an existing one, and tags it so it falls into a Keila segment. It is a **non-interfering side effect**: the native Statamic submission (storage + notification emails) always completes unchanged, regardless of what happens with Keila.

Requirements
------------

[](#requirements)

- Statamic 6, Laravel 12.40+ or 13, PHP 8.3+
- A Keila instance running **≥ v0.17.0** (the contacts API needs `id_type=email` lookups, added in 0.17.0)

Installation
------------

[](#installation)

```
composer require reachweb/statamic-keila-integration
```

Publish the config:

```
php artisan vendor:publish --tag=statamic-keila-integration-config
```

Add your credentials to `.env`:

```
KEILA_URL=https://news.example.com
KEILA_API_TOKEN=
```

The token identifies the Keila **project** that contacts are written to (one token = one project). Forms differentiate via tags, not separate projects.

Configuration
-------------

[](#configuration)

Edit `config/statamic-keila-integration.php` and map each Statamic form handle you want forwarded:

```
'forms' => [
    'newsletter' => [
        'opt_in_field' => 'newsletter_opt_in',          // a TOGGLE field; must pass Laravel 'accepted'
        'tags'         => ['newsletter', 'website'],     // -> data.tags, drives a Keila segment
        'source'       => 'website-footer',              // optional -> data.source
        'field_map'    => [
            // statamic field handle => keila target
            'email'         => 'email',
            'first_name'    => 'first_name',
            'last_name'     => 'last_name',
            'room_interest' => 'data.room_interest',      // arbitrary field -> nested custom data
        ],
    ],
],
```

### How mapping works

[](#how-mapping-works)

`field_map` is **explicit** — every Keila field you want populated, including `email`, must be listed. There are no conventional defaults.

- A target of `email`, `first_name`, `last_name`, or `external_id` maps to that **top-level** Keila contact field.
- A target starting with `data.` maps into the contact's **nested custom-data** object (dot paths may nest, e.g. `data.preferences.room`).
- A form whose `field_map` resolves no valid `email` is skipped (with a logged warning).

### The opt-in field

[](#the-opt-in-field)

The opt-in field **must be a toggle** (or any field whose submitted value passes Laravel's [`accepted`](https://laravel.com/docs/validation#rule-accepted) rule: `true`, `"1"`, `"on"`, `"yes"`). Add it to your form blueprint:

```
-
  handle: newsletter_opt_in
  field:
    type: toggle
    display: 'Subscribe to our newsletter'
```

If the toggle is off (or absent), nothing is sent to Keila.

### The Keila segment

[](#the-keila-segment)

Tags are stored on each contact under custom data as `data.tags` (an array). Keila has no native "tags" field — create a **segment** in Keila that filters contacts where `data.tags` contains your tag (e.g. `newsletter`), and target your campaigns at that segment.

Behaviour
---------

[](#behaviour)

On an accepted submission, a queued job:

1. Looks the contact up by email.
2. Builds the contact's custom data by **merging onto whatever already exists** — the tag list becomes the union of existing + configured tags, and other custom fields are never clobbered.
3. Then:
    - **New** contact → created with `status: active`.
    - **Active** contact → tags/data refreshed; status stays `active`.
    - **Unsubscribed** contact → tags/data refreshed, but status is **left as-is**. A bare form submit will **not** re-subscribe someone who previously unsubscribed — that's an explicit withdrawal of consent, and because the API path bypasses Keila's double opt-in, anyone could submit a third party's address. Reactivation must go through a real confirmation step you implement.
    - **Unreachable** contact (hard bounce) → tags/data refreshed, but status is **left as-is** — the addon will not resurrect a bounced address.

### Queue &amp; reliability

[](#queue--reliability)

The job implements `ShouldQueue` and respects your app's `QUEUE_CONNECTION`:

- With a **real queue worker**, the sync is deferred to the worker and retried (3 tries, exponential backoff) on 5xx / 429 / timeout failures. Permanent errors (400/403) are logged and dropped.
- With `QUEUE_CONNECTION=sync`, it runs **after the HTTP response is sent**, so a slow or failing Keila never delays or breaks the visitor's submission. Note that `$tries` / `backoff()` only apply to a real worker — under `sync` the job runs **once**, so a transient Keila failure (5xx / 429 / timeout) is logged via `failed()` and the contact is left for the next submission rather than retried. Run a real queue if you need the retry behaviour.

Emails are masked in logs (`j***@example.com`). Errors are never surfaced to the site visitor.

### Consent &amp; proof of consent

[](#consent--proof-of-consent)

This addon is **single opt-in**: contacts created via the API are set `active` immediately because the Keila API path **bypasses Keila's built-in double opt-in** (no confirmation email is sent). Consent enforcement is therefore the integration's responsibility:

- On the **first** sync of a contact, the submitter's IP and a UTC ISO-8601 timestamp are recorded as `data.consent_ip`, `data.consent_at`, and `data.consent_source` (the form handle) — a basic audit trail. The original record is **preserved** on later re-submits, so a return visit can't overwrite it.
- A submitted opt-in only proves the submitter ticked a box, not that they own the address. If you need verifiable consent — and to re-subscribe anyone who has unsubscribed — add your own email-confirmation step around this addon; a bare form submit will not do it.

Frontend: smooth (AJAX) submissions
-----------------------------------

[](#frontend-smooth-ajax-submissions)

This addon handles the server side — it does not render your form. It does, however, ship an optional **publishable [Alpine](https://alpinejs.dev) partial** that submits the native Statamic form with `fetch` for inline, no-reload feedback, with a no-JS fallback (a normal POST + `{{ if success }}`).

### Ready-made partial

[](#ready-made-partial)

Publish it:

```
php artisan vendor:publish --tag=statamic-keila-integration-views
```

That copies `newsletter.antlers.html` to `resources/views/vendor/statamic-keila-integration/`. Include it anywhere — e.g. your footer:

```
{{ partial:statamic-keila-integration::newsletter }}
```

It is self-contained: the Alpine component lives inline in `x-data`, so there's **no JS import or build step** — Alpine (already on your page) picks it up. It's also toast-aware but not toast-dependent — on success it optionally calls `$store.toasts?.push(...)` and dispatches a `newsletter:subscribed` event, both degrading gracefully when absent.

> You can include it **without** publishing — the addon auto-namespaces its views, so the same `{{ partial:statamic-keila-integration::newsletter }}` resolves to the packaged copy. Publishing just gives you an editable copy that takes precedence.

Then customise the published copy:

- **Styling** — the defaults use generic, portable Tailwind core utilities. Restyle freely with your own classes.
- **Copy** — strings are hardcoded English run through the `| trans` modifier; translate them via your lang files, or swap in your own strings / globals.
- **Fields** — match the form handle (`form:newsletter`) and the opt-in field `name` (`newsletter_opt_in`) to your own form blueprint.

### Or build your own

[](#or-build-your-own)

The partial relies on Statamic returning JSON when the form is submitted with an AJAX header:

- success → `200 {"success": true, "redirect": …}`
- validation error → `400 {"error": {"": ""}, "errors": [...]}`
- detection → send the `X-Requested-With: XMLHttpRequest` header.

A minimal progressive enhancement (the same idea as the shipped partial, distilled):

```

  {{ form:newsletter }}
     … inputs + opt-in toggle …
    You're subscribed!
  {{ /form:newsletter }}

```

Notes:

- **Copy:** this addon is **single opt-in** (the contact is set `active` immediately, no confirmation email), so say "You're subscribed!" — not "check your inbox to confirm."
- **Static caching:** if you run Statamic's static caching, the form's `_token` must stay fresh. Submitting `new FormData(form)` picks up the token Statamic keeps live via its nocache layer — but test a real submit on a cached page (a stale token returns `419`).

Spam protection
---------------

[](#spam-protection)

Keila skips CAPTCHA for API calls, so keep spam protection on the form itself (honeypot / Cloudflare Turnstile). That is out of scope for this addon.

Testing
-------

[](#testing)

```
composer install
vendor/bin/phpunit
```

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/043c16e10a57aa2e23f25400c12c1ef9c809ddcb369fbd79cf9c162d89fa8289?d=identicon)[reachweb](/maintainers/reachweb)

---

Top Contributors

[![afonic](https://avatars.githubusercontent.com/u/7423993?v=4)](https://github.com/afonic "afonic (4 commits)")

### Embed Badge

![Health badge](/badges/reachweb-statamic-keila-integration/health.svg)

```
[![Health](https://phpackages.com/badges/reachweb-statamic-keila-integration/health.svg)](https://phpackages.com/packages/reachweb-statamic-keila-integration)
```

###  Alternatives

[statamic-rad-pack/runway

Eloquently manage your database models in Statamic.

135212.4k7](/packages/statamic-rad-pack-runway)[statamic/seo-pro

68488.6k](/packages/statamic-seo-pro)[statamic/statamic

Statamic

829176.7k](/packages/statamic-statamic)[statamic/eloquent-driver

Allows you to store Statamic data in a database.

126688.5k15](/packages/statamic-eloquent-driver)[rias/statamic-redirect

29322.9k](/packages/rias-statamic-redirect)[duncanmcclean/statamic-cargo

Comprehensive e-commerce addon for Statamic. Build bespoke e-commerce sites without the complexity.

3310.1k](/packages/duncanmcclean-statamic-cargo)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
