Skip to content

Documentation

Payments from Stripe

Money is the one thing a CRM has to get right, and the thing most of them make you wire up yourself. Kicklace takes Stripe's webhook directly: one address pasted, one signing secret back, and a payment becomes a purchase on the person's timeline with the lifecycle moved on. This is every event and what it becomes.

There is no code to write and nothing to deploy. A payment becomes a purchase on the person's timeline and the lifecycle moves them to Purchaser — the same event your own server would have posted, so an automation written for a purchase answers a Stripe payment without knowing Stripe exists.

Kicklace reads Stripe; it never writes to it, never charges anybody, and never asks for an API key of yours. The screen is Settings → Integrations → Stripe, and only an owner or an admin can change it.


Setting it up

1. Make the address. Settings → Integrations → Stripe → Connect Stripe. You get an address of the form https://www.kicklace.com/api/webhooks/stripe/<token>. The token is not the security — the signature is — but it is not published either, so nobody can guess your endpoint.

2. Add it in Stripe. Developers → Webhooks → Add endpoint, paste the address, and choose the events in the table below. Ticking more is harmless: anything Kicklace has no use for is answered 200 and ignored, which is what stops Stripe retrying it.

3. Bring the signing secret back. Stripe shows it once the endpoint exists, which is why it is the second step and not the first. Paste it into Kicklace. It is encrypted at rest and never shown back — the screen says "set on the 9th" and offers Replace. Until it is in, nothing is taken: Kicklace answers no_signing_secret with a sentence saying where to paste it, and writes the refusal to your website's heartbeat.

How the signature is checked

Stripe signs with Stripe-Signature: t=<unix seconds>,v1=<hex>. Kicklace recomputes an HMAC-SHA256 of <t>.<the exact request body>, keyed with the whole whsec_… secret, compares it in constant time, and refuses anything more than five minutes old. A header carrying several v1 values — which is what a secret being rolled looks like — passes if any of them matches. Schemes that are not v1 are ignored, v0 included: it is sent for test events, and accepting it would be a downgrade.

There is no Stripe SDK here and no dependency: a webhook signature is an HMAC, and Node has one.

What each event becomes

What Stripe sends What lands in Kicklace
checkout.session.completed purchase, for the session's total
payment_intent.succeeded purchase, for the amount received
invoice.paid purchase, for the amount paid
charge.refunded refund, for the amount refunded
customer.subscription.created, still in its trial trial_started
customer.subscription.created, not in a trial subscription_started
customer.subscription.updated, with a new period end subscription_renewed
customer.subscription.deleted subscription_cancelled
checkout.session.expired checkout_abandoned, with what they were about to spend

Everything else is answered 200 and ignored.

The money is in whole currency units. Stripe sends 4900 for $49.00, so Kicklace stores 49; the currencies Stripe keeps with no minor unit — yen, won, and the rest — are already whole and are left alone. That number is properties.value, which is what a chart adds up when you ask for revenue.

A trial is Stripe's own status. A subscription created with a trial is trialing, and that is what decides it, read against the moment the event happened rather than now — so a webhook replayed a week later still says what was true when it was sent. trial_end is only consulted where a payload carries no status at all.

A purchase, a trial starting and a cancellation are the events that move anybody — up to Purchaser, into Trial, and down to Churned (or from Trial back to Account), where the workspace has those stages. The rest land on the timeline under their own names, and an automation can answer any of them by name; a subscription starting and a subscription renewing are things that happened, not places a person stands. Events has the whole catalogue and the lifecycle has the ladder.

Who it lands on

Kicklace looks for the address on whatever object Stripe sent, in this order: customer_details.email, customer_email, receipt_email, billing_details.email, and an expanded customer object's own email.

A subscription event carries no address at all — only a customer id. So the first time an event arrives with both, Kicklace writes that id down as a stripe_customer identity on the person, and from then on a subscription event finds them by it. A checkout or an invoice is what teaches Kicklace who cus_… is, which is why those are worth having switched on even if subscriptions are what you care about.

An event with no address and a customer id nobody here has seen is answered 200 and ignored. Kicklace will not invent a person out of a billing id, because a record with no address and no first touch is not a person — it is a row.

Nothing lands twice

A single subscription payment reaches a webhook three times over: the checkout session, the payment intent, and the paid invoice. They are one purchase, so Kicklace keys them by the invoice where there is one, the payment intent where there is not, and the event's own id where there is neither. That key is the same idempotency_key the events API takes, so a retry — Stripe's, or your own server's — lands once.

Where you see it working

Every post shows in your website's heartbeat on Settings → Your website, beside the posts from your site, with what happened to it: recorded, ignored, refused for a bad signature. A refusal is never silent.

What never happens

  • Nothing is charged and no card is touched. Kicklace reads a webhook; it has no keys into your Stripe account and could not take a payment if it wanted to.
  • Nothing is sent because a payment arrived, unless you wrote an automation that says so — and an automation that emails people is switched on by a person, not by a webhook.
  • A refusal is never used to make Stripe try harder. Anything Kicklace has no use for gets a 200, because a refused delivery is retried for three days and would fill your log with noise. The only 500 Kicklace answers is one where Kicklace itself fell over, which is exactly when you want Stripe to try again.