A free, complete CRM: contacts, invoices with Stripe & PayPal, tasks, campaigns, automations and an API. Pro for team access.

Alp CRM Main Screenshot

The Alp CRM dashboard

A complete, free CRM inside your Statamic Control Panel: contacts and companies, quotes and invoices with online payments, tasks and a calendar, email campaigns, segments, automations, reports, a REST API and webhooks. It's built with Statamic's own UI components, so it looks and feels like the rest of the Control Panel.

Alp CRM was previously called Radpack CRM (see Upgrading from Radpack CRM). It's inspired by Jetpack CRM — a CRM that lives in your CMS — and is free and open source under the MIT license. It isn't affiliated with Jetpack CRM or Automattic.

Start here: Requirements · Installation · Settings · Using the CRM · REST API · Webhooks · Operations and troubleshooting · Upgrading

Screenshots

Contacts A contact's profile
Quote editor An invoice
The client's invoice page with payment buttons Tasks
Calendar Segment rules
Campaign report Automation editor
Reports CSV import
Integrations Client portal billing

Features

Contacts and companies

  • Contacts and companies (B2B), with statuses, owners, tags, aliases (other email addresses for the same person) and Gravatar avatars
  • Custom fields: edit the contact, company, task and transaction blueprints like any other Statamic blueprint
  • Notes, calls, meetings, emails and text messages logged on a timeline, plus a full activity history
  • Files on contacts and companies, optionally shared with the client
  • A client password manager, encrypted at rest; every reveal is logged
  • Filters, bulk actions (tag, change status, delete) and a bulk tagger for whole segments

Sales

  • Quotes and invoices with a line-item editor, tax rates, discounts, numbering and multiple currencies
  • PDF quotes and invoices, emailed with the PDF attached
  • Client pages to view and download documents, accept or decline quotes, and pay invoices
  • Online payments with Stripe and PayPal; payments mark invoices paid automatically
  • Transactions (sales and refunds), lifetime value per contact, and imports from Stripe and PayPal

Work

  • Tasks, calls, meetings and deadlines, assigned to team members, with email reminders
  • A month calendar of tasks and invoice due dates
  • A dashboard with your week's tasks, key numbers and latest activity

Marketing

  • Email contacts from their profile, now or scheduled, with reusable templates and merge tags
  • Segments: dynamic groups by status, tags, dates, lifetime value or any custom field
  • Campaigns to a segment, sent in batches, with open and click tracking and one-click unsubscribe
  • Sync with Mailchimp, Kit (ConvertKit) and AWeber
  • Text messages with Twilio

Automation and reporting

  • Automations: "when a contact is tagged VIP, if they're a customer, wait a day, then email them and create a task"
  • Reports: revenue and new contacts by month, quote acceptance, days to pay, top customers, sales by source, and a status funnel

Connections

  • Statamic forms and site registrations create contacts automatically
  • CSV import with column mapping, and CSV export
  • Google Contacts import
  • A REST API with API keys, signed webhooks and REST hooks for Zapier, Make and n8n
  • Billing and files pages in Client Portal, our free client portal addon
  • White label: rename "CRM" in the navigation

Requirements

  • Statamic 6, PHP 8.3+. Statamic Core is enough for one person: the site owner gets the whole CRM. To give team members access with CRM permissions you need Statamic Pro, because Core allows only one user and has no roles. The client portal pages also need Pro, through Client Portal.
  • A database (SQLite, MySQL, MariaDB or PostgreSQL), as for any Laravel app
  • A mail driver, for sending documents, emails and reminders
  • The Laravel scheduler (* * * * * php artisan schedule:run), for reminders, scheduled emails, campaigns, automations and syncing
  • PHP's cURL extension for public webhook delivery. Alp CRM blocks delivery if it cannot pin a checked DNS address to the outgoing request.
  • If your app uses a worker-backed Laravel queue connection such as database, redis or sqs, a running queue worker for webhook deliveries.

Installation

composer require rad-themes/alp-crm
php artisan migrate

