Verifying Webhooks

Koalafi signs every webhook request so you can confirm it genuinely came from us. Verification follows the HTTP Message Signatures standard and uses Ed25519 asymmetric cryptography.


1. Retrieve the Signing Public Key

Query your dealer's webhook configuration to get the current signing key:

query dealer {
  dealer {
    webhookConfig {
      signingKey {
        algorithm
        keyId
        publicKey
      }
    }
  }
}

The publicKey field is base64-encoded and prefixed with whpk_. Before using it you must strip the prefix and base64-decode the remainder.

The algorithm field indicates the key type — currently always ED25519.


2. Assemble the Signature Base

The Signature-Input header describes which parts of the request were signed and in what order. Example:

sig1=("content-digest" "@method" "@target-uri" "content-type" "message-id");keyid="koalafi-prod";created=1779394418;expires=1779394718
FieldDescription
sig1Label for this signature. Multiple signatures may appear as a comma-separated list.
(...)Ordered list of components that make up the signature base.
keyidIdentifier of the key used to sign the message. Must match the keyId from Step 1 — reject messages with an unknown keyid.
createdSignature creation time (UNIX timestamp). Reject signatures created in the future.
expiresSignature expiry time (UNIX timestamp). Reject expired signatures.

2.1 content-digest

Found in the Content-Digest header:

sha-256=:QPdRoh8ApUZY8jDX2v6PddTHBuPaV4IZNJ7XO4ChhPc=:

Verify this by computing SHA-256 over the raw request body and comparing. Example body:

{
  "type": "lease.changed",
  "version": "1",
  "timestamp": "2021-08-23T00:05:07-04:00",
  "data": {
    "leaseId": 15616,
    "leaseDisplayId": "63603-1",
    "newStatus": "approved",
    "previousStatus": "preApproved",
    "publicDealerId": "ec3a245a-26a9-4047-8283-9f02d9d03ce8",
    "approvedAmount": "4600",
    "orderIds": ["421e326f-c4b3-4563-8c12-0bf5ecda6c52"]
  }
}

2.2 @method

The HTTP method of the request. All Koalafi webhook requests use POST.

2.3 @target-uri

The full URI the request was sent to — this should match the endpoint you registered.

2.4 content-type and message-id

These are the literal header values from the request. For example:

Content-Type: application/json
Message-Id: msgid_daee8e95-6fd2-5c8a-aacb-ec1c06632760

2.5 Putting It Together

Assemble the signature base by formatting each component on its own line:

"content-digest": sha-256=:QPdRoh8ApUZY8jDX2v6PddTHBuPaV4IZNJ7XO4ChhPc=:
"@method": POST
"@target-uri": https://your-webhook-endpoint.example.com/
"content-type": application/json
"message-id": msgid_daee8e95-6fd2-5c8a-aacb-ec1c06632760

3. Verify the Signature

The signature is in the Signature header:

sig1=:XwFfiFbRrLGEEis5Krjl2xBidf86+e/ASPTSROLxiJk2m8gT55CG69TbS1XNGA6o1CwkHec+RQZCiscqnr+QDQ==:

The value is base64-encoded. Decode it, then verify it against the signature base from Step 2 using the public key from Step 1 and the Ed25519 algorithm.

Use a well-tested cryptographic library rather than implementing Ed25519 yourself. The following are trusted options:

LanguageLibrary
Gocrypto/ed25519 (standard library)
Pythonpyca/cryptography
JavaBouncy Castle
C# / .NETBouncy Castle C#
CNaCl / OpenSSL

If you're working in a language or environment not listed above, contact us and we'll help you find the right library.