CITTA Stripe to iinsight Bridge
Receives Stripe payment webhooks, verifies them, logs each event, emails the team, and exposes a clearly-marked placeholder for the future iinsight case-creation API call (Phase 2).
by Stallioni Net Solutions · github.com/sanjeev-stallioni/citta-stripe-iinsight · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/sanjeev-stallioni/citta-stripe-iinsight/archive/refs/heads/main.zipA lightweight, single-file WordPress plugin that receives Stripe payment webhooks, verifies them, logs each event, emails the CITTA team, and (Phase 2) automatically creates the client "case" in iinsight.
Built for CITTA, a trauma-informed NDIS recovery service. The code favours calm, predictable, auditable behaviour over cleverness.
What it does (Phase 1 — live)
- Receives Stripe webhooks at a dedicated REST endpoint.
- Verifies the Stripe signature (HMAC-SHA256) before trusting any data.
- Processes only
checkout.session.completed(paid) events; safely ignores the rest. This event carries the Payment Link'sprogrammetadata plus the customer details, so program detection works without code changes. - Extracts customer name, email, phone, amount, currency and program.
- Logs each event to a custom database table (with idempotency so Stripe's duplicate deliveries never double-process).
- Emails an admin notification to the configured address.
- Creates the iinsight case (Phase 2, when enabled) and fires a
citta_stripe_payment_processedaction for any further extension.
Phase 2 — automatic iinsight case creation (implemented)
When enabled, a successful payment also creates a client (Person) case in iinsight:
- Authenticates (
POST /auth/login, Basic Auth + form-data) and caches the bearer token in a transient until it expires. POST /api1/cases/new_clientwith the customer data and the configured IDs.- Stores the returned
case_numberiniinsight_case_idand setsiinsight_statustocreated(orfailed) — visible in the Event Log. - Emails the team if creation fails, so the case can be created manually.
Case creation only runs after the event row is logged, so a Stripe retry can never create a duplicate case.
Configuring Phase 2 (Settings → CITTA Stripe → iinsight Integration)
| Setting | Notes |
|---|---|
| Enable integration | Off by default |
| Environment | UAT (sandbox) or Production — separate URLs |
| Client ID / Client Secret | Basic Auth pair (generated on the iinsight platform) |
| iinsight Username / Password | Service-account user login (form-data) |
| Business Division ID | from GET /api1/cases/business_divisions |
| Service Contract ID | from GET /api1/cases/service_contracts |
| Finance Template ID | from GET /api1/cases/finance_templates |
| Team ID / Assigned-to User ID | from the iinsight Users endpoints |
Secrets are never displayed back or logged; leaving a secret field blank keeps the stored value. Test in UAT before switching to Production.
Installation
- Copy the
citta-stripe-iinsight/folder intowp-content/plugins/. - In WordPress admin → Plugins, activate CITTA Stripe to iinsight
Bridge. Activation creates the
{prefix}citta_stripe_eventstable. - Go to Settings → CITTA Stripe.
- Copy the webhook endpoint URL shown at the top of the page, e.g.
https://your-site.com/wp-json/citta/v1/stripe-webhook.
Connecting Stripe
- In the Stripe Dashboard, add a new webhook endpoint and paste the URL above.
- Subscribe it to the
checkout.session.completedevent. - Copy the endpoint's Signing secret (starts with
whsec_). - Paste it into Settings → CITTA Stripe → Stripe Signing Secret and save.
The stored secret is never displayed back or written to any log. Leave the field blank when saving to keep the existing secret.
Test vs Live mode: Stripe keeps separate webhook endpoints and separate signing secrets for sandbox/test and live. Pay in the same mode as the endpoint you created, and paste that mode's signing secret. A live-mode secret on a test payment (or vice versa) fails signature verification (HTTP 401) and nothing is logged.
Reachability: Stripe must be able to reach the endpoint over the public internet — a
localhost/.testURL won't receive live webhooks. For local testing use the Stripe CLI:stripe listen --forward-to <webhook-url>.
Configuring your Stripe Payment Links
Each Payment Link needs a little configuration so the plugin can identify the program and collect the customer details iinsight requires.
1. Program metadata (required)
Set a Metadata key named program on the Payment Link (Dashboard →
the link → Metadata) with one of:
program value |
Displayed as |
|---|---|
foundation |
Citta Foundation Program |
integration_circle |
Citta Integration Circle |
This works because the plugin listens to checkout.session.completed, and
Stripe copies the Payment Link's metadata onto the Checkout Session. (It does
not appear on the PaymentIntent — which is why the plugin uses the Checkout
Session event, not payment_intent.succeeded.) Unknown or missing values are
still logged; they simply show the raw key.
Per-link: metadata lives on each individual Payment Link. A new link (e.g. after a price change) needs its own
programvalue.
2. Customer fields to collect (Options section)
| Option | Setting | Why |
|---|---|---|
| Collect customer names | On, not optional | iinsight needs first + last name |
| Require phone number | On (recommended) | maps to the iinsight case mobile |
| Collect business names | Off | avoids the business name overwriting the person's name |
| Collect customer addresses | Optional | not used by the plugin |
Stripe collects the name as a single field; the plugin splits it (first word = first name, remainder = last name) and always keeps the full name. Email is collected automatically.
Prices
Program prices are managed entirely in Stripe — the plugin reads the paid amount dynamically from each Checkout Session, so you can change a price (or spin up a new link) anytime without touching the plugin. The amount shown in the log and email always reflects what the customer actually paid.
Customer receipts / invoices
This plugin emails your team, not the customer. If you want the customer to receive a receipt, enable it under Stripe Settings → Customer emails (note: Stripe only sends these in live mode, not sandbox). For a formal tax invoice, turn on Post-payment invoice on the Payment Link. Both are Stripe features, independent of this plugin.
Settings reference
| Setting | Default | Notes |
|---|---|---|
| Stripe Signing Secret | (empty) | whsec_...; required for webhooks to be accepted. |
| Notification Email | (set in settings) | Where payment notifications are sent. |
| Logging | On | Records events; also powers idempotency. |
| Admin Emails | On | Sends the notification email per payment. |
| Email Subject | (built-in default) | Editable subject line; supports merge tags. Blank = default. |
| Email Body | (built-in default) | Editable HTML body; supports merge tags. Blank = default. |
| Delete Data on Uninstall | Off | When off, history & settings survive plugin deletion. |
Editing the notification email
The recipient, subject, and HTML body are all editable under Settings → CITTA Stripe — nothing about the email is hardcoded. Use these merge tags in the subject or body and they're replaced per payment:
| Tag | Replaced with |
|---|---|
{program} |
Program name (e.g. Citta Foundation Program) |
{amount} |
Amount with currency (e.g. AUD $2,327.88) |
{name} |
Customer full name |
{first_name} / {last_name} |
Customer first / last name |
{email} |
Customer email |
{phone} |
Customer phone |
{currency} |
Currency code (e.g. AUD) |
{payment_intent} |
Stripe payment intent ID |
{timestamp} |
Date/time the payment was received |
Leave either field blank and save to restore the built-in default. Customer
values are HTML-escaped when merged, so they can't break the layout; the body
is sanitised with wp_kses_post() on save (scripts/iframes stripped, email-safe
table/inline-style markup kept).
Event log
Settings → CITTA Stripe Log lists the 50 most recent events (newest first): date, payment intent, customer, email, program, amount and iinsight status. Each row has a View raw payload section for debugging.
Lifecycle behaviour
- Activation — creates the table (via
dbDelta) and seeds default options. - Deactivation — does not drop the table or delete settings; logs and config are preserved.
- Uninstall (delete) — drops the table and removes options only if "Delete Data on Uninstall" was enabled. Otherwise nothing is destroyed.
Security notes
- Webhook signature is verified before any other processing.
- Signatures compared with
hash_equals()(constant-time); timestamps outside a 5-minute window are rejected as possible replays. - All DB writes use
$wpdbprepared statements / format arrays. - All admin output is escaped; the settings form uses the WordPress Settings API nonce.
- The signing secret and full card data are never logged.
HTTP responses returned to Stripe
| Code | Meaning |
|---|---|
| 200 | Handled, ignored, or duplicate — Stripe stops retrying. |
| 400 | Malformed request (empty body / invalid JSON). |
| 401 | Missing or invalid signature. |
| 500 | Not configured or unexpected server error — Stripe will retry. |
Requirements
- WordPress 5.8+
- PHP 7.4+
Extending
The iinsight integration (Phase 2) is built in. For anything further — a CRM sync, a Slack ping, a second email — hook the action that fires after every processed payment:
add_action( 'citta_stripe_payment_processed', function ( $data ) {
// $data contains the sanitised payment + customer fields
// (program_label, amount, customer_name, customer_email, etc.).
} );
Version 1.1.0 — Phase 1 (webhook, logging, email) and Phase 2 (automatic iinsight case creation) both implemented. Configure Phase 2 under Settings → CITTA Stripe and test in the UAT sandbox before going live.