Overview
SolvaPay webhooks push event notifications to your application via HTTP POST requests whenever significant business events occur — a payment succeeds, a purchase is cancelled, a customer is created, and so on. Instead of polling the API, you register an endpoint URL and SolvaPay delivers a signed JSON payload for each event. Your server verifies the signature, processes the event, and returns a2xx response.
How it works
Quick Start
1. Install the SDK
2. Create a webhook endpoint
In SolvaPay Console, go to Settings -> Webhooks and add your endpoint URL:whsec_…) — you will need it to verify signatures.
3. Handle incoming webhooks
Payload Format
Every webhook POST contains a JSON body with this structure:HTTP Headers
| Header | Example | Description |
|---|---|---|
SV-Signature | t=1740000000,v1=a1b2c3… | HMAC signature for verification |
SV-Event-Id | evt_abc123def456 | Unique event identifier |
SV-Delivery | dlv_xyz789 | Unique delivery attempt ID (use for idempotency) |
Content-Type | application/json | Always JSON |
User-Agent | SolvaPay/1.0 (+webhooks) | Identifies the sender |
Signature Verification
Every webhook includes anSV-Signature header. You must verify it to confirm the
request genuinely came from SolvaPay and has not been tampered with.
How the signature is computed
- SolvaPay takes the current Unix timestamp and the raw JSON body.
- It concatenates them as
"{timestamp}.{rawBody}". - It computes an HMAC-SHA256 of that string using your signing secret (including the
whsec_prefix) as the key. - The result is sent as
t={timestamp},v1={hex_digest}.
Using the SDK
verifyWebhook returns a typed WebhookEvent object with full IntelliSense for event
types and payload fields:
verifyWebhook throws a SolvaPayError if the signature is invalid or older than 5 minutes.
Edge Runtimes
For Vercel Edge Functions, Cloudflare Workers, or Deno Deploy, import from the edge entry point. It uses the Web Crypto API and returns aPromise:
Manual verification (without the SDK)
Event Types
SolvaPay emits 46 event types spanning the full lifecycle — from customer and purchase creation through payments, refunds, disputes, payouts, credits, metered usage, and catalog changes. The canonical list is also available programmatically fromGET /v1/sdk/webhooks/event-types and is what powers both the SDK WebhookEventType
type and the event multiselect in the Console.
Customer Events
| Event | Description |
|---|---|
customer.created | A new customer record has been created (hosted auth, API, or SDK sync). |
customer.updated | A customer record has been updated. |
customer.deleted | A customer record has been deleted. |
Purchase & Subscription Events
The complete purchase/subscription state machine:| Event | Description |
|---|---|
purchase.created | A new purchase (subscription or one-time) was created. Includes trial starts. |
purchase.activated | A purchase activated after its first successful payment. |
purchase.updated | A purchase was modified (generic change not covered by a more specific event). |
purchase.trial_ending | A trial is ending soon. |
purchase.trial_converted | A trial converted to a paid subscription. |
purchase.suspended | A purchase was suspended (trial ended, payment required). |
purchase.past_due | A recurring payment failed and dunning has started. |
purchase.cancellation_scheduled | Cancellation scheduled for the end of the billing period. |
purchase.cancelled | A purchase was cancelled. |
purchase.reactivated | A scheduled cancellation was reversed. |
purchase.expired | A purchase expired (naturally, or superseded by a plan switch). |
purchase.renewed | A subscription successfully renewed (recurring charge). |
purchase.renewal_reminder | The upcoming-renewal reminder window was reached. |
purchase.refunded | A purchase’s entitlement was revoked by a full refund. |
purchase.plan_changed | A subscription was upgraded or downgraded to a new plan. |
Payment, Refund & Dispute Events
| Event | Description |
|---|---|
payment.succeeded | A payment was successfully processed. |
payment.failed | A payment attempt failed. |
payment.refunded | A refund was successfully processed. |
payment.refund_failed | A refund attempt failed. |
payment.refund_pending | A refund was initiated and is still in-flight. |
payment.canceled | A payment intent was canceled. |
payment.disputed | A chargeback/dispute was opened. |
payment.dispute_closed | A dispute was resolved (won or lost). |
Payout Events
| Event | Description |
|---|---|
payout.paid | A provider payout settled. |
payout.failed | A provider payout failed. |
Checkout Session Events
| Event | Description |
|---|---|
checkout_session.created | A hosted checkout session was created. |
checkout_session.completed | A hosted checkout session was completed (converted). |
checkout_session.expired | A hosted checkout session expired (abandoned). |
Credit Events
| Event | Description |
|---|---|
customer.credit.topped_up | A customer’s credit balance was topped up. |
customer.credit.low_balance | A customer’s credit balance is running low. |
customer.credit.exhausted | A customer’s credit balance reached zero. |
customer.credit.debited | A customer’s credit was debited by usage. |
customer.credit.granted | Free credit was granted to a customer. |
customer.credit.adjusted | A customer’s credit balance was manually adjusted. |
Usage & Metering Events
| Event | Description |
|---|---|
usage.charged | A metered / usage-based charge was processed. |
usage.recorded | A metered usage event was recorded. |
usage.reset | Usage counters were reset for a new period. |
Catalog Events
| Event | Description |
|---|---|
product.created | A product was created. |
product.updated | A product was updated. |
product.archived | A product was archived. |
plan.created | A plan was created. |
plan.updated | A plan was updated. |
plan.archived | A plan was archived. |
Reactivation: undoing a pending cancellation via
reactivateRenewal emits
purchase.reactivated.Plan switching: calling activatePlan with a different plan emits purchase.expired
for the old purchase, purchase.created for the new one, and purchase.plan_changed.Choosing which events to receive
By default, every endpoint receives all event types. You can subscribe an endpoint to a specific subset so your handler only gets the events it cares about.In the Console
Under Settings → Webhooks, expand an endpoint and pick Selected events to choose specific event types (grouped by category). Leave it on All events to receive everything — including any new event types SolvaPay adds later.Via the API
PassenabledEvents when creating or updating an endpoint. An empty or omitted array
means “all events”; a non-empty array subscribes to only those types.
Event Payloads
payment.succeeded
payment.failed
payment.refunded
payment.refund_failed
purchase.created
purchase.updated
purchase.cancelled
purchase.expired
purchase.suspended
customer.created
This payload can include aproduct field with a product reference.
SolvaPay only populates product when the customer is created through the no-code MCP integration’s
hosted OAuth for a product-linked client.
To learn more about SDK customer syncing, see
/sdks/typescript/setup/core-concepts.
customer.updated
customer.deleted
Thecustomer.deleted payload contains only the customer id and a created timestamp.
Other fields are not included since the record has been removed.
Customer sync with the TypeScript SDK
When you integrate with the TypeScript SDK (instead of the no-code MCP integration’s hosted OAuth), sync your users withensureCustomer or the Next.js syncCustomer helper.
This flow is idempotent. SolvaPay first looks up a customer by externalRef, then creates one
if none exists.
When a new customer is created this way, SolvaPay sends a customer.created webhook event with
product set to null.
/sdks/typescript/setup/core-concepts
and /sdks/typescript/guides/nextjs.
checkout_session.created
checkout_session.completed and checkout_session.expired carry the same object shape,
with status set to used and expired respectively.
Purchase lifecycle events
Allpurchase.* events share the purchase object shape shown under
purchase.created; status reflects the new state. Two events add
extra context:
Dispute & payout events
Credit events
credits is the running balance after the movement; amount is the signed delta.
customer.credit.low_balance and customer.credit.exhausted fire automatically as the
balance crosses the warning threshold and zero.
Usage & metering events
usage.reset fires when counters roll over for a new billing period.
Catalog events
Best Practices
Always verify signatures
Always verify signatures
Never process a webhook without checking
SV-Signature. Without verification, any
third party could forge requests to your endpoint.Use the delivery ID for idempotency
Use the delivery ID for idempotency
The
SV-Delivery header is unique per delivery attempt. Store processed delivery IDs
and skip duplicates to avoid processing the same event twice.Respond within 10 seconds
Respond within 10 seconds
Return a
2xx as quickly as possible. If your handler needs to do heavy processing
(e.g. sending emails, updating external systems), acknowledge the webhook immediately
and move the work to a background queue.Handle unknown events gracefully
Handle unknown events gracefully
Log and return
200 for event types you don’t recognise. This way the delivery is
marked successful and SolvaPay won’t retry it.Keep the raw body intact
Keep the raw body intact
Signature verification requires the exact body bytes. Always read the body as a raw
string (
request.text() or express.raw()) before parsing it as JSON.Retry Schedule
If your endpoint returns a non-2xx status code or times out, SolvaPay retries delivery with exponential backoff:| Retry | Delay after previous |
|---|---|
| 1 | 5 minutes |
| 2 | 15 minutes |
| 3 | 1 hour |
| 4 | 6 hours |
| 5 | 24 hours |
| 6 | 48 hours |
| 7+ | 72 hours |
Testing Locally
Using ngrok
Sending a test event
Use the dashboard test button, or call the API directly:payment.succeeded test event to the endpoint so you can verify your handler
and signature verification are working correctly.