Open the CRM from CRM at the top of the Control Panel sidebar, under Dashboard. All its pages are also in the CRM section at the bottom of the sidebar; drag sections into the order you like in Preferences → CP Navigation. Super users can use everything straight away; other users need the permissions below.

First-run checklist

  1. In CRM → Settings → Business, enter the business name, address, contact details and optional logo. These appear on client documents.
  2. In Sales, choose the currency, invoice and quote prefixes, payment terms and tax rates before sending your first document.
  3. Configure your mail driver and send a test email through your app. Document emails, reminders and campaigns use that driver.
  4. Set up Laravel's scheduler. If your queue connection uses workers, run one too; webhook deliveries use the queue.
  5. Create a test contact and a draft quote or invoice. Review the PDF and the client-facing link before sending real documents.
  6. Add payment, mailing-list and other integration credentials only for services you plan to use. Each integration is optional.

No sample CRM records are installed. Your contacts, documents and settings start with your own data.

Permissions

Under Users → Permissions, each role can get:

Permission Allows
View CRM Seeing contacts, companies, sales, tasks, campaigns and reports
Create and edit CRM records Creating and changing them, sending emails and documents
Delete CRM records Deleting them
Manage client passwords Seeing and editing saved client passwords

Settings, integrations, API keys and webhooks need Configure addons.

Give the Manage client passwords permission only to staff who should reveal saved client credentials. API keys are managed separately under Configure addons and can expose CRM data; use a read-only key when an integration only needs to read.

Settings

CRM → Settings (or Tools → Addons → Alp CRM → Settings):

  • Business: name, logo, address and tax number shown on quotes, invoices and emails
  • Sales: default currency, numbering, payment terms, quote validity, tax rates, and default terms
  • Reports: which statuses form your funnel, in order
  • Payments: Stripe and PayPal
  • Integrations: Mailchimp, Kit, AWeber, Twilio and Google Contacts
  • Lead capture: which forms create contacts, and whether registrations do
  • Email: campaign sending speed, and the wording of invoice and quote emails
  • White label: what to call the CRM in the navigation

Settings are separate from your Statamic blueprints. Use the settings screen for site-wide behavior, and custom fields for data stored on each record. Changes to the default currency or tax configuration do not rewrite previously saved invoices.

Keeping secrets out of git

Settings are saved in resources/addons/alp-crm.yaml, which is usually in version control. API secrets can come from your .env instead, and those values take precedence:

ALP_CRM_STRIPE_SECRET_KEY=
ALP_CRM_STRIPE_WEBHOOK_SECRET=
ALP_CRM_PAYPAL_CLIENT_ID=
ALP_CRM_PAYPAL_SECRET=
ALP_CRM_MAILCHIMP_API_KEY=
ALP_CRM_KIT_API_KEY=
ALP_CRM_AWEBER_CLIENT_ID=
ALP_CRM_AWEBER_CLIENT_SECRET=
ALP_CRM_TWILIO_SID=
ALP_CRM_TWILIO_TOKEN=
ALP_CRM_GOOGLE_CLIENT_ID=
ALP_CRM_GOOGLE_CLIENT_SECRET=

Publish the config with php artisan vendor:publish --tag=alp-crm-config to change it. It also sets the disk for client files (ALP_CRM_FILES_DISK, default local, which is private) and the file types that may be attached (file_extensions).

OAuth tokens (AWeber, Google) and sync positions are stored encrypted in storage/app/alp-crm.

Scheduler and queue

Add this cron entry on the server that runs your Statamic site, with the correct PHP binary and project path:

* * * * * cd /path/to/statamic && php artisan schedule:run >> /dev/null 2>&1

The addon registers its own scheduled commands; there is no separate Alp CRM cron entry. For a worker-backed queue connection such as database, redis or sqs, run your normal Laravel worker as well (for example, php artisan queue:work). The sync, background and deferred connections do not need a separate worker. The scheduler handles campaigns, scheduled emails, reminders, delayed automations and enabled hourly imports. See Scheduled commands for frequencies and manual commands.

Upgrading

