All pages
Powered by GitBook
1 of 7

Webhooks

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.

Setting Up a Webhook

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'

Trigger Entity and Event

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

Enable

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.

Configuring the Webhook

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

Built-In Headers

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.

Webhook Signature Verification

All webhook requests sent from Pakk are signed. Here's how to verify the authenticity of the incoming request.

How Requests are Signed

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.

Verifying the 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"))
}

Tips and Tools

  • 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.

Data Transformation

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.

Write custom Javascript to transform data

Enable Data Transformation

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.

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

Aborting Webhook Execution

If the business logic in your transformation code needs to abort execution of the webhook for any reason, simple return 0, null or false.

Tips

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.

Testing the Webhook

Test Record

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.

Running a Test

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.

JSON Payload

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.

Test Results

After running a test, results can be viewed in the 'History' tab

Webhook Execution Log

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

Successul Executions

For executions which were successful, you'll simply see the server result (200).

Unsuccessful Executions

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