Build custom integrations to other services using event-based webhooks - available for all record types in Pakk.
Webhooks refers to an integration technique where you give Pakk the address of a third-party service/server that you'd like it to 'call into' whenever a certain type of event happens in Pakk.
Webhooks allow you to build trigger-based integrations into external systems - in that respect they can be seen as the flip side of the coin from the Pakk API. The API needs to be 'called into' either by an external system or integration middleware, making it a 'pull'-based mechanism. For example, if you wanted to add new Pakk orders to an external system and you could only use the Pakk API, you'd have to use a technique like polling via the API to regularly check for new orders in Pakk.
Webhooks essentially inverts that paradigm, allowing you to contact an external service immediately on record creation/update - making it a 'push'-based mechanism.
In this section we explain how to work with webhooks to integrate Pakk into external systems.
Create a new webhook by going to Setup > Webhooks > Create New
Give your webhook a name - this is internal only, and serves for you to be able to identify the function of the webhook in the future. A good name might be something like 'Send Orders to Warehouse'
The first think you need to decide is what triggers the execution of this webhook. Pakk supports triggering webhooks on both creation and update of *any* record type in the Pakk system (e.g. Order, Purchase Order, Return, Expense).
You can work on, and test, your webhook without it actually triggering when the trigger condition is met. Once you're ready to 'go live' with the webhook, simply enable it.
The most important field you need to enter is the webhook URL - this is the remote server address you want to ping when the trigger entity/event condition is met.
As well as the URL, you also have the option to set up any number of custom headers in the request. These would most often be used for authentication against the remote API, for example: Authorization: Bearer 238ujdfnd8gudjfdnfg
Note that Pakk, by default and without you having to specify them, will send a number of additional headers with the request:
User-Agent: Pakk-Webhook-Tester
X-Pakk-WebhookID: xxxxxxxxxxxxxxxxxxxxxxxxx
(actually sends the unique ID of the webhook)
X-Pakk-Webhook-TriggerEvent: Create/Edit
X-Pakk-Webhook-TriggerEntity: entityName
X-Pakk-Webhook-Signature: xxxxxxxxxxxxxxxxxxxxx
(sends the verification signature)
You can use the X-Pakk-WebhookID
as a simple method to secure the API endpoint on the remote server since the webhook unique identifier won't be known to any external systems and so functions as a quasi API key. If you ever need to 'revoke' the key, you can simple inactivate that webhook, duplicate it, and you'll have a new one with a different ID.
We recommend, however, that you implement webhook signature verification on your server, if you have code control over the endpoint.
All webhook requests sent from Pakk are signed. Here's how to verify the authenticity of the incoming request.
Pakk signs all webhook requests by hashing the request body bytes using HMAC-SHA256 with the webhook signing key (which you can find on the Webhook record in the admin panel) and encoding to Base64. The resulting signature is send in the header X-Pakk-Webhook-Signature
.
You should implement verification code that performs the same procedure and compares the result against the received signature. Here's an example of what that code would look like in Go:
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"io"
"net/http"
)
// SharedSecret is the secret key used for signing and verifying webhook requests.
// In production, securely retrieve this from a configuration file or environment variable.
var SharedSecret = "your-shared-secret"
// VerifyWebhookSignature verifies that the request's payload matches the signature in the header.
func VerifyWebhookSignature(payload []byte, receivedSignature string) bool {
// Generate the HMAC-SHA256 signature
hash := hmac.New(sha256.New, []byte(SharedSecret))
hash.Write(payload)
expectedSignature := base64.StdEncoding.EncodeToString(hash.Sum(nil))
// Use hmac.Equal to prevent timing attacks when comparing signatures
return hmac.Equal([]byte(expectedSignature), []byte(receivedSignature))
}
// WebhookHandler handles incoming webhooks and verifies their signatures.
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
// Extract the signature from the header
receivedSignature := r.Header.Get("X-Pakk-Webhook-Signature")
if receivedSignature == "" {
http.Error(w, "Missing signature header", http.StatusUnauthorized)
return
}
// Read the raw JSON payload from the request body
payload, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
return
}
defer r.Body.Close() // Make sure to close the body when done reading
// Verify the signature
if !VerifyWebhookSignature(payload, receivedSignature) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process the webhook payload as it's been verified successfully
w.WriteHeader(http.StatusOK)
w.Write([]byte("Webhook received and verified"))
}
Use a testing tool like https://www.devglan.com/online-tools/hmac-sha256-online to quickly produce expected hashes and compare to the signature sent by Pakk using the previously suggested webhook testing tool.
The Pakk webhooks framework enables advanced use cases with data transformation using Javascript code you write yourself.
If you have control over the server code at the webhook endpoint, then the shape of the data being sent is less important as you will be able to parse and process the data as needed with your own server-side code.
However, when the webhook endpoint is outside of your control, for example, if it is part of a third-party SaaS application, you'll probably need the shape of the webhook data to conform to a specific model. In this case, you can use Pakk's 'webhook data transformation' functionality to sculpt the request body that is sent to the webhook.
Before you can run any data transformation code, you need to check 'Enable Transformation' on the webhook configuration screen. This will activate a code input box for the transformation code.
Enter the Javascript code to be run over the data. You'll need to take the following into account:
Your code is executed in the cloud in a sandboxed Deno environment (as opposed to Node). Deno is mostly similar to Node, but if you need to get into more advanced use cases, you'll need to consult the Deno documentation: https://docs.deno.com
There are limits to the complexity of the code and the compute time it can take up - if you start hitting these limits, you'll be able to see the errors on the Webhook log
The input data to your code is as a Javascript object data.
This data object has one or two sub objects: newState
(always present) and previousState
(only present for updates). Each of these obejcts is the full JSON representation of the record.
The outut of your code should be a Javascript object with the same name, data
If the business logic in your transformation code needs to abort execution of the webhook for any reason, simple return 0
, null
or false
.
For most use cases, the easist thing to do is probably edit the input data
and return it. For example, seting the memo
field on an order could be done like this:
data.newState.memo="Testing"; return data.newState;
Alternatively, if you only need to return a small subset of fields, for example, you only need to extract the customer's email address from an order, you could start with a blank data
object, like this:
data={"email": data.newState.customer.email}; return data;
As always, you can view the shape of the input data in the regular API docs, or simply send test webhook requests to a testing service without any transformations to directly see the untransformed data shape.
Remember that any transformations you apply are also applied when you test the webhook, making development easy.
Sometimes you might need to take action depending on how a record has changed - this can be achieved by comparing the previous state to the new state. For example, you could determine if an order has just been dispatched by comparing the new status to the previous.
You can select a record from the database (obviosuly of the same type as the trigger entity) that will be sent as the payload when using the 'Test' action.
We highly recommend a webhook testing service like Webhook.site to send test requests, allowing you to observe the headers and payload in real time.
The shape of the JSON payload is the full record. You can either observe the payload in a testing service like Webhook.site, or refer to the API documentation for the entity in question - the API request/response formats are exactly the same as those for webhooks.
After running a test, results can be viewed in the 'History' tab
Each webhook execution is logged in the system for reference and debugging.
To view the execution log for any webhook, go to its 'History' tab - there you'll get a top-level list view of the most recent execution along with details of when the execution happened and whether it was successful. You can drill into any execution for more details by clicking 'View'.
For executions which were successful, you'll simply see the server result (200).
In the case of failed executions, the system logs more information for debugging purposes:
Result: will either be a non-200 result in the case of server errors, or 0 in the case that the server was not reached
Headers: the headers that were sent as part of the request
Request Body: a copy of the JSON payload that was sent
Response Body: a copy of the JSON response from the server
Hard Error: a hard error means the server was not even reached
Error Message/Error: in the case of a hard error, error messages are logged