From Alp CRM 2.0.x to 2.1.x

Update the package and run migrations as you normally would for a Statamic addon:

composer update rad-themes/alp-crm
php artisan migrate

Version 2.1 changes client portal authorization. A matching email address or contact alias no longer grants portal access. For every existing client who should see billing or shared files, open their CRM contact, set Portal user to the correct Statamic account, and save. Check the portal as that account. A registration links the account only when it created the contact and any company it named was new; a registration matching an existing contact, or naming an existing company, does not link. This prevents an unverified registration from inheriting another contact's or company's records.

Outgoing webhooks now reject redirects, unresolved hosts and private or reserved addresses. If an endpoint has moved, update its saved URL to the final public HTTPS address. Webhook delivery needs PHP cURL; a missing extension blocks the request rather than allowing an unchecked DNS lookup. The ALP_CRM_ALLOW_PRIVATE_WEBHOOKS override is intended for development only.

From Radpack CRM

Alp CRM is the same addon under a new name: Radpack is Statamic's own brand, so we renamed ours. Your data stays where it is.

  1. composer remove rad-themes/radpack-crm && composer require rad-themes/alp-crm
  2. php artisan migrate (stored references are converted to the new name)
  3. Rename resources/addons/radpack-crm.yaml to resources/addons/alp-crm.yaml, and storage/app/radpack-crm to storage/app/alp-crm
  4. Rename any RADPACK_CRM_… variables in .env to ALP_CRM_…, and a published config/radpack-crm.php to config/alp-crm.php
  5. Update integrations that call the CRM: the API is now at /api/alp-crm/v1, public links at /!/alp-crm/…, webhook headers are X-Alp-Event and X-Alp-Signature, and Stripe's webhook URL is /!/alp-crm/webhooks/stripe. Existing API keys keep working.

Using the CRM

Custom fields

Contacts, companies, tasks and transactions use blueprints. Edit them in Fields → Blueprints, under Alp-crm, to add fields such as "Industry" or "Birthday". Custom fields show on profiles, in segments and automations (as Custom field), as merge tags ({{ industry }}), and in imports, exports and the API.

Lead capture

In Settings → Lead capture, pick the Statamic forms that should create contacts. Submissions are matched to contacts by email: new people become leads, and existing contacts get any missing details filled in, without overwriting what you have. Fields are mapped by handle: email, name, first_name, last_name, phone, company, and any contact field handle such as city. Other fields are saved as a note. The form's title is added as a tag.

Turn on Add users who register to the CRM to do the same for site registrations. A registration links the user account to the contact only when it created that contact and any company it named was new. It does not link when the email matches an existing contact, or when the company name matches an existing company — Statamic verifies neither, so an unverified registration can't inherit a contact's or a company's billing. The contact is still filed under the matching company; link the account yourself with the contact's Portal user field once you've checked it.

For public registration forms, treat submitted names, phone numbers and company names as unverified lead information. Review a captured contact before using those details for billing or portal access — a contact filed under an existing company by a registration is never linked to that user account automatically.

Import and export

Contacts → Import takes a CSV (comma, semicolon or tab separated). Match each column to a field, choose whether to update existing contacts (by email) or companies (by name), and tag everyone imported. Columns named Company create or link companies; Tags columns can hold several tags separated by commas.

The import flow has two steps: upload the file, then review its preview and column mapping before importing. The maximum upload is 20 MB. The result reports created, updated and skipped rows, with a sample of errors. A completed import deletes its temporary CSV; abandoned uploads older than a day are cleaned up when the next file is uploaded. Keep your source CSV until you have checked the imported records.

Export contacts or companies from the … menu on their list, or a segment from the segments list. Values that spreadsheet apps would run as formulas are escaped.

Quotes, invoices and payments

Create a quote, send it, and the client can accept or decline it on its page. Convert an accepted quote to an invoice in one click. Invoices track payments and their balance, and become Overdue after their due date.

