Error 400 sent in response to webhook event net.authorize.customer.subscription.cancelled
Summary
When Authorize.net sends a webhook on event net.authorize.customer.subscription.cancelled
, this extension responds with Error 400
. This causes some problems:
- Authorize.net will eventually disable the webhook due to errors.
- The recurring contribution associated with this ARB subscription will not be canceled in civicrm.
From looking at the extension code, it seems reasonable to believe this happens in every case for this webhook event.
Here's an example of such webhook payload:
{"notificationId":"cf6a892f-24ef-40c9-826d-d822160c8fe5","eventType":"net.authorize.customer.subscription.cancelled","eventDate":"2021-10-14T21:44:17.9492105Z","webhookId":"[REDACTED_WEBHOOK_ID]","payload":{"name":"Contribution: General Fund","amount":50.00,"status":"canceled","profile":{"customerProfileId":[REDACTED_CUSTOMER_PROFILE_ID],"customerPaymentProfileId":[REDACTED_CUSTOMER_PAYMENT_PROFILE_ID]},"entityName":"subscription","id":"[REDACTED_ID]"}}
Code execution process:
Here's what seems to be happening (values taken from above example payload, code references to version 2.2.4, currently in use on the site in question and latest available at time of writing):
- Authorize.net sends the above webhook payload to the listener for this site's authorize.net payment processor.
- In
CRM_Core_Payment_AuthNetIPN::main()
, right near the beginning (line 79), the extension calls authorize.net api methodgetTransactionDetailsRequest(['transId' => X)
, where X is the value of theid
parameter provided in the webhook payload. - Authorize.net api responds with an error, 'The record cannot be found.'
- Seeing this error, the extension invokes
CRM_Core_Payment_MJWIPNTrait::exception()
, which sends anError 400
response to authorize.net - Further down in
main()
(line 226 and following), the extension would mark the related recurring contribution as canceled, but code execution never reaches this point due to the exception.
Diagnosis of problem:
You might see that the problem lies in Step 2: This webhook is a cancellation notification for an ARB subscription, but the extension is handling it (in this part of the code) as if it were a notification about a transaction:
- The extension is calling a.net method getTransactionDetailsRequest, which takes a field
transId
expected to be the ID of a transaction, and which provides details on that transaction. - But the entity at hand is a subscription, not a transaction, and the passed value for the 'transId' field is not a transaction ID but a subscription ID, as provided by the webhook payload. I.e., it's using the wrong api method and sending an invalid transaction id to that method.
Remediation:
Probably main()
should differentiate on $this->eventType
to handle subscription-related webhook events separately from transaction-related ones.
I'm not sure if it's necessary for CRM_Core_Payment_AuthNetIPN::main()
to retrieve information about a canceled subscription, but I suspect it is. If so, the appropriate authorize.net api method is probably getSubscription, which accepts field subscriptionId
, which should be the value of the id
parameter provided in the webhook payload.