[BWS] Feat: webhooks support for external services - #4216
Open
Gamboster wants to merge 7 commits into
Open
Conversation
Gamboster
marked this pull request as draft
August 10, 2026 19:29
Gamboster
force-pushed
the
feat/externalServicesWebhooks
branch
from
August 13, 2026 15:56
0ee627c to
15d9dc4
Compare
Gamboster
force-pushed
the
feat/externalServicesWebhooks
branch
from
August 18, 2026 19:04
aae7a67 to
0b13750
Compare
Gamboster
marked this pull request as ready for review
August 19, 2026 14:06
Gamboster
force-pushed
the
feat/externalServicesWebhooks
branch
from
August 19, 2026 18:07
bd4a5be to
5491731
Compare
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Critical authentication gaps and unresolved delivery, ordering, and partner-specific correctness issues block approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds webhook support for six external partners, including signature verification, event persistence, ordering checks, and Braze tracking.
Changes:
- Adds partner-specific webhook routes and verification.
- Adds deduplication and stale-event detection.
- Adds Braze tracking, configuration, and integration tests.
File summaries
| File | Description |
|---|---|
packages/bitcore-wallet-service/test/integration/externalservices/transak.test.ts |
Tests Transak webhook handling. |
packages/bitcore-wallet-service/test/integration/externalservices/simplex.test.ts |
Tests Simplex JWT verification. |
packages/bitcore-wallet-service/test/integration/externalservices/sardine.test.ts |
Tests Sardine webhook parsing. |
packages/bitcore-wallet-service/test/integration/externalservices/ramp.test.ts |
Tests Ramp signatures and mapping. |
packages/bitcore-wallet-service/test/integration/externalservices/moonpay.test.ts |
Tests MoonPay webhook verification. |
packages/bitcore-wallet-service/test/integration/externalservices/banxa.test.ts |
Tests Banxa signatures and mapping. |
packages/bitcore-wallet-service/src/lib/storage.ts |
Persists, deduplicates, and orders webhook events. |
packages/bitcore-wallet-service/src/lib/routes/webhooks.ts |
Implements shared webhook routing and processing. |
packages/bitcore-wallet-service/src/lib/routes/setup.ts |
Captures raw webhook request bodies. |
packages/bitcore-wallet-service/src/lib/model/onrampWebhookEvent.ts |
Defines normalized webhook events. |
packages/bitcore-wallet-service/src/lib/model/index.ts |
Exports the webhook event model. |
packages/bitcore-wallet-service/src/lib/expressapp.ts |
Registers webhook routes. |
packages/bitcore-wallet-service/src/lib/braze.ts |
Adds Braze event tracking. |
packages/bitcore-wallet-service/src/externalservices/transak.ts |
Fetches tokens and verifies Transak webhooks. |
packages/bitcore-wallet-service/src/externalservices/simplex.ts |
Verifies and maps Simplex webhooks. |
packages/bitcore-wallet-service/src/externalservices/sardine.ts |
Verifies and maps Sardine webhooks. |
packages/bitcore-wallet-service/src/externalservices/ramp.ts |
Adds Ramp callbacks and webhook verification. |
packages/bitcore-wallet-service/src/externalservices/moonpay.ts |
Verifies and maps MoonPay webhooks. |
packages/bitcore-wallet-service/src/externalservices/banxa.ts |
Verifies and maps Banxa webhooks. |
packages/bitcore-wallet-service/src/config.ts |
Adds webhook configuration fields. |
packages/bitcore-wallet-service/bws.example.config.js |
Documents example webhook settings. |
Review details
Suppressed comments (2)
packages/bitcore-wallet-service/src/lib/routes/webhooks.ts:102
- The added tests call each service parser directly, so this shared endpoint behavior is untested: raw-body capture, HTTP status mapping, persistence failures, duplicate/stale Braze suppression, and acknowledgement are never exercised through Express. Add endpoint-level tests in the existing Express app suite for these branches.
export function registerWebhookRoutes(router: express.Router, context: RouteContext) {
packages/bitcore-wallet-service/src/lib/storage.ts:1970
- The new idempotency and ordering implementation has no coverage in the existing storage test suite; all added tests invoke provider parsers directly. Add storage-level cases for first insert, duplicate insert, stale/newer ordering, and TTL field creation so the central delivery guarantees are exercised.
async storeOnrampWebhookEvent({ event }: {
event: IOnrampWebhookEvent;
}): Promise<IStoreOnrampWebhookEventResult> {
- Files reviewed: 21/21 changed files
- Comments generated: 18
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+326
to
+328
| } else { | ||
| logger.warn('Banxa webhook: no secretKey configured, skipping signature verification'); | ||
| } |
Comment on lines
+598
to
+600
| } else { | ||
| logger.warn('MoonPay webhook: no webhookSecretKey configured, skipping signature verification'); | ||
| } |
Comment on lines
+312
to
+315
| const publicKeys: { key: string; env: string }[] = [ | ||
| { key: (config.ramp.production as any)?.webhookSigningKey, env: 'production' }, | ||
| { key: (config.ramp.sandbox as any)?.webhookSigningKey, env: 'sandbox' } | ||
| ]; |
| // ourselves append to webhookStatusUrl/offrampWebhookV3Url when | ||
| // requesting the signed widget URL (see rampGetSignedPaymentUrl) - Ramp | ||
| // echoes it back verbatim on this call. | ||
| userId: typeof req.query?.userId === 'string' ? req.query.userId : undefined, |
Comment on lines
+259
to
+260
| const sigHeader = req.headers['x-sardine-signature'] as string | undefined; | ||
| if (sigHeader && webhookSecrets.length) { |
Comment on lines
+91
to
+97
| this.request.post( | ||
| URL, | ||
| { | ||
| headers, | ||
| body, | ||
| json: true | ||
| }, |
Comment on lines
+66
to
+68
| if (!isDuplicate) { | ||
| // Fire-and-forget: analytics tracking must never block or fail the webhook ack. | ||
| brazeService |
| ...(event.isEmbedded !== undefined && { isEmbedded: event.isEmbedded }) | ||
| }; | ||
|
|
||
| console.log(`Storing onramp webhook event ${eventName} for partner ${event.partner} with id ${id} storedEvent:`, JSON.stringify(storedEvent)); |
| console.log(`Storing onramp webhook event ${eventName} for partner ${event.partner} with id ${id} storedEvent:`, JSON.stringify(storedEvent)); | ||
|
|
||
| try { | ||
| await this.db.collection(collections.ONRAMP_WEBHOOK_EVENTS).insertOne(storedEvent); |
Comment on lines
+2010
to
+2012
| let isStale = false; | ||
| if (storedEvent.updatedAt) { | ||
| const newer = await this.db.collection(collections.ONRAMP_WEBHOOK_EVENTS).findOne( |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Ticket Reference: RN-2714
This PR adds webhook endpoints for all six partners so BWS can react to status changes (processing, completed, failed, refunded, etc.) in real time instead of relying purely on client-side polling, while guarding against the usual webhook pitfalls: unverified senders, duplicate/retried deliveries, and out-of-order delivery.
Webhook processing will be rolled out gradually: Not all endpoints will be operational immediately. Initially, we will enable the Moonpay webhook endpoint until we verify that everything is working as expected with this single partner. (Until we enable webhooks in the configuration of each external service, the endpoints will not receive traffic.) Then we will continue with the other partners, one by one, until all are operational simultaneously.
The first thing we will do upon receiving a call is check that it is not a duplicate or out-of-order status and then send an event to Braze (a requirement of the marketing team). Any additional processing that needs to be done will be added in the future in another PR.
Changelog
POST /v1/service/{partner}/webhookroutes in a newsrc/lib/routes/webhooks.ts, with a shared handler that verifies the signature, persists the event, and always responds so the partner doesn't retry unnecessarily.Moonpay-Signature-V2: t=...,s=...), with support for both the standard and embedded webhook secrets.X-Signature-SHA256, including expiry (exp) enforcement.fast-json-stable-stringify-equivalent) serialization of the body, sent inX-Body-Signature.POST\n{path}\n{nonce}\n{body}, sent viaAuthorization: Bearer {apiKey}:{signature}:{nonce}.X-Sardine-Signature.POST /partners/api/v2/refresh-token), now fetched and cached per environment instead of a static secret.onramp_webhook_eventscollection (storage.ts) keyed by a deterministic id (partner + env + event + externalId + delivery version), so retried deliveries from a partner are safely ignored.src/lib/braze.ts) that fires a "BWS - ONRAMP Webhook Received" track event per unique delivery (skipped for duplicates/stale events and when no user id is available).userIdquery param to the signedwebhookStatusUrl/offrampWebhookV3Urlit generates, which Ramp echoes back on the webhook call.OnrampWebhookEventmodel (src/lib/model/onrampWebhookEvent.ts) as the common shape used across all partners' webhook handlers and storage.bws.example.config.js/config.ts) for webhook secrets/signing keys per partner and environment.test/integration/externalservices/*.test.ts) covering signature verification (valid/invalid/missing signature, missing secret configured), payload parsing, and error handling for each partner's new*HandleWebhookmethod.Testing Notes
bws.example.config.js) for signature verification to be enforced; if left unset, verification is skipped with a warning logged (useful for local testing without real credentials, but should be set in staging/production).npm testinpackages/bitcore-wallet-service) exercise the signature verification and field-mapping logic per partner in isolation, including duplicate/invalid-signature/missing-header/missing-config edge cases.