Service Order Servicer Webhook

Webhook request

Authentication and Processing

Webhooks sent from Extend require digital signature verification. Once verified, the webhooks can be processed for business logic.

  • The webhook payload is JSON.
  • The signature is sent as a signature header.
  • Extend's public key id is sent as the X-Extend-Key-Id header.
  • Extend's key id is also sent in the webhook request payload as kid. Confirm that the value of X-Extend-Key-Id and kid match.

As part of the verification flow, you will need to make an HTTP GET request to Extend's JWKS endpoint: https://api.helloextend.com/service-orders/oauth/keys

{{API_HOST}}/service-orders/oauth/keys
{
  "keys": [
    {
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "key_ops": [
        "sign",
        "verify"
      ],
      "kid": "b35edb9f-4be9-4cb4-9425-6f3d04ac9347",
      "n": "tvFPcO-lSUcW9n_dv45wfY0yF47CXZLICqS5kWIypfpP2NX4WRDK8c0bXx6g9P4n_UjJqyDsa-ocMil2dWRDn2oGCzrZLvk6bwerQfCij0Qvy92YZAWJ8r2QrQN69LZzrzCqpMbleclb4du5L46tIdWpOuWx9JWOD_F_HROyd26-SIUvrsLnk7pjSsRx1Bp9mw-qjIB2tEFCINrD3IDcFicPKMpWImhNA-Bukq3ZySzq2VvqAqmhif5EsLw4XWXOucM7YWLn5MfscRippwky94emVD4Hc3LaIYdjxcSMd9Gyr4vgEQU-sgj_hrBfjCabLEsfELIbrC4aPaNN1VTar91ErycEtmH5fNAhI7ka1YUFt5-AgvXDcPs4vnHuTX86TG_0IMhSxr-jvdarYJbTdbLU2-ViRSjIc3FSg4qjvxmyfJNXkwIWHy5Mh3kEK3GA573GofyCTjo20IwjNwhys0PdMnC9YXJQATPE4BvADaaA4UiKDeMfpOXA4Y6dgA9HDRVoA0RgNAycfHh0eIyjnOZjaU7napAxQjr7xqTw_8d8_MpCdAaAb70iL5pbbAtXpuhB27fvKNgvQGD3-X6idEbMYuJmZHzqDMfEmJROd4SjQl0OR5Lay5KpZyvjVsskh0GEbU_HR87UYP8FJn-avDhG6jU-HtSDJahACXeaS-s",
      "e": "AQAB"
    }
  ]
}
  • Parse the JSON response which contains an array of keys in JWK format. Find the specific key that matches the kid from the webhook payload.
  • Convert the JWK (JSON Web Key) to PEM format, which is required for most signature verification libraries. This typically involves using a crypto library to create a public key object from the JWK and exporting it in PEM format. Here is an example using the crypto Node module.
/**
 * Convert a JWK to PEM format
 *
 * @param jwk - The JWK object (the full key Object from the 'keys' array)
 * @returns PEM formatted key
 */
function jwkToPem(jwk: any): string {
  try {
    const publicKey = crypto.createPublicKey({
      key: jwk as unknown as NodeJsonWebKey,
      format: 'jwk',
    })
    return publicKey.export({ type: 'spki', format: 'pem' }).toString()
  } catch (error) {
    Log.error('Error converting JWK to PEM', error)
    throw error
  }
}

Verify the Digital Signature. Use a cryptographic library to verify that the signature was created using the private key corresponding to the public key. The verification should use the RSA-SHA256 algorithm. The signature is typically base64-encoded and needs to be decoded before verification.

import crypto from 'crypto'
/**
 * Returns true if signature verification passed
 *
 * @param body - Stringified body of the webhook
 * @param publicKey - Base64 encoded publicKey.
 * @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
 */
/**
 * Verify webhook signature using JWKS approach
 *
 * @param body - Stringified body of the webhook
 * @param signature - Base64 encoded signature from 'signature' header
 * @param keyId - Key ID from 'X-Extend-Key-Id' header (should match kid in payload)
 * @returns true if the verification passed
 */