Draft documents are only in the Control Panel. Sending a quote or invoice creates a client-facing link with a long random token; the client can view or download its PDF without a portal account. Configure Business, Sales and your mail driver before using Send. Use Record payment for offline payments; Stripe and PayPal payments are recorded through their checkout flows.

To take payments online:

  • Stripe: add your secret key. In Stripe, add a webhook to the URL shown on CRM → Settings → Integrations for the events checkout.session.completed, charge.succeeded and charge.refunded, and paste its signing secret. Payments are also confirmed when the client comes back from Stripe, so they're recorded even before the webhook is set up.
  • PayPal: add the client ID and secret of a REST app (sandbox or live).

Set PayPal's Mode to match the credentials you entered. Enable hourly transaction import only if you want payments outside Alp CRM invoices reflected in CRM reports; PayPal's import also requires Transaction search on the REST app. The Create contacts for new payers switch controls whether imports create contacts for unfamiliar payers.

Turn on the imports to add your other Stripe and PayPal payments as transactions every hour, creating contacts for new customers.

Email, segments and campaigns

Write to a contact from the Emails tab of their profile, now or later. Email templates are reusable messages with merge tags: {{ first_name }}, {{ last_name }}, {{ name }}, {{ email }}, {{ company }}, {{ business_name }} and any contact field. Templates can only use variables, not Antlers tags.

Before sending a campaign, create a segment, review its audience, then use Send test on the campaign. Campaigns do not send to unsubscribed contacts, and the batch size in CRM → Settings → Email limits how many recipients are attempted each minute. A stopped scheduler leaves scheduled messages and campaign recipients waiting; resume it to continue processing.

Segments group contacts by rules and update automatically. Use them to filter the contacts list, export, bulk tag, or send a campaign. Campaigns:

  • go to subscribed contacts with an email address, once per address
  • send in batches every minute (set the batch size to suit your mail provider)
  • track opens and clicks; links are signed so they can't be used to redirect elsewhere
  • include an unsubscribe link and a one-click List-Unsubscribe header. Opening the link shows a confirmation, so link scanners can't unsubscribe people.

Send campaigns through a transactional email provider (Postmark, Amazon SES, Mailgun, Resend…) with SPF, DKIM and DMARC set up for your domain.

Automations

An automation has a trigger (contact created, tagged or status changed, form submitted, quote accepted, invoice paid, task completed, and more), optional rules the contact must match, and steps. Each step can wait minutes, hours or days first:

  • add or remove tags, change status
  • send an email template or a text message
  • create a task, add a note
  • email your team, or post to a webhook URL

Start from a recipe such as Welcome new leads or Accepted quote → customer. Each automation shows its recent runs, and steps are logged on the contact. Automations that trigger each other stop after three levels, so they can't loop.

Client portal

Install Client Portal (composer require rad-themes/client-portal, version 1.1 or later). A logged-in client sees:

  • Billing: their invoices (with View & pay), quotes (with Review) and payments, including their company's
  • Files: files you've shared with them from their profile

A client only sees a contact's records once you link their user account to that contact, in the contact's Portal user field (or automatically, when their own registration created both the contact and its company). Matching on the email address is deliberately not enough: Statamic doesn't verify email addresses on front-end registration, so on a site with open registration anyone could sign up as [email protected] and read their billing.

To give an existing client access:

  1. Create or identify the client's Statamic user account through your own trusted onboarding process.
  2. Open the contact in CRM → Contacts → Edit, choose that account in Portal user, and save.
  3. Mark only the intended contact or company files as shared with the portal.
  4. Sign in as that client to verify billing and files. Remove the Portal user selection to revoke access.

Billing includes documents for the linked contact and their company. Check the contact's company assignment before linking a user, because company invoices and quotes may also become visible. Shared file downloads enforce the same link and are served as downloads from the configured files disk.

Integrations

CRM → Settings → Integrations shows each connection's status, with Connect buttons for AWeber and Google and Sync now for imports.

  • Mailchimp, Kit, AWeber: contacts sync as they change, with their tags. Unsubscribing in the CRM unsubscribes them in the list. Optionally only sync contacts with certain tags.
  • Twilio: text contacts from their profile and from automations. Messages are logged as notes.
  • Google Contacts: import contacts with an email address, once or every hour.

