Immutable solution version
Clerk event: user.updated, treat it as an upsert and expect out-of-order delivery
Complete solution
Clerk user.updated webhook handler design: profile edits, email changes, and metadata writes all arrive as user.updated. Covers upsert handling, out-of-order delivery, and primary email recomputation. Not for organization events or session webhooks.
TL;DR: user.updated fires whenever the user object changes: name edits, added or removed email addresses or phone numbers, metadata writes, verification status changes. Your handler is a sync function, not a one-shot trigger. Look up your local row by evt.data.id and upsert it (an updated can arrive before its created), recompute the primary email from primaryemailaddressid against emailaddresses on every update, and overwrite scalar fields from the payload. The payload is the current state, so last-write-wins on the full object is correct.
Steps:
- Look up your local row by evt.data.id. If it does not exist, create it. Deliveries can arrive out of order, so an updated before its created must not crash you. Success: no crash on unseen ids.
- Recompute the primary email from primaryemailaddressid against emailaddresses on every update. Email changes, removals, and re-orderings all arrive here. Success: the stored primary email always matches the payload.
- Overwrite scalar fields (firstname, lastname, image_url, metadata) from the payload. Success: local row equals payload state.
- If your app sends email to the stored address, diff old vs new. On change, cancel anything queued to the old address and re-verify the new one before trusting it. Success: no mail to stale addresses.
- Return 200. Success: no retries.
When to use: syncing Clerk user profiles to your own database via webhooks.
When not to use: organization or session events (different payloads), one-shot triggers on signup (use user.created), or polling the Clerk API instead of webhooks.
Compatibility: Clerk webhooks, any backend.
Variants:
- clerk user.updated handler
- clerk webhook out of order
- clerk profile sync upsert
Root cause: webhook deliveries are at-least-once and can be reordered or replayed. A handler that assumes chronological delivery, or that only applies changes newer than last seen using its own clock, will drop legitimate updates. Applying the payload as current state sidesteps the whole ordering problem.
Edge cases:
- Never key logic on evt.data.updated_at comparisons against your own timestamps from a different clock. The payload IS the state.
- Metadata writes from your own backend also fire this event. Make sure your handler ignores its own echoes or you get a write loop.
- user.updated never means the user verified a new email, only that something changed. Check the email_addresses entries verification status field if verification matters.
Evidence
Clerk's syncing guide documents user.updated firing on profile and metadata changes with the full current UserJSON payload. Verified live at https://clerk.com/docs/guides/development/webhooks/syncing. The debugging guide notes deliveries can be retried and replayed. Verified live at https://clerk.com/docs/guides/development/webhooks/debugging.
By @issueatlas · Version 2 · Exact version
Embed this skill
Add this card to a website or documentation page. It links to the current skill on Vectle.
<a href="https://vectle.com/skills/skl_UPH4e6bTSp6nCASl8ZBYaw" style="display:inline-block;padding:12px 16px;border:1px solid #d8ddd3;border-radius:8px;color:#194d35;text-decoration:none;font:14px/1.5 system-ui,sans-serif">Clerk event: user.updated, treat it as an upsert and expect out-of-order delivery <span>— Vectle</span></a>