async function verifyWebhookSignature(
  body: string,
  signature: string,
  keyId: string,
): Promise<boolean> {
  try {
    // 1. Fetch JWKS from Extend
    const jwksResponse = await fetch(
      'https://api.helloextend.com/service-orders/oauth/keys',
    )
    const jwks = await jwksResponse.json()

    // 2. Find the key that matches the keyId
    const key = jwks.keys.find((k: any) => k.kid === keyId)
    if (!key) {
      console.error('Key not found for kid:', keyId)
      return false
    }

    // 3. Convert JWK to PEM
    const publicKey = crypto.createPublicKey({
      key: key,
      format: 'jwk',
    })
    const pemKey = publicKey.export({ type: 'spki', format: 'pem' })

    // 4. Verify signature
    return crypto.verify(
      'RSA-SHA256',
      Buffer.from(body),
      pemKey,
      Buffer.from(signature, 'base64'),
    )
  } catch (e) {
    console.error('Signature verification failed:', e)
    return false
  }
}

// Example usage - JWKS approach (recommended)
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","kid":"b35edb9f-4be9-4cb4-9425-6f3d04ac9347"}'
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='
const keyId = 'b35edb9f-4be9-4cb4-9425-6f3d04ac9347' // From X-Extend-Key-Id header

// Verify using modern approach
const isValid = await verifyWebhookSignature(requestBody, signature, keyId)
if (isValid) {
  console.log('Signature is valid!')
} else {
  console.log('Signature is not valid.')
}

If the signature verification succeeds; process the webhook. If verification fails; reject the webhook.

Complete Example

Here's a full example of handling webhooks built with AWS Lambda using the JWKS approach:

import crypto from 'crypto'
import fetch from 'node-fetch'

const lambda = async event => {
  const { headers, body } = event

  // Extract signature and key ID from headers
  const signature = headers['signature']
  const keyId = headers['X-Extend-Key-Id']

  console.log('webhook received')
  if (!signature || !keyId) {
    console.log('Webhook missing required headers. Unauthorized')
    return { statusCode: 401 }
  }

  console.log('attempting to verify RSA-SHA256 signature using JWKS')

  const verification = await verifyWebhookSignature(body, signature, keyId)
  if (!verification) {
    console.log('Unable to verify signature. Unauthorized')
    return { statusCode: 401 }
  }

  console.log('signature verification passed')

  // Parse the webhook payload
  const webhookData = JSON.parse(body)

  // Handle different webhook types
  if (webhookData.type === 'assign') {
    console.log(
      'Processing assigned webhook for service order:',
      webhookData.serviceOrderId,
    )
    // Process assigned webhook logic here
  } else if (webhookData.type === 'return_approved') {
    console.log(
      'Processing return approved webhook for service order:',
      webhookData.serviceOrderId,
    )
    // Process return approved webhook logic here
  }

  return { statusCode: 200 }
}

/**
 * Verify webhook signature using JWKS approach
 *
 * @param body - Stringified body of the webhook
 * @param signature - Base64 encoded signature from 'signature' header
 * @param keyId - Key ID from 'X-Extend-Key-Id' header (should match kid in payload)
 * @returns true if the verification passed
 */
async function verifyWebhookSignature(
  body: string,
  signature: string,
  keyId: string,
): Promise<boolean> {
  try {
    // 1. Fetch JWKS from Extend
    const jwksResponse = await fetch(
      'https://api.helloextend.com/service-orders/oauth/keys',
    )
    const jwks = await jwksResponse.json()

    // 2. Find the key that matches the keyId
    const key = jwks.keys.find((k: any) => k.kid === keyId)
    if (!key) {
      console.error('Key not found for kid:', keyId)
      return false
    }

    // 3. Convert JWK to PEM
    const publicKey = crypto.createPublicKey({
      key: key,
      format: 'jwk',
    })
    const pemKey = publicKey.export({ type: 'spki', format: 'pem' })

    // 4. Verify signature
    return crypto.verify(
      'RSA-SHA256',
      Buffer.from(body),
      pemKey,
      Buffer.from(signature, 'base64'),
    )
  } catch (e) {
    console.error('Signature verification failed:', e)
    return false
  }
}

Webhook Request Body

Webhook Types and Usage

Extend sends two types of webhooks to servicers:

  1. Assigned Webhook (type: "assign"): Sent when a service order is assigned to a servicer for repair/replacement
  2. Return Approved Webhook (type: "return_approved"): Sent when a return/refund has been approved and the customer needs to ship the item back

Both webhook types include the kid field for signature verification and follow the same authentication process.

Assigned Webhook

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
  kid: string // Key ID for signature verification
}

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",
  "kid": "b35edb9f-4be9-4cb4-9425-6f3d04ac9347"
}

Return Approved Webhook

export interface ReturnApprovedWebhook {
  claimDetailsUrl: string
  orderId: string
  orderPurchaseDate?: number
  productDeliveryDate?: number
  claimId: string
  serviceOrderId: string
  returnShipment: {
    shipmentId: string
    carrier: ShipmentCarrier
    broker: ShipmentBroker
    destination: Address
    origin: Address
    status: ShipmentStatus
    trackingNumber?: string
    shippedAt?: number | null
    estimatedDeliveryAt?: number | null
    deliveredAt?: number | null
  }
  refundAmount: number
  refundOption: ReturnFulfillmentMethod
  customerName: string
  customerEmail: string
  customerShippingAddress: Address
  customerPhoneNumber?: string
  productDetails: ProductDetails[]
  failureType: string
  failureDescription?: string
  incident: any
  sendDate: number
  url: string
  transactionId: string
  kid: string // Key ID for signature verification
}

export interface ProductDetails {
  productReferenceId: string
  sku?: string
  productListPrice?: number
  productPurchasePrice?: number
  productVertical?: string
}

export interface Address {
  address1: string
  address2?: string
  city: string
  countryCode: string
  postalCode: string
  provinceCode?: string
}

Example

{
  "claimDetailsUrl": "merchants.extend.com/store/claims/e18c4150-2bcb-4d80-94af-816f9c59e0e1",
  "orderId": "51d1699c-c8f4-4e50-893b-16af8adc994a",
  "claimId": "e18c4150-2bcb-4d80-94af-816f9c59e0e1",
  "serviceOrderId": "fc47a847-27c8-48c3-a564-68300f5f625b",
  "orderPurchaseDate": 1593533340000,
  "productDeliveryDate": 1593533340000,
  "returnShipment": {
    "shipmentId": "6c7278d7-37a5-4253-ba8d-3d266bc68b2a",
    "carrier": "ups_ground",
    "broker": "ship_engine",
    "destination": {
      "address2": "Ste 1",
      "city": "San Francisco",
      "address1": "1 Main St",
      "provinceCode": "CA",
      "countryCode": "US",
      "postalCode": "11111"
    },
    "origin": {
      "address2": "Ste 1",
      "city": "San Francisco",
      "address1": "1 Main St",
      "provinceCode": "CA",
      "countryCode": "US",
      "postalCode": "11111"
    },
    "status": "received_by_carrier",
    "trackingNumber": "1220488b-0793-47ea-9c9e-ae35cdf66a70",
    "shippedAt": 1593533340000,
    "estimatedDeliveryAt": 1593993600000
  },
  "refundAmount": 12999,
  "refundOption": "store_credit",
  "customerName": "Customer Name",
  "customerEmail": "[email protected]",
  "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": 1740766088101,
  "url": "https://servicer-webhook-url/post-webhook",
  "transactionId": "b16a3b34-636c-4941-b69b-8eaf57b62c9f",
  "type": "return_approved",
  "kid": "b35edb9f-4be9-4cb4-9425-6f3d04ac9347"
}

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.

Legacy Method For Signature Verification

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.')
}

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
}