Mailchimp needs an API key and audience ID; Kit needs a v4 API key and can optionally add subscribers to a form; AWeber needs app credentials and a list ID, followed by Connect. Use Only sync contacts with these tags to narrow the mailing-list audience. Google Contacts needs an OAuth web client with the People API enabled; enter the credentials, connect the account, and choose whether to import hourly. Sync now runs the selected configured import or list sync without waiting for the next schedule tick.

REST API

Create an API key in CRM → Settings → API & webhooks. Keys can be read-only. Send the key as a bearer token:

curl https://example.com/api/alp-crm/v1/contacts?tag=vip \
-H "Authorization: Bearer alp_…"

Copy the key when it is first displayed; Alp CRM stores only its hash and cannot show the full key again. Revoke a key you no longer use. A read-only key can use safe methods such as GET, while write methods return 403. Missing or invalid keys return 401. You can also send the key in X-Api-Key.

For example, create a contact with a write-enabled key:

curl -X POST https://example.com/api/alp-crm/v1/contacts \
-H "Authorization: Bearer alp_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"first_name":"Ada","last_name":"Lovelace","email":"[email protected]","status":"lead"}'

The API returns a data object for one record and data plus meta for a paginated list. A create returns 201, an update returns 200, and a delete returns 204. Blueprint validation errors use Laravel's JSON validation response. Use GET /me to check a key before configuring a third-party integration.

Endpoint
GET /me Check the key works
GET /events Event names for webhooks
GET POST /contacts, GET PATCH DELETE /contacts/{id} Filter with search, email, status, tag, company_id, updated_since
POST /contacts/upsert Create or update by email
POST DELETE /contacts/{id}/tags {"tags": ["VIP"]}
POST /contacts/{id}/notes {"body": "…", "type": "call"}
GET POST /companies, GET PATCH DELETE /companies/{id}
GET POST /tasks, GET PATCH DELETE /tasks/{id}
GET POST /transactions, GET PATCH DELETE /transactions/{id}
GET /quotes, GET /invoices, GET /quotes/{id}, GET /invoices/{id} Read only
POST /hooks, DELETE /hooks/{id} REST hook subscriptions (Zapier, Make, n8n)

Writes are validated with the same blueprints as the Control Panel, so required fields and custom fields work the same way. Send relations as company_id, contact_id, owner_id and assigned_to, and custom fields at the top level or inside fields. Lists are paginated (per_page up to 100) and limited to 120 requests a minute per IP.

POST /contacts/upsert matches the primary email or an alias and either creates the contact or updates the match. Protect write-enabled keys like administrator credentials: they can change CRM data and subscribe webhooks. A successful API call does not automatically grant a Statamic user portal access; set Portal user deliberately.

Webhooks

Add webhooks in CRM → Settings → API & webhooks, or subscribe through the API. Each event is POSTed as JSON:

{
"id": "1f0c…",
"event": "invoice.paid",
"created_at": "2026-10-02T10:00:00+00:00",
"data": { "id": 12, "object": "invoice", "number": "INV-0012", "total": 1200, "…": "…" },
"context": {}
}

Verify the X-Alp-Signature header, an HMAC-SHA256 of the raw body using the webhook's signing secret:

$expected = 'sha256='.hash_hmac('sha256', $request->getContent(), $secret);
abort_unless(hash_equals($expected, $request->header('X-Alp-Signature')), 401);

The request also carries X-Alp-Event and X-Alp-Delivery. Verify the signature against the raw request body before parsing JSON. Keep the signing secret private; it is separate from the API key. In the Control Panel, use a webhook's Test action to send a sample contact.created event. Its last status, error and send time help diagnose delivery. A REST hook subscription that responds 410 Gone is removed.

The delivery URL must use HTTP or HTTPS and resolve to public IP addresses. Alp CRM checks the address, pins it for the request, and does not follow redirects. A 3xx response means you should replace the saved URL with its final public destination. Delivery will not follow a redirect to 127.0.0.1, a link-local metadata address or another internal service.

