Skip to content

Conversation

@MattBro
Copy link
Contributor

@MattBro MattBro commented Dec 5, 2025

Problem

When Vercel customers pay their invoices, Vercel sends a marketplace.invoice.paid webhook. We need to receive this and mark the corresponding Stripe invoice as paid so our billing flows work correctly.

Changes

  • Add /webhooks/vercel endpoint with HMAC-SHA1 signature verification
  • Forward marketplace.* events to billing service
  • Use capture_exception for error tracking

How did you test this code?

pytest ee/api/vercel/test/test_vercel_webhooks.py -v

E2E: Created Stripe invoice → finalized → Vercel webhook received → Stripe invoice marked paid.

🤖 Generated with Claude Code

MattBro and others added 2 commits December 5, 2025 16:56
Adds PostHog endpoint to receive webhooks from Vercel and forward
billing events (marketplace.*) to the billing service.

- Add /webhooks/vercel endpoint with HMAC-SHA1 signature verification
- Add handle_billing_provider_webhook to BillingManager for forwarding
- Support configurationId in various payload locations
- Return 500 on errors to trigger Vercel retry

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Consolidate to single webhook URL following the /webhooks/<provider>
convention, which is more standard for external webhooks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@MattBro
Copy link
Contributor Author

MattBro commented Dec 5, 2025

Companion PR: https://github.com/PostHog/billing/pull/1669

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Apply Clean Code principles to improve readability:
- Extract _is_valid_signature() for signature verification
- Extract _extract_config_id() for config ID lookup logic
- Extract _is_billing_event() for event type checking
- Extract _get_integration() for database lookup
- Extract _forward_to_billing_service() for billing call
- Remove implementation comments (code is self-documenting)
- Flatten main function with early returns

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@MattBro MattBro force-pushed the matt/vercel-billing-2b-webhook branch from afecd62 to ddffa1d Compare December 5, 2025 23:18
@MattBro MattBro requested review from a team and rafaeelaudibert December 6, 2025 11:22
Already applied in ee/urls.py when registering the route.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@MattBro MattBro force-pushed the matt/vercel-billing-2b-webhook branch from ddffa1d to 1249ea3 Compare December 6, 2025 11:24
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're phasing ee out. You can add this to the main posthog folder.

Even better you can talk to the devex team to figure out where this kind of code should live. Ideally it should live inside the products folder but I have a hard time coming up with a "product" to file this under

Copy link
Contributor

@pawel-cebula pawel-cebula Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have much context on the transition from ee to posthog but won't it cause issues for hobby installs to move it now on its own? At least until we complete the migration and have some other way to separate the APIs that are only relevant for cloud.

Copy link
Member

@rafaeelaudibert rafaeelaudibert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will let Billing give you the final stamp

Copy link
Contributor

@pawel-cebula pawel-cebula left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Use capture_exception for Sentry error tracking

😮

Copy link
Contributor

@pawel-cebula pawel-cebula Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have much context on the transition from ee to posthog but won't it cause issues for hobby installs to move it now on its own? At least until we complete the migration and have some other way to separate the APIs that are only relevant for cloud.


logger = structlog.get_logger(__name__)

BILLING_EVENT_PREFIX = "marketplace."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to forward all marketplace events if we're handling just one? Maybe also keep a list of speciifc events that's in sync with billing?

MattBro and others added 7 commits December 8, 2025 11:21
- Only check documented fields: payload.installationId and payload.configuration.id
- Remove undocumented configurationId checks
- Update tests to use installationId (per Vercel webhook API docs)
- Add reference to Vercel docs in comment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Both fields have the same value per docs, no need to check both.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Makes misconfiguration more visible via capture_exception.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Change URL from /api/billing-provider-webhook to /api/webhooks/billing-provider
- Filter to only forward marketplace.invoice.paid (not all marketplace.* events)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
secret = getattr(settings, "VERCEL_CLIENT_INTEGRATION_SECRET", None)
if not secret:
logger.error("vercel_webhook_missing_secret")
capture_exception(Exception("VERCEL_CLIENT_INTEGRATION_SECRET not configured"), {})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MattBro I don't believe just capturing an exception is enough for this. I'm not monitoring exceptions closely. The best solution here is something that feeds into incident.io. Nice to have, ofc, but we'll miss this if we never implement it. Add to the polishing list - if you arleady have one

Copy link
Member

@rafaeelaudibert rafaeelaudibert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good from my end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants