Repository: stripe/stripe-cli (go)

Issue:
Stripe-cli 1.6.0 to 1.6.4 ignores valid payloads and sees them as malformed events
### Issue
Webhook payloads for customer.subscription.updated were filtered out by Stripe CLI version 1.6.4 instead of being forwarded to the desired endpoint

### Expected Behavior
web hooks get forwarded to configured endpoint

### Steps to reproduce
Set your default api version to 2015-09-03
Use stripe-cli any version after and including 1.6.0
Set Stripe CLI to listen to all all web hooks and forward them somewhere
Make sure logging level is set to debug
Update a subscription via the dashboard
Look for the incoming payload followed by time="Wed, 07 Jul 2021 14:20:12 UTC" level=debug msg="Received malformed event from Stripe, ignoring"

### Traceback
```
time="Wed, 07 Jul 2021 14:20:10 UTC" level=debug msg="Incoming message" message="{"webhook_id":"1625667607-wh_BjYS4iHtNOuaoS","webhook_conversation_id":"1625667607-wc_LvR5FnwsjOWiVG","event_payload":"{\n  \"id\": \"evt_1JAbbfCJZWINMVesDB7BL9Gd\",\n  \"object\": \"event\",\n  \"api_version\": \"2020-08-27\",\n  \"created\": 1625667607,\n  \"data\": {\n    \"object\": {\n      \"id\": \"sub_Jo9k3qUoCL2CdR\",\n      \"object\": \"subscription\",\n      \"application_fee_percent\": null,\n      \"automatic_tax\": {\n        \"enabled\": false\n      },\n      \"billing_cycle_anchor\": 1628734656,\n      \"billing_thresholds\": null,\n      \"cancel_at\": null,\n      \"cancel_at_period_end\": false,\n      \"canceled_at\": null,\n      \"collection_method\": \"charge_automatically\",\n      \"created\": 1625651544,\n      \"current_period_end\": 1628734656,\n      \"current_period_start\": 1625651544,\n      \"customer\": \"cus_Jo9KjcY44fGzj1\",\n      \"days_until_due\": null,\n      \"default_payment_method\": null,\n      \"default_source\": null,\n      \"default_tax_rates\": [\n\n      ],\n      \"discount\": null,\n      \"ended_at\": null,\n      \"items\": {\n        \"object\": \"list\",\n        \"data\": [\n          {\n            \"id\": \"si_Jo9kt4sglulMkz\",\n            \"object\": \"subscription_item\",\n            \"billing_thresholds\": null,\n            \"created\": 1625651545,\n            \"metadata\": {\n              \"orderCount\": \"1\",\n              \"lastOrderId\": \"1033825\"\n            },\n            \"plan\": {\n              \"id\": \"price_1HpECzCJZWINMVesSprmNjl5\",\n              \"object\": \"plan\",\n              \"active\": true,\n              \"aggregate_usage\": null,\n              \"amount\": 1680,\n              \"amount_decimal\": \"1680\",\n              \"billing_scheme\": \"per_unit\",\n              \"created\": 1612534011,\n              \"currency\": \"gbp\",\n              \"interval\": \"day\",\n              \"interval_count\": 28,\n              \"livemode\": false,\n              \"metadata\": {\n                \"boxes\": \"1\",\n                \"boxSize\": \"28\",\n                \"dailyConsumption\": \"1\",\n                \"baseSku\": \"TURME\",\n                \"retired\": \"0\",\n                \"productVariant\": \"TURME28\"\n              },\n              \"nickname\": \"2021 Turmeric+ 28\",\n              \"product\": \"prod_IQ4LQ4YFeqYB20\",\n              \"tiers_mode\": null,\n              \"transform_usage\": null,\n              \"trial_period_days\": 24,\n              \"usage_type\": \"licensed\"\n            },\n            \"price\": {\n              \"id\": \"price_1HpECzCJZWINMVesSprmNjl5\",\n              \"object\": \"price\",\n              \"active\": true,\n              \"billing_scheme\": \"per_unit\",\n              \"created\": 1612534011,\n              \"currency\": \"gbp\",\n              \"livemode\": false,\n              \"lookup_key\": null,\n              \"metadata\": {\n                \"boxes\": \"1\",\n                \"boxSize\": \"28\",\n                \"dailyConsumption\": \"1\",\n                \"baseSku\": \"TURME\",\n                \"retired\": \"0\",\n                \"productVariant\": \"TURME28\"\n              },\n              \"nickname\": \"2021 Turmeric+ 28\",\n              \"product\": \"prod_IQ4LQ4YFeqYB20\",\n              \"recurring\": {\n                \"aggregate_usage\": null,\n                \"interval\": \"day\",\n                \"interval_count\": 28,\n                \"trial_period_days\": 24,\n                \"usage_type\": \"licensed\"\n              },\n              \"tiers_mode\": null,\n              \"transform_quantity\": null,\n              \"type\": \"recurring\",\n              \"unit_amount\": 1680,\n              \"unit_amount_decimal\": \"1680\"\n            },\n            \"quantity\": 1,\n            \"subscription\": \"sub_Jo9k3qUoCL2CdR\",\n            \"tax_rates\": [\n\n            ]\n          }\n        ],\n        \"has_more\": false,\n        \"total_count\": 1,\n        \"url\": \"/v1/subscription_items?subscription=sub_Jo9k3qUoCL2CdR\"\n      },\n      \"latest_invoice\": \"in_1JAXQbCJZWINMVes1Sjxxxh6\",\n      \"livemode\": false,\n      \"metadata\": {\n        \"shipping\": \"true\",\n        \"shippingUpgraded\": \"false\",\n        \"source\": \"magento\",\n        \"orderCount\": \"1\",\n        \"lastOrderId\": \"1033825\"\n      },\n      \"next_pending_invoice_item_invoice\": null,\n      \"pause_collection\": null,\n      \"pending_invoice_item_interval\": null,\n      \"pending_setup_intent\": null,\n      \"pending_update\": null,\n      \"plan\": {\n        \"id\": \"price_1HpECzCJZWINMVesSprmNjl5\",\n        \"object\": \"plan\",\n        \"active\": true,\n        \"aggregate_usage\": null,\n        \"amount\": 1680,\n        \"amount_decimal\": \"1680\",\n        \"billing_scheme\": \"per_unit\",\n        \"created\": 1612534011,\n        \"currency\": \"gbp\",\n        \"interval\": \"day\",\n        \"interval_count\": 28,\n        \"livemode\": false,\n        \"metadata\": {\n          \"boxes\": \"1\",\n          \"boxSize\": \"28\",\n          \"dailyConsumption\": \"1\",\n          \"baseSku\": \"TURME\",\n          \"retired\": \"0\",\n          \"productVariant\": \"TURME28\"\n        },\n        \"nickname\": \"2021 Turmeric+ 28\",\n        \"product\": \"prod_IQ4LQ4YFeqYB20\",\n        \"tiers_mode\": null,\n        \"transform_usage\": null,\n        \"trial_period_days\": 24,\n        \"usage_type\": \"licensed\"\n      },\n      \"quantity\": 1,\n      \"schedule\": null,\n      \"start_date\": 1625651544,\n      \"status\": \"trialing\",\n      \"transfer_data\": null,\n      \"trial_end\": 1628734656,\n      \"trial_start\": 1625651544\n    },\n    \"previous_attributes\": {\n      \"billing_cycle_anchor\": 1627612809,\n      \"current_period_end\": 1627612809,\n      \"trial_end\": 1627612809\n    }\n  },\n  \"livemode\": false,\n  \"pending_webhooks\": 6,\n  \"request\": {\n    \"id\": \"req_oVXbEfceo8VdC1\",\n    \"idempotency_key\": null\n  },\n  \"type\": \"customer.subscription.updated\"\n}","http_headers":{"Content-Type":"application/json; charset=utf-8","Cache-Control":"no-cache","User-Agent":"Stripe/1.0 (+https://stripe.com/docs/webhooks)","Accept":"*/*; q=0.5, application/xml","Stripe-Signature":"t=1625667607,v1=2e0492f6578693adce5e581ef3df932c6d733970fbf18ab209a0efb51e081d44,v0=d090bb7ba128744a9b6d2a3898a2f15d08be6d3b2fccf3dfea83b3bb7c3f6207"},"endpoint":{"api_version":"2020-08-27"},"type":"webhook_event"}" prefix=websocket.Client.readPump
time="Wed, 07 Jul 2021 14:20:10 UTC" level=debug msg="Processing webhook event" prefix=proxy.Proxy.processWebhookEvent webhook_converesation_id="1625667607-wc_LvR5FnwsjOWiVG" webhook_id="1625667607-wh_BjYS4iHtNOuaoS"
time="Wed, 07 Jul 2021 14:20:10 UTC" level=debug msg="Received event with non-default API version, ignoring" api_version=2020-08-27 prefix=proxy.Proxy.filterWebhookEvent
time="Wed, 07 Jul 2021 14:20:11 UTC" level=debug msg="Sending ping message" prefix=websocket.Client.writePump
time="Wed, 07 Jul 2021 14:20:11 UTC" level=debug msg="Received pong message" prefix=websocket.Client.readPump
time="Wed, 07 Jul 2021 14:20:12 UTC" level=debug msg="Incoming message" message="{"webhook_id":"1625667607-wh_zy7r5i11RIzuOD","webhook_conversation_id":"1625667607-wc_iLI9nRX1npWE26","event_payload":"{\n  \"id\": \"evt_1JAbbfCJZWINMVesDB7BL9Gd\",\n  \"object\": \"event\",\n  \"api_version\": \"2015-09-03\",\n  \"created\": 1625667607,\n  \"data\": {\n    \"object\": {\n      \"id\": \"sub_Jo9k3qUoCL2CdR\",\n      \"object\": \"subscription\",\n      \"application_fee_percent\": null,\n      \"automatic_tax\": {\n        \"enabled\": false\n      },\n      \"billing\": \"charge_automatically\",\n      \"billing_cycle_anchor\": 1628734656,\n      \"billing_thresholds\": null,\n      \"cancel_at\": null,\n      \"cancel_at_period_end\": false,\n      \"canceled_at\": null,\n      \"collection_method\": \"charge_automatically\",\n      \"created\": 1625651544,\n      \"current_period_end\": 1628734656,\n      \"current_period_start\": 1625651544,\n      \"customer\": \"cus_Jo9KjcY44fGzj1\",\n      \"days_until_due\": null,\n      \"default_payment_method\": null,\n      \"default_source\": null,\n      \"default_tax_rates\": [\n\n      ],\n      \"discount\": null,\n      \"ended_at\": null,\n      \"invoice_customer_balance_settings\": {\n        \"consume_applied_balance_on_void\": true\n      },\n      \"items\": {\n        \"object\": \"list\",\n        \"data\": [\n          {\n            \"id\": \"si_Jo9kt4sglulMkz\",\n            \"object\": \"subscription_item\",\n            \"billing_thresholds\": null,\n            \"created\": 1625651545,\n            \"metadata\": {\n              \"orderCount\": \"1\",\n              \"lastOrderId\": \"1033825\"\n            },\n            \"plan\": {\n              \"id\": \"price_1HpECzCJZWINMVesSprmNjl5\",\n              \"object\": \"plan\",\n              \"active\": true,\n              \"aggregate_usage\": null,\n              \"amount\": 1680,\n              \"amount_decimal\": \"1680\",\n              \"billing_scheme\": \"per_unit\",\n              \"created\": 1612534011,\n              \"currency\": \"gbp\",\n              \"interval\": \"day\",\n              \"interval_count\": 28,\n              \"livemode\": false,\n              \"metadata\": {\n                \"boxes\": \"1\",\n                \"boxSize\": \"28\",\n                \"dailyConsumption\": \"1\",\n                \"baseSku\": \"TURME\",\n                \"retired\": \"0\",\n                \"productVariant\": \"TURME28\"\n              },\n              \"name\": \"2021 Turmeric+ 28\",\n              \"nickname\": \"2021 Turmeric+ 28\",\n              \"product\": \"prod_IQ4LQ4YFeqYB20\",\n              \"statement_descriptor\": \"FUTUREYOU-TURME28\",\n              \"tiers\": null,\n              \"tiers_mode\": null,\n              \"transform_usage\": null,\n              \"trial_period_days\": 24,\n              \"usage_type\": \"licensed\"\n            },\n            \"price\": {\n              \"id\": \"price_1HpECzCJZWINMVesSprmNjl5\",\n              \"object\": \"price\",\n              \"active\": true,\n              \"billing_scheme\": \"per_unit\",\n              \"created\": 1612534011,\n              \"currency\": \"gbp\",\n              \"livemode\": false,\n              \"lookup_key\": null,\n              \"metadata\": {\n                \"boxes\": \"1\",\n                \"boxSize\": \"28\",\n                \"dailyConsumption\": \"1\",\n                \"baseSku\": \"TURME\",\n                \"retired\": \"0\",\n                \"productVariant\": \"TURME28\"\n              },\n              \"nickname\": \"2021 Turmeric+ 28\",\n              \"product\": \"prod_IQ4LQ4YFeqYB20\",\n              \"recurring\": {\n                \"aggregate_usage\": null,\n                \"interval\": \"day\",\n                \"interval_count\": 28,\n                \"trial_period_days\": 24,\n                \"usage_type\": \"licensed\"\n              },\n              \"tiers_mode\": null,\n              \"transform_quantity\": null,\n              \"type\": \"recurring\",\n              \"unit_amount\": 1680,\n              \"unit_amount_decimal\": \"1680\"\n            },\n            \"quantity\": 1,\n            \"subscription\": \"sub_Jo9k3qUoCL2CdR\",\n            \"tax_rates\": [\n\n            ]\n          }\n        ],\n        \"has_more\": false,\n        \"total_count\": 1,\n        \"url\": \"/v1/subscription_items?subscription=sub_Jo9k3qUoCL2CdR\"\n      },\n      \"latest_invoice\": \"in_1JAXQbCJZWINMVes1Sjxxxh6\",\n      \"livemode\": false,\n      \"metadata\": {\n        \"shipping\": \"true\",\n        \"shippingUpgraded\": \"false\",\n        \"source\": \"magento\",\n        \"orderCount\": \"1\",\n        \"lastOrderId\": \"1033825\"\n      },\n      \"next_pending_invoice_item_invoice\": null,\n      \"pause_collection\": null,\n      \"pending_invoice_item_interval\": null,\n      \"pending_setup_intent\": null,\n      \"pending_update\": null,\n      \"plan\": {\n        \"id\": \"price_1HpECzCJZWINMVesSprmNjl5\",\n        \"object\": \"plan\",\n        \"active\": true,\n        \"aggregate_usage\": null,\n        \"amount\": 1680,\n        \"amount_decimal\": \"1680\",\n        \"billing_scheme\": \"per_unit\",\n        \"created\": 1612534011,\n        \"currency\": \"gbp\",\n        \"interval\": \"day\",\n        \"interval_count\": 28,\n        \"livemode\": false,\n        \"metadata\": {\n          \"boxes\": \"1\",\n          \"boxSize\": \"28\",\n          \"dailyConsumption\": \"1\",\n          \"baseSku\": \"TURME\",\n          \"retired\": \"0\",\n          \"productVariant\": \"TURME28\"\n        },\n        \"name\": \"2021 Turmeric+ 28\",\n        \"nickname\": \"2021 Turmeric+ 28\",\n        \"product\": \"prod_IQ4LQ4YFeqYB20\",\n        \"statement_descriptor\": \"FUTUREYOU-TURME28\",\n        \"tiers\": null,\n        \"tiers_mode\": null,\n        \"transform_usage\": null,\n        \"trial_period_days\": 24,\n        \"usage_type\": \"licensed\"\n      },\n      \"quantity\": 1,\n      \"schedule\": null,\n      \"start\": 1625667606,\n      \"start_date\": 1625651544,\n      \"status\": \"trialing\",\n      \"tax_percent\": null,\n      \"transfer_data\": null,\n      \"trial_end\": 1628734656,\n      \"trial_start\": 1625651544\n    },\n    \"previous_attributes\": {\n      \"billing_cycle_anchor\": 1627612809,\n      \"current_period_end\": 1627612809,\n      \"start\": 1625667560,\n      \"trial_end\": 1627612809\n    }\n  },\n  \"livemode\": false,\n  \"pending_webhooks\": 5,\n  \"request\": \"req_oVXbEfceo8VdC1\",\n  \"type\": \"customer.subscription.updated\"\n}","http_headers":{"Content-Type":"application/json; charset=utf-8","Cache-Control":"no-cache","User-Agent":"Stripe/1.0 (+https://stripe.com/docs/webhooks)","Accept":"*/*; q=0.5, application/xml","Stripe-Signature":"t=1625667607,v1=e17b07f1a1235846af14f801623cf4c0b0e0a464184a44298b8112437307a363,v0=e534420e1b43937c0aaeeca81153670e451b7dd26abe014980e578e89a25115d"},"endpoint":{"api_version":null},"type":"webhook_event"}" prefix=websocket.Client.readPump
time="Wed, 07 Jul 2021 14:20:12 UTC" level=debug msg="Processing webhook event" prefix=proxy.Proxy.processWebhookEvent webhook_converesation_id="1625667607-wc_iLI9nRX1npWE26" webhook_id="1625667607-wh_zy7r5i11RIzuOD"
time="Wed, 07 Jul 2021 14:20:12 UTC" level=debug msg="Received malformed event from Stripe, ignoring"
time="Wed, 07 Jul 2021 14:20:13 UTC" level=debug msg="Sending ping message" prefix=websocket.Client.writePump
time="Wed, 07 Jul 2021 14:20:13 UTC" level=debug msg="Received pong message" prefix=websocket.Client.readPump

```
### Environment
docker-compose setup on macOS


Pull request:
dynamically deserialize request log event payload `request` field
 ### Reviewers
r? @vcheung-stripe 
cc @stripe/developer-products

 ### Summary
Fixes #702. 

It turns out that Stripe event payloads belonging to older API versions present the `request` field as a string, instead of a map. This caused our unmarshalling step to soft fail as we ignored any 'invalid' payload shapes. The current tests are passing in the CLI for these changes.

+ [x] ~~I still need to manually verify this with the VS Code extension's RPC pipeline~~

edit: I have verified the RPC pipeline with local CLI build's debug logs, and manually verified in VS Code extension as well. Both new and old event payload shapes are coming through as expected with the correct fields and values 🎉 

Patch:
diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go
index 99fdac7..1becd1a 100644
--- a/pkg/proxy/proxy.go
+++ b/pkg/proxy/proxy.go
@@ -10,6 +10,7 @@ import (
 	"net/http"
 	"net/url"
 	"os"
+	"reflect"
 	"strconv"
 	"strings"
 	"time"
@@ -321,6 +322,15 @@ func (p *Proxy) processWebhookEvent(msg websocket.IncomingMessage) {
 		return
 	}
 
+	req, err := ExtractRequestData(evt.RequestData)
+
+	if err != nil {
+		p.cfg.Log.Debug("Received malformed event from Stripe, ignoring")
+		return
+	}
+
+	evt.Request = req
+
 	p.cfg.Log.WithFields(log.Fields{
 		"prefix":                  "proxy.Proxy.processWebhookEvent",
 		"webhook_id":              webhookEvent.WebhookID,
@@ -517,6 +527,44 @@ func Init(cfg *Config) (*Proxy, error) {
 	return p, nil
 }
 
+// ExtractRequestData takes an interface with request data from a Stripe event payload
+// and properly parses it into a StripeRequest struct before returning it
+func ExtractRequestData(data interface{}) (StripeRequest, error) {
+	var req StripeRequest
+
+	reqDataValue := reflect.ValueOf(data)
+	// here we check the type of the RequestData as it could be either a string or a map
+	// this depends on which API version is in use when listening to events
+	// versions including and prior to 2017-05-25 present the request field as a string
+	switch reqDataValue.Kind() {
+	case reflect.String:
+		req = StripeRequest{
+			ID:             data.(string),
+			IdempotencyKey: "",
+		}
+	case reflect.Map:
+		reqDataMap := reqDataValue.Interface().(map[string]interface{})
+
+		var id = ""
+		if rawID, ok := reqDataMap["id"]; ok && rawID != nil {
+			id = rawID.(string)
+		}
+
+		var idempotencyKey = ""
+		if rawKey, ok := reqDataMap["idempotency_key"]; ok && rawKey != nil {
+			idempotencyKey = rawKey.(string)
+		}
+
+		req = StripeRequest{
+			ID:             id,
+			IdempotencyKey: idempotencyKey,
+		}
+	default:
+		return StripeRequest{}, errors.New("Received malformed event from Stripe")
+	}
+	return req, nil
+}
+
 //
 // Private types
 //
diff --git a/pkg/proxy/stripeevent.go b/pkg/proxy/stripeevent.go
index 29cc992..ea2d87c 100644
--- a/pkg/proxy/stripeevent.go
+++ b/pkg/proxy/stripeevent.go
@@ -3,6 +3,8 @@ package proxy
 import "fmt"
 
 // StripeEvent is a representation of a Stripe `event` object
+// we define RequestData as an interface for backwards compatibility
+// Request will hold the deserialized request data
 type StripeEvent struct {
 	Account         string                 `json:"account"`
 	APIVersion      string                 `json:"api_version"`
@@ -10,15 +12,16 @@ type StripeEvent struct {
 	Data            map[string]interface{} `json:"data"`
 	ID              string                 `json:"id"`
 	Livemode        bool                   `json:"livemode"`
-	Request         StripeRequestData      `json:"request"`
 	PendingWebhooks int                    `json:"pending_webhooks"`
 	Type            string                 `json:"type"`
+	RequestData     interface{}            `json:"request"`
+	Request         StripeRequest
 }
 
-// StripeRequestData is a representation of the Request field in a Stripe `event` object
-type StripeRequestData struct {
-	ID             string `json:"id"`
-	IdempotencyKey string `json:"idempotency_key"`
+// StripeRequest is a representation of the Request field in a Stripe `event` object
+type StripeRequest struct {
+	ID             string
+	IdempotencyKey string
 }
 
 // IsConnect return true or false if *StripeEvent is connect or not.
diff --git a/pkg/rpcservice/events_resend.go b/pkg/rpcservice/events_resend.go
index 79cf4a9..7e9245b 100644
--- a/pkg/rpcservice/events_resend.go
+++ b/pkg/rpcservice/events_resend.go
@@ -62,9 +62,15 @@ func (srv *RPCService) EventsResend(ctx context.Context, req *rpc.EventsResendRe
 		return nil, err
 	}
 
+	reqData, err := proxy.ExtractRequestData(evt.RequestData)
+
+	if err != nil {
+		return nil, err
+	}
+
 	request := rpc.StripeEvent_Request{
-		Id:             evt.Request.ID,
-		IdempotencyKey: evt.Request.IdempotencyKey,
+		Id:             reqData.ID,
+		IdempotencyKey: reqData.IdempotencyKey,
 	}
 
 	return &rpc.EventsResendResponse{
diff --git a/pkg/rpcservice/listen.go b/pkg/rpcservice/listen.go
index dad44e0..2e9be20 100644
--- a/pkg/rpcservice/listen.go
+++ b/pkg/rpcservice/listen.go
@@ -183,10 +183,18 @@ func buildStripeEventResp(raw *proxy.StripeEvent) (*rpc.ListenResponse, error) {
 	if err != nil {
 		return nil, err
 	}
+
+	reqData, err := proxy.ExtractRequestData(raw.RequestData)
+
+	if err != nil {
+		return nil, err
+	}
+
 	request := rpc.StripeEvent_Request{
-		Id:             raw.Request.ID,
-		IdempotencyKey: raw.Request.IdempotencyKey,
+		Id:             reqData.ID,
+		IdempotencyKey: reqData.IdempotencyKey,
 	}
+
 	return &rpc.ListenResponse{
 		Content: &rpc.ListenResponse_StripeEvent{
 			StripeEvent: &rpc.StripeEvent{


Test patch:
diff --git a/pkg/rpcservice/listen_test.go b/pkg/rpcservice/listen_test.go
index 2054d89..ecf89f6 100644
--- a/pkg/rpcservice/listen_test.go
+++ b/pkg/rpcservice/listen_test.go
@@ -110,9 +110,9 @@ func TestListenStreamsEvents(t *testing.T) {
 							"id": "cs_test_12345",
 						},
 					},
-					Request: proxy.StripeRequestData{
-						ID:             "req_12345",
-						IdempotencyKey: "foo",
+					RequestData: map[string]interface{}{
+						"id":              "req_12345",
+						"idempotency_key": "foo",
 					},
 				},
 			}
@@ -430,9 +430,9 @@ func TestBuildStripeEventResponseSucceeds(t *testing.T) {
 				"id": "cs_test_12345",
 			},
 		},
-		Request: proxy.StripeRequestData{
-			ID:             "req_12345",
-			IdempotencyKey: "foo",
+		RequestData: map[string]interface{}{
+			"id":              "req_12345",
+			"idempotency_key": "foo",
 		},
 	}
 
@@ -469,3 +469,54 @@ func TestBuildStripeEventResponseSucceeds(t *testing.T) {
 	assert.Nil(t, err)
 	assert.Equal(t, expected, actual)
 }
+
+func TestBuildLegacyStripeEventResponseSucceeds(t *testing.T) {
+	raw := &proxy.StripeEvent{
+		Account:         "acct_12345",
+		APIVersion:      "2017-04-06",
+		Created:         12345,
+		ID:              "evt_12345",
+		Livemode:        false,
+		PendingWebhooks: 2,
+		Type:            "checkout.session.completed",
+		Data: map[string]interface{}{
+			"object": map[string]interface{}{
+				"id": "cs_test_12345",
+			},
+		},
+		RequestData: "req_12345",
+	}
+
+	expectedData, err := structpb.NewStruct(map[string]interface{}{
+		"object": map[string]interface{}{
+			"id": "cs_test_12345",
+		},
+	})
+	if err != nil {
+		t.Fatalf("Failed to create expected event data")
+	}
+
+	expected := &rpc.ListenResponse{
+		Content: &rpc.ListenResponse_StripeEvent{
+			StripeEvent: &rpc.StripeEvent{
+				Id:              "evt_12345",
+				Account:         "acct_12345",
+				ApiVersion:      "2017-04-06",
+				Data:            expectedData,
+				Type:            "checkout.session.completed",
+				Created:         12345,
+				Livemode:        false,
+				PendingWebhooks: 2,
+				Request: &rpc.StripeEvent_Request{
+					Id:             "req_12345",
+					IdempotencyKey: "",
+				},
+			},
+		},
+	}
+
+	actual, err := buildStripeEventResp(raw)
+
+	assert.Nil(t, err)
+	assert.Equal(t, expected, actual)
+}