Events: contact.created, contact.updated, contact.status_changed, contact.tagged, contact.unsubscribed, contact.deleted, company.created, company.updated, company.deleted, form.submitted, quote.created, quote.sent, quote.accepted, quote.declined, invoice.created, invoice.sent, invoice.paid, transaction.created, task.created, task.completed.

For developers

Every event above is also a Laravel event:

use RadThemes\AlpCrm\Events\CrmEvent;
 
Event::listen(function (CrmEvent $event) {
if ($event->name === 'invoice.paid') {
// $event->payload, $event->contact, $event->context
}
});

Add your own automation step:

use RadThemes\AlpCrm\Automations\Actions;
 
Actions::extend('slack', 'Post to Slack', function (array $step, ?Contact $contact, array $context) {
// …
return 'Posted to Slack'; // shown in the run log
});

Data lives in your database in crm_* tables, with Eloquent models in RadThemes\AlpCrm\Models.

Scheduled commands

Registered automatically; they need the Laravel scheduler.

Command When
alp-crm:send-emails Every minute: scheduled emails and campaign batches
alp-crm:automations Every minute: delayed automation steps
alp-crm:task-reminders Every five minutes
alp-crm:sync Hourly: Stripe, PayPal and Google imports that are turned on. Run alp-crm:sync stripe (or paypal, google, lists) any time.

Operations and troubleshooting

Symptom Check
Scheduled email, campaign, reminder or delayed automation is not running Run php artisan schedule:list to confirm the commands are registered, then make sure the server invokes schedule:run every minute. Check the app's mail configuration and logs.
Webhook remains unsent If the app uses a worker-backed queue connection, check the queue worker and failed jobs. Then use API & webhooks → Test and inspect the last status or error.
Webhook is blocked or answers 3xx Use the final public HTTP(S) URL, check its DNS records, and confirm PHP cURL is installed. Private and unresolved destinations and redirects are refused.
Client cannot see billing or files in Client Portal Confirm Client Portal is installed, the contact's Portal user is the correct account, and the contact or company owns the document or shared file. Email matching alone gives no access.
Payment button is missing Check that the invoice is payable and that the relevant Stripe or PayPal credentials and mode are configured under Settings → Payments.
CSV row was skipped Check the mapping preview, required email or name fields, the update-existing option, and the import result's errors.

Back up both the Laravel database (crm_* tables) and the storage disk used for uploaded client files. The database contains the CRM records; file metadata alone cannot restore the uploaded bytes. Keep APP_KEY safe as encrypted client passwords and OAuth tokens depend on it.

Translations

Every string goes through Laravel's translator, using the English text as the key. To translate the CRM, add the strings to your site's lang/{locale}.json.

Security notes

  • Client pages for quotes and invoices use long random links and are not indexed by search engines.
  • Client files are stored on a private disk and always downloaded, never displayed inline. Uploads are limited to the file types in config/alp-crm.php.
  • Client portal access needs an explicit link between a user account and a contact (the contact's Portal user field), never a matching email address or company name.
  • Saved client passwords and OAuth tokens are encrypted with your app key; API keys are stored as hashes.
  • Stripe webhooks and campaign links are signed and verified.
  • Webhooks and automation webhook steps won't call private or local addresses, won't call a host that doesn't resolve, and don't follow redirects — the request goes to the address that was checked. PHP's cURL extension is required for public webhook delivery; without it, delivery is blocked rather than falling back to an unpinned DNS lookup. Set ALP_CRM_ALLOW_PRIVATE_WEBHOOKS=true only for development when private addresses are needed.
  • PDFs are rendered with remote file fetching turned off; your logo is embedded in the document.
  • Email templates can't run Antlers tags or PHP.

Found a security issue? Please report it privately through the repository's Security → Report a vulnerability page rather than opening an issue.

License

MIT. See LICENSE.md.

Development

composer install
npm install
npx vite build # Control Panel assets, into resources/dist
vendor/bin/phpunit