Webhook request on service order assignment
A service order is automatically created in Extend's system when a claim is approved. A repair service order is then assigned to a servicer. This will trigger a webhook request to the endpoint the servicer has configured. This request will contain relevant service order data, such as customer and product information, and most importantly, the id of the service order. These webhook requests will be signed using asymmetric encryption (RSA-SHA256). The public key from the endpoint below should be used to verify the request. The expected request body is also included below.
Public Key endpoint
/ GET /service-orders/public-key
interface ResponseBody {
key: string
}
The public key will be base64 encoded.
Signature
The webhook request will come with a signature header. The webhook signature will be base64 encoded. The signature will be what is used to verify that the message has not been tampered with or altered. Any message where the signature cannot be verified should be discarded. Use a SHA256 signature verification library to verify the message.
An example of how to verify a digital signature.
import crypto from 'crypto'
/**
* Returns true if signature verification passed
*
* @param body - Stringified body of the webhook
* @param publicKey - Base64 encoded publicKey. Public key can be fetched at GET /service-orders/public-key
* The public key endpoint returns the key Base64 encoded.
* @param signature - Base64 encoded signature. Found in webhook ['signature'] header.
* The signature found in the request header is Base64 encoded.
* @returns true if the verification passed
*/
function verifyDigitalSignature(
body: string,
publicKey: string,
signature: string,
): boolean {
try {
return crypto.verify(
'RSA-SHA256',
Buffer.from(body),
Buffer.from(publicKey, 'base64'),
Buffer.from(signature, 'base64'),
)
} catch (e) {
return false
}
}
// Example usage
const publicKey = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUF0dkZQY08rbFNVY1c5bi9kdjQ1dwpmWTB5RjQ3Q1haTElDcVM1a1dJeXBmcFAyTlg0V1JESzhjMGJYeDZnOVA0bi9VakpxeURzYStvY01pbDJkV1JECm4yb0dDenJaTHZrNmJ3ZXJRZkNpajBRdnk5MllaQVdKOHIyUXJRTjY5TFp6cnpDcXBNYmxlY2xiNGR1NUw0NnQKSWRXcE91V3g5SldPRC9GL0hST3lkMjYrU0lVdnJzTG5rN3BqU3NSeDFCcDltdytxaklCMnRFRkNJTnJEM0lEYwpGaWNQS01wV0ltaE5BK0J1a3EzWnlTenEyVnZxQXFtaGlmNUVzTHc0WFdYT3VjTTdZV0xuNU1mc2NSaXBwd2t5Cjk0ZW1WRDRIYzNMYUlZZGp4Y1NNZDlHeXI0dmdFUVUrc2dqL2hyQmZqQ2FiTEVzZkVMSWJyQzRhUGFOTjFWVGEKcjkxRXJ5Y0V0bUg1Zk5BaEk3a2ExWVVGdDUrQWd2WERjUHM0dm5IdVRYODZURy8wSU1oU3hyK2p2ZGFyWUpiVApkYkxVMitWaVJTakljM0ZTZzRxanZ4bXlmSk5Ya3dJV0h5NU1oM2tFSzNHQTU3M0dvZnlDVGpvMjBJd2pOd2h5CnMwUGRNbkM5WVhKUUFUUEU0QnZBRGFhQTRVaUtEZU1mcE9YQTRZNmRnQTlIRFJWb0EwUmdOQXljZkhoMGVJeWoKbk9aamFVN25hcEF4UWpyN3hxVHcvOGQ4L01wQ2RBYUFiNzBpTDVwYmJBdFhwdWhCMjdmdktOZ3ZRR0QzK1g2aQpkRWJNWXVKbVpIenFETWZFbUpST2Q0U2pRbDBPUjVMYXk1S3BaeXZqVnNza2gwR0ViVS9IUjg3VVlQOEZKbithCnZEaEc2alUrSHRTREphaEFDWGVhUytzQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==';
const requestBody = '{"serviceOrderId":"73752710-8cdf-4fe3-8327-6936fa3badd6","claimId":"7b67180d-0ae6-4d81-a0ca-d7eeda41870e","contractId":"770e5f9d-fb2d-4c53-9137-cbb647d037f5","servicerId":"26db19a3-d9db-4ad4-9575-b7ca2f6e990a","customerName":"Paul Name","customerEmail":"[email protected]","contractPurchaseDate":1655668093818,"customerShippingAddress":{"city":"San Francisco","address1":"533 Mission Street","provinceCode":"CA","countryCode":"USA","postalCode":"94105"},"customerPhoneNumber":"555-535-5555","productDetails":[{"productReferenceId":"PRODUCT-REPAIR-385UmuLNaUqVmpohrmnvMqrncttQCz","productListPrice":25199,"productPurchasePrice":25199}],"failureType":"webhook parse","failureDescription":"webhook","incident":{"failureType":"electricalFailure","description":"BROKE PLZ HALP","occurredAt":1695152893819,"FailureType":"webhook parse","FailureDescription":"webhook","productCondition":"nonfunctional"},"sendDate":1695152952396,"url":"https://webhook.site/70a09585-f934-4b01-855b-0869e5e19752","transactionId":"63815176-f685-44ca-8180-fb13e39d5d04","type":"assign"}';
const signature = 'JJbRyjmmylP4GJtNCT7wANYVMkmUYENuX13sXqMjbAUcfidBBix/hzCjnoycg6lCT1Ktny2OnmixEHQAwNFOuReTlid+AFvCQnQ0nA+aUjvD3JSYCLLE2SOuvbZ7/Q/fQZNnFrDUX6MXcYkuoNLUmKRagG5uq0UyIEtWgfyLtGJQlZ91KXJkvZsFQu+AO5dFiDceJA+IW11sCN5e/9gKWuHxzxwKepvA8S0lyPwQWYqk1V/O/bV5s5uD5zrI6Wndd8Yq7ejYsWd6ZeHbjrcTbbr35lKsTcpoLxtBjD+H/1X5+pYAPlW7ymLWpPxEAug8tnpzy5HVMtwNZ3br8S5afC9BgR7abGcfa3PjyYvM1tk6+JfCPLa9gIJShu1+eXTq0GUQnxu/0bL/ihnwadHlFZyLVzmRjBkPrsuQsM6KTvXS5Gw+BGo+zy/p7iFUFHws1r6zZNONe0ZxTudpDgGN7IoXmvyj7OOJ7VyGROggB0Ptu2iiNz81uTT1M9z+QCR2pa9E+a6fTWTZIXP86acb8HuKDb8Rd9wkU4G609aZc3QHX0ANKIFSWqhz9WDh0XZgT8daMuda+TmtI4NkTS7j9yQLqvbYQ1W2RpCKVBLgXCgiylBoHfRWFZ/dlOdkojBWaFnWa8ytU69IHFgVAd4V4RVPgjNyWgwovtQtiAvpT3o=';
if (verifyDigitalSignature($signature, $publicKey, $requestBody)) {
console.log("Signature is valid!")
} else {
console.log("Signature is not valid.")
}
<?php
// Signature is base64 encoded and found in the 'signature' header on the request
// Public Key returned from Extend API as base64 encoded string
// Request body. Stringified JSON body sent in the webhook request
function verifyDigitalSignature($signature, $publicKey, $requestBody)
{
// Load the public key
$publicKeyResource = openssl_pkey_get_public(base64_decode($publicKey));
if ($publicKeyResource === false) {
return false; // Failed to load the public key
}
// Verify the signature
$result = openssl_verify($requestBody, base64_decode($signature), $publicKeyResource, OPENSSL_ALGO_SHA256);
return ($result === 1); // 1 means the signature is valid
}
// Example usage
$publicKey = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUF0dkZQY08rbFNVY1c5bi9kdjQ1dwpmWTB5RjQ3Q1haTElDcVM1a1dJeXBmcFAyTlg0V1JESzhjMGJYeDZnOVA0bi9VakpxeURzYStvY01pbDJkV1JECm4yb0dDenJaTHZrNmJ3ZXJRZkNpajBRdnk5MllaQVdKOHIyUXJRTjY5TFp6cnpDcXBNYmxlY2xiNGR1NUw0NnQKSWRXcE91V3g5SldPRC9GL0hST3lkMjYrU0lVdnJzTG5rN3BqU3NSeDFCcDltdytxaklCMnRFRkNJTnJEM0lEYwpGaWNQS01wV0ltaE5BK0J1a3EzWnlTenEyVnZxQXFtaGlmNUVzTHc0WFdYT3VjTTdZV0xuNU1mc2NSaXBwd2t5Cjk0ZW1WRDRIYzNMYUlZZGp4Y1NNZDlHeXI0dmdFUVUrc2dqL2hyQmZqQ2FiTEVzZkVMSWJyQzRhUGFOTjFWVGEKcjkxRXJ5Y0V0bUg1Zk5BaEk3a2ExWVVGdDUrQWd2WERjUHM0dm5IdVRYODZURy8wSU1oU3hyK2p2ZGFyWUpiVApkYkxVMitWaVJTakljM0ZTZzRxanZ4bXlmSk5Ya3dJV0h5NU1oM2tFSzNHQTU3M0dvZnlDVGpvMjBJd2pOd2h5CnMwUGRNbkM5WVhKUUFUUEU0QnZBRGFhQTRVaUtEZU1mcE9YQTRZNmRnQTlIRFJWb0EwUmdOQXljZkhoMGVJeWoKbk9aamFVN25hcEF4UWpyN3hxVHcvOGQ4L01wQ2RBYUFiNzBpTDVwYmJBdFhwdWhCMjdmdktOZ3ZRR0QzK1g2aQpkRWJNWXVKbVpIenFETWZFbUpST2Q0U2pRbDBPUjVMYXk1S3BaeXZqVnNza2gwR0ViVS9IUjg3VVlQOEZKbithCnZEaEc2alUrSHRTREphaEFDWGVhUytzQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==';
$requestBody = '{"serviceOrderId":"73752710-8cdf-4fe3-8327-6936fa3badd6","claimId":"7b67180d-0ae6-4d81-a0ca-d7eeda41870e","contractId":"770e5f9d-fb2d-4c53-9137-cbb647d037f5","servicerId":"26db19a3-d9db-4ad4-9575-b7ca2f6e990a","customerName":"Paul Name","customerEmail":"[email protected]","contractPurchaseDate":1655668093818,"customerShippingAddress":{"city":"San Francisco","address1":"533 Mission Street","provinceCode":"CA","countryCode":"USA","postalCode":"94105"},"customerPhoneNumber":"555-535-5555","productDetails":[{"productReferenceId":"PRODUCT-REPAIR-385UmuLNaUqVmpohrmnvMqrncttQCz","productListPrice":25199,"productPurchasePrice":25199}],"failureType":"webhook parse","failureDescription":"webhook","incident":{"failureType":"electricalFailure","description":"BROKE PLZ HALP","occurredAt":1695152893819,"FailureType":"webhook parse","FailureDescription":"webhook","productCondition":"nonfunctional"},"sendDate":1695152952396,"url":"https://webhook.site/70a09585-f934-4b01-855b-0869e5e19752","transactionId":"63815176-f685-44ca-8180-fb13e39d5d04","type":"assign"}';
$signature = 'JJbRyjmmylP4GJtNCT7wANYVMkmUYENuX13sXqMjbAUcfidBBix/hzCjnoycg6lCT1Ktny2OnmixEHQAwNFOuReTlid+AFvCQnQ0nA+aUjvD3JSYCLLE2SOuvbZ7/Q/fQZNnFrDUX6MXcYkuoNLUmKRagG5uq0UyIEtWgfyLtGJQlZ91KXJkvZsFQu+AO5dFiDceJA+IW11sCN5e/9gKWuHxzxwKepvA8S0lyPwQWYqk1V/O/bV5s5uD5zrI6Wndd8Yq7ejYsWd6ZeHbjrcTbbr35lKsTcpoLxtBjD+H/1X5+pYAPlW7ymLWpPxEAug8tnpzy5HVMtwNZ3br8S5afC9BgR7abGcfa3PjyYvM1tk6+JfCPLa9gIJShu1+eXTq0GUQnxu/0bL/ihnwadHlFZyLVzmRjBkPrsuQsM6KTvXS5Gw+BGo+zy/p7iFUFHws1r6zZNONe0ZxTudpDgGN7IoXmvyj7OOJ7VyGROggB0Ptu2iiNz81uTT1M9z+QCR2pa9E+a6fTWTZIXP86acb8HuKDb8Rd9wkU4G609aZc3QHX0ANKIFSWqhz9WDh0XZgT8daMuda+TmtI4NkTS7j9yQLqvbYQ1W2RpCKVBLgXCgiylBoHfRWFZ/dlOdkojBWaFnWa8ytU69IHFgVAd4V4RVPgjNyWgwovtQtiAvpT3o=';
if (verifyDigitalSignature($signature, $publicKey, $requestBody)) {
echo "Signature is valid!";
} else {
echo "Signature is not valid.";
}
?>
Full featured example of handling webhook built with AWS lambda.
import crypto from 'crypto'
import fetch from 'node-fetch'
const lambda = async event => {
const { headers, body } = event
// Signature is a base64 encoded string in header 'siganture'
const signature = headers['signature']
console.log('webhook received')
if (!signature) {
console.log('Webhook does not contain signature. Unauthorized')
return { statusCode: 401 }
}
// Public Key returned as base64 encoded string
const publicKey = await getPublicKey()
console.log('attempting to verify RSA-SHA256 signature')
const verification = verifyDigitalSignature(body, publicKey, signature)
if (!verification) {
console.log('Unable to verify signature. Unauthorized')
return { statusCode: 401 }
}
console.log('signature verification passed')
return { statusCode: 200 }
}
/**
* Returns true if signature verification passed
*
* @remarks
* The crypto package is a built-in Node module
*
* @param body - Stringified body of the webhook
* @param publicKey - Base64 encoded publicKey. Public key can be fetched at GET /service-orders/public-key
* @param signature - Base64 encoded signature. Found in webhook ['signature'] header.
* @returns true if the verification passed
*/
function verifyDigitalSignature(
body: string,
publicKey: string,
signature: string,
): boolean {
try {
return crypto.verify(
'RSA-SHA256',
Buffer.from(body),
Buffer.from(publicKey, 'base64'),
Buffer.from(signature, 'base64'),
)
} catch (e) {
console.log(e)
return false
}
}
async function getPublicKey(): Promise<string> {
console.log('Getting Extend Public Key')
const API_HOST = 'https://api.helloextend.com/'
const url = `${API_HOST}/service-orders/public-key`
const publicKeyResponse = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const key = (await publicKeyResponse.json()).key
if (!key) {
throw new Error('Unable to parse public key')
}
return key
}
Webhook Request Body
interface AssignedWebhook {
contractId: string
contractPurchaseDate: number
productDeliveryDate?: number
claimId: string
serviceOrderId: string
serviceAmountAuthorized?: number
orderNumber?: string
customerName: string
customerEmail: string
customerShippingAddress: Address
customerPhoneNumber?: string
productDetails: ProductDetails[]
failureType: string
failureDescription?: string
incident: {
FailureType: string
// Can include additional properties
}
sendDate: number
servicerId: string
url: string
transactionId: string
type: string
}
interface ProductDetails {
productReferenceId: string
sku?: string
productListPrice?: number // In cents
productPurchasePrice?: number // In cents
productVertical?: string
}
interface Address {
address1: string
address2?: string
city: string
countryCode: string
postalCode: string
provinceCode?: string
}
Example
{
"serviceOrderId": "4319c6c7-48aa-4086-a114-b63d9161aa18",
"claimId": "09c6dfc3-3e29-4b3d-a83a-19d9b2c5971a",
"contractId": "15e0abb5-6b96-4f68-827d-ff962027221f",
"servicerId": "3855a17d-3b61-44c9-b45c-43469e9f1d0a",
"customerName": "Customer Name",
"customerEmail": "[email protected]",
"contractPurchaseDate": 1654117627000,
"customerShippingAddress": {
"address2": "Ste 1",
"city": "San Francisco",
"address1": "1 Main St",
"provinceCode": "CA",
"countryCode": "US",
"postalCode": "11111"
},
"customerPhoneNumber": "111-111-1111",
"productDetails": [
{
"productReferenceId": "referenceId",
"productListPrice": 25199,
"productPurchasePrice": 25199
}
],
"failureType": "electricalFailure",
"failureDescription": "The description of the failure.",
"incident" : {
"FailureType": "electricalFailure",
},
"sendDate": 1690910629602,
"url": "https://servicer-webhook-url/post-webhook",
"transactionId": "b16a3b34-636c-4941-b69b-8eaf57b62c9f",
"type": "assign"
}
Webhooks should be responded to with a status code 200. All requests that do not receive a 200 will be retried every hour for 48 hours.