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
| Field | Description |
|---|---|
sig1 | Label for this signature. Multiple signatures may appear as a comma-separated list. |
(...) | Ordered list of components that make up the signature base. |
keyid | Identifier of the key used to sign the message. Must match the keyId from Step 1 — reject messages with an unknown keyid. |
created | Signature creation time (UNIX timestamp). Reject signatures created in the future. |
expires | Signature expiry time (UNIX timestamp). Reject expired signatures. |
2.1 content-digest
content-digestFound 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
@methodThe HTTP method of the request. All Koalafi webhook requests use POST.
2.3 @target-uri
@target-uriThe full URI the request was sent to — this should match the endpoint you registered.
2.4 content-type and message-id
content-type and message-idThese 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:
| Language | Library |
|---|---|
| Go | crypto/ed25519 (standard library) |
| Python | pyca/cryptography |
| Java | Bouncy Castle |
| C# / .NET | Bouncy Castle C# |
| C | NaCl / OpenSSL |
If you're working in a language or environment not listed above, contact us and we'll help you find the right library.
