Migrating from homegrown webhooks to Svix without breaking your customers
You can migrate a production webhook system to Svix without your customers noticing. The playbook: map your existing model onto Svix applications and endpoints, import signing secrets so verification keeps working, backfill entities, run both systems side by side, and cut traffic over one customer at a time with a per-customer flag you can flip back.
What makes migration different from greenfield
A greenfield webhook launch gets to make every decision fresh. A migration inherits a running system with customers attached to it, and those customers have code in production that expects your webhooks to behave exactly the way they behave today. That's the whole constraint. Endpoints must keep receiving, signatures must keep verifying, and there must be no silent gap where events stop flowing and nobody notices.
The good news is that migration is easier than greenfield in one important way: the hard product decisions are already made. You know which event types you send, what the payloads look like, and which customers consume them. What's left is an infrastructure swap, and infrastructure swaps are a solved problem if you sequence them correctly.
One rule before anything else: don't redesign your event catalog mid-migration. It's tempting to fix the naming scheme or restructure payloads while you're in there, but that turns a transparent infrastructure change into a breaking API change for every consumer. Migrate what you have, verify it works, and evolve the catalog afterwards. One change at a time.
Map your existing model onto Svix
Svix's model has three pieces you need to map to. An application represents one of your customers, an endpoint is a URL that customer registered, and an event type describes one kind of event. Whatever your homegrown schema calls these, the mapping is usually mechanical: your tenants or organizations become applications, the URLs in your webhook_subscriptions table become endpoints, and your event-name enum becomes the event type catalog.
The detail that saves you a mapping table later: Svix lets you use your own identifiers as application UIDs. If your internal customer key is org_58a3b9, make that the application UID and every later API call can reference the customer directly, without storing a Svix-generated ID next to your own. Do this from the start of the migration. Retrofitting UIDs after you've stored app_... IDs everywhere is exactly the kind of annoying cleanup work a migration is supposed to spare you.
Event types carry over as-is. Copy your existing names and payload schemas into the Svix event type catalog, including the ones you're not proud of. Consumers subscribed to PAYMENT_SUCCESS_V2 need to keep receiving PAYMENT_SUCCESS_V2.
Keep signatures verifying
This is the part of a migration people are most nervous about, and they're right to be. Your customers' handlers verify a webhook signature with a shared secret, and both the secret and the signing scheme are load-bearing. Break either one and every verifying consumer starts rejecting your webhooks the moment you cut over.
The secret is the easy half. Svix generates a signing secret per endpoint, but it also lets you supply one when creating the endpoint. Base64-encode your existing key bytes and pass them as the endpoint's secret, and the key material your customer already holds stays valid. No re-exchanging secrets over email, no coordinated rotation across your whole customer base.
The scheme is the half that needs honesty. Svix signs the concatenation of the message ID, timestamp, and body with HMAC-SHA256, and delivers the result in the svix-signature header alongside svix-id and svix-timestamp. This is the Standard Webhooks scheme, and it includes replay protection that most homegrown schemes lack. But most homegrown schemes are also different: typically an HMAC over the raw body alone, hex-encoded, in a custom header. Same secret, different signature.
That gives each customer one of three positions. Customers who don't verify signatures at all, and there are more of these than anyone likes to admit, notice nothing. Customers already using a Standard Webhooks compatible verification, or the Svix libraries, work immediately once the secret matches. Customers with custom verification code need to swap it once, and the per-customer cutover in the next sections is what makes that a scheduled, supported change for one customer at a time rather than a breaking change for everyone at once. Point them at the verification libraries, keep the legacy system sending until they confirm, then flip their flag. For cases with stricter compatibility requirements, talk to the Svix team; this is a well-worn path and there are options beyond what fits in a guide.
Backfill applications and endpoints
Before any traffic moves, Svix needs to know about every customer and every registered URL. There are two ways to get there, and most teams use both.
The script-based backfill walks your existing subscriptions table and creates the corresponding applications and endpoints through the API. Write it to be idempotent: create-or-update semantics on every entity, keyed on your UIDs, so running it twice is safe and running it after a partial failure just fills in the gaps. Svix's application create supports get-or-create directly, which does most of this for you. An idempotent backfill script also doubles as a drift checker you can rerun during the dual-write phase to confirm the two systems still agree.
Just-in-time creation is the complement: your sending code creates the application on first send if it doesn't exist. This covers customers created after the backfill ran and makes the ordering between "backfill finished" and "new signups flow in" a non-issue. Backfill for the existing base, just-in-time for everyone new.
Dual-write entities, keep sending from the legacy path
With the backfill done, change your application code so that every subscription change writes to both systems. A customer adds a webhook URL, and your code registers it in your existing tables and creates the Svix endpoint. A customer deletes one, and both are removed. Deliveries still go exclusively through the legacy path; Svix is receiving configuration, not traffic.
This phase exists to prove the mapping under real conditions before anything customer-visible depends on it. Let it run for a week or two, rerun the drift check, and compare what's in the Svix dashboard against your own tables. Discrepancies found now are a bug report. Discrepancies found after cutover are an incident.
This is also the right time to add an Idempotency-Key to your send path, derived deterministically from the domain event, something like invoice-paid-inv_123-2026-08-11T12:00:00Z. It makes retries and replays on your side safe, because sending the same event twice creates one message instead of two. Your legacy system may have never had this. It's one of the quiet upgrades of the migration.
Cut over one customer at a time
The cutover mechanism is a per-customer flag on your send path: if the flag is set, the event goes to Svix via message.create; otherwise it goes down the legacy path. That's the entire mechanism, and its simplicity is the point. Rollback is flipping a flag back, not a deploy.
Sequence the rollout the way you'd sequence any risky rollout. Start with an internal account, then a few friendly customers who know it's coming, then widen in cohorts. For each cohort, watch the delivery logs in the Svix dashboard until you've seen real deliveries succeed, and for verifying customers, until their swapped verification code has accepted real traffic. Then move on. A migration of a few hundred customers done this way takes a few weeks of calendar time and almost none of anyone's attention, which is exactly what you want from it.
Resist the big-bang cutover even though the flag makes it possible. Moving everyone at once turns every edge case you didn't predict into a simultaneous, customer-wide event. Moving cohort by cohort turns the same edge cases into a Tuesday.
Don't drop events at the seam
The cutover moment for each customer has two failure modes worth designing against: an event delivered by neither system, and retries still in flight on the legacy side.
The in-flight problem is about draining, not racing. If your legacy system retries failed deliveries with exponential backoff over a day or two, then at the moment you flip a customer's flag, it may still hold undelivered events for them. Keep the legacy delivery workers running until their retry window has fully drained for migrated customers. New events go to Svix, old retries finish where they started, and the two streams don't conflict because they carry different events.
Gaps get closed with replay. Svix keeps the full delivery history, so if an endpoint missed events during the transition, the recover endpoint re-sends everything that failed since a timestamp you choose, and Replay Missing covers messages that were never attempted against an endpoint at all. The reason overlap is safe rather than scary is consumer-side idempotency: every Svix delivery carries a svix-id the consumer can dedupe on, so the correct bias during a migration is to replay generously. A duplicate costs an idempotency-check hit. A gap costs a support escalation and a customer reconciling missing data.
Watch it, then turn the old system off
While the migration runs, wire up operational webhooks, which are Svix's webhooks about your webhooks. message.attempt.exhausted tells you a delivery ran through the whole retry schedule and failed, message.attempt.failing warns that an endpoint is deteriorating, and endpoint.disabled fires when Svix disables an endpoint after roughly five days of sustained failure. During cutover these are your early-warning system for a customer whose verification swap didn't take: their endpoint starts rejecting everything with 4xx responses, and you find out from an alert instead of from their support ticket. Our webhook monitoring guide covers what to do with these signals long-term.
"Done" is a metrics statement, not a feeling: every customer's flag is flipped, legacy retries have drained, delivery success rates in Svix match or beat what the old system managed, and the operational alerts have been quiet. Keep the legacy path deployable but idle for a few weeks in case something forces a flag back. Then delete it. Actually delete it: the send path, the retry workers, the cron jobs, the subscriptions table sync. A migration that leaves the old system half-alive hasn't reduced your surface area, it has doubled it.
What your customers get out of it
Everything above is about making the migration invisible, but the end state is better than invisible. Your customers gain the App Portal, an embeddable console where they see every delivery attempt, response codes and all, and replay failures themselves. The "did you receive our webhook" support thread, the one your team currently answers by grepping production logs, becomes a page the customer can check on their own.
You gain the things your homegrown system was probably missing a few of: the full retry schedule, automatic endpoint disabling, delivery analytics, and replay tooling, none of it yours to maintain anymore. Teams rarely migrate because the old system is on fire. They migrate because it's fine, and "fine" still costs an engineer-week every quarter. For the wider launch context beyond the migration itself, the Svix implementation guide covers the end-to-end rollout, and our notes on why webhooks as a service lay out the build-versus-buy tradeoff in full.
Frequently asked questions
Will my customers need to change their webhook handler code?
Customers who don't verify signatures notice nothing. Customers using Standard Webhooks compatible verification work as soon as their secret is imported. Customers with custom verification code swap it once for the Svix scheme, which the per-customer cutover lets you schedule with each of them individually.
Can I keep my existing webhook signing secrets?
Yes. Svix lets you supply the signing secret when creating an endpoint instead of generating a new one, so the key material your customers already hold stays valid and no secrets need to be re-exchanged.
How long should the legacy and new systems run in parallel?
Dual-write configuration for a week or two before moving traffic, then cut over customer by customer over a few weeks. Keep the legacy system deployable until every cohort is stable and its retry window has drained, then remove it completely.
What happens to events that fail during the cutover?
Svix keeps full delivery history, so failed deliveries can be recovered in bulk from a chosen timestamp and never-attempted messages can be replayed to an endpoint. Since every delivery carries a svix-id consumers can dedupe on, replaying generously is safe.