Docs/Notify/Webhooks

Webhooks#

Webhooks let Karla push events to your systems in real time. Instead of polling for changes, your endpoint receives a POST request the moment something happens — a shipment goes out for delivery, a parcel is delivered, a claim is submitted, etc.

Create a webhook in the portal#

The easiest way is the Karla Portal: go to Settings → Webhooks, add a destination URL, pick the events you care about, and save. Karla starts delivering events immediately.

That's all most merchants need. The rest of this page covers the programmatic setup and the details of how to build a receiver that stays secure and reliable.

Create a webhook via the API#

If you'd rather manage webhooks from code (CI pipelines, infra-as-code, multi-shop setups), use the Webhooks API.

POST /v1/shops/{slug}/webhooks

curl -X POST https://api.gokarla.io/v1/shops/your-shop-slug/webhooks \
  -u your-username:your-private-api-key \
  -H "Content-Type: application/json" \
  -d '{
    "enabled_events": [
      "shipments/in_delivery/DELIVERY_ATTEMPTED",
      "shipments/delivered"
    ],
    "secret": "41013bd9-9072-42cd-9902-66da38361be9",
    "description": "Shipment Deliveries",
    "status": "active",
    "url": "https://example.com/my-webhook-endpoint"
  }'
const response = await fetch(
  "https://api.gokarla.io/v1/shops/your-shop-slug/webhooks",
  {
    method: "POST",
    headers: {
      Authorization:
        "Basic " +
        Buffer.from("your-username:your-private-api-key").toString("base64"),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      enabled_events: [
        "shipments/in_delivery/DELIVERY_ATTEMPTED",
        "shipments/delivered",
      ],
      secret: "41013bd9-9072-42cd-9902-66da38361be9",
      description: "Shipment Deliveries",
      status: "active",
      url: "https://example.com/my-webhook-endpoint",
    }),
  },
);

const webhook = await response.json();
console.log(webhook);
import requests
from requests.auth import HTTPBasicAuth

webhook_data = {
    "enabled_events": [
        "shipments/in_delivery/DELIVERY_ATTEMPTED",
        "shipments/delivered"
    ],
    "secret": "41013bd9-9072-42cd-9902-66da38361be9",
    "description": "Shipment Deliveries",
    "status": "active",
    "url": "https://example.com/my-webhook-endpoint"
}

response = requests.post(
    'https://api.gokarla.io/v1/shops/your-shop-slug/webhooks',
    json=webhook_data,
    auth=HTTPBasicAuth('your-username', 'your-private-api-key')
)

webhook = response.json()
print(webhook)
<?php
$ch = curl_init();

$webhook_data = [
    "enabled_events" => [
        "shipments/in_delivery/DELIVERY_ATTEMPTED",
        "shipments/delivered"
    ],
    "secret" => "41013bd9-9072-42cd-9902-66da38361be9",
    "description" => "Shipment Deliveries",
    "status" => "active",
    "url" => "https://example.com/my-webhook-endpoint"
];

curl_setopt($ch, CURLOPT_URL, 'https://api.gokarla.io/v1/shops/your-shop-slug/webhooks');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($webhook_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_USERPWD, 'your-username:your-private-api-key');

$response = curl_exec($ch);
curl_close($ch);

$webhook = json_decode($response, true);
print_r($webhook);
?>
require 'net/http'
require 'uri'
require 'json'

uri = URI('https://api.gokarla.io/v1/shops/your-shop-slug/webhooks')
request = Net::HTTP::Post.new(uri)
request.basic_auth('your-username', 'your-private-api-key')
request['Content-Type'] = 'application/json'

webhook_data = {
  enabled_events: [
    "shipments/in_delivery/DELIVERY_ATTEMPTED",
    "shipments/delivered"
  ],
  secret: "41013bd9-9072-42cd-9902-66da38361be9",
  description: "Shipment Deliveries",
  status: "active",
  url: "https://example.com/my-webhook-endpoint"
}

request.body = webhook_data.to_json

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

webhook = JSON.parse(response.body)
puts webhook

If no enabled_events is provided, the webhook will listen to ALL events (["*"]). See Events for the full event catalog and how filtering works.

Configuration fields#

FieldDefaultDescription
urlrequiredYour publicly reachable HTTPS endpoint. Localhost and private-network addresses are rejected, and redirects are not followed — register the final URL of your receiver.
enabled_events["*"]The event references to subscribe to. Values outside the event catalog are rejected with a 422 validation error.
secretgeneratedThe signing secret, 16–64 characters. If you don't provide one, Karla generates it for you.
descriptionAn optional label for the endpoint.
statusactiveactive or inactive. Inactive webhooks receive no deliveries.
dedup_enabledtrueWhether shipment events are deduplicated and filtered for staleness before delivery (see below).
stale_event_threshold24Hours (1720) after which a shipment event counts as stale and is not delivered. Only applies while dedup_enabled is true.

Event deduplication and staleness#

By default, Karla filters shipment events before delivering them to your webhook:

  • One notification per event group. Several carrier events can map to the same event group; with dedup_enabled: true your endpoint receives at most one notification per event group per shipment, so you don't need to deduplicate on your side.
  • Stale events are dropped. A shipment event older than stale_event_threshold hours is not delivered, which keeps late carrier backfills from triggering outdated notifications.

Set dedup_enabled: false if you want the raw firehose instead: every carrier event is delivered as it arrives, with no deduplication and no staleness filtering.

Deduplication applies to shipment events only — claim events are always delivered.

Managing webhooks#

You can check which webhooks are defined in your shop using the Search Webhook endpoint.

Webhooks can be updated once they are live, using the Update Webhook endpoint. You can change description, status, url, dedup_enabled, and stale_event_threshold; the event selection and the secret are fixed at creation.

{
  "description": "My new description",
  "url": "https://example.com/my-new-webhook-endpoint"
}

You can delete webhooks with the Delete Webhook endpoint.

Securing your endpoint#

You should secure your integration by making sure your handler verifies that all webhook requests are generated by Karla.

We include a Karla-Signature header in each signed event that contains a timestamp and a signature that you should verify. The timestamp has a t= prefix, and the signature has a v1= prefix.

Karla-Signature:
t=1710864000,
v1=7f3a8b2c1d9e4f6a5b8c7d0e3f2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1

We provide newlines for clarity, but a real Karla-Signature header is on a single line.

Karla generates signatures using a hash-based message authentication code (HMAC) with SHA-256.

Verify the signature#

  1. Extract the timestamp and signatures from the header.
  2. Concatenate the timestamp as a string with . and the actual JSON payload.
  3. Compute an HMAC with the SHA256 hash function. Use the provided signing secret as the key.
  4. Compare the signature in the header to the expected signature. For an equality match, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.

Webhook Verification Sample Code#

Node.js/TypeScript#

import crypto from "crypto";

function verifyKarlaSignature(
  payload: string,
  signature: string,
  secret: string,
): boolean {
  const elements = signature.split(",");
  const timestamp = elements.find((el) => el.startsWith("t="))?.split("=")[1];
  const providedSignature = elements
    .find((el) => el.startsWith("v1="))
    ?.split("=")[1];

  if (!timestamp || !providedSignature) {
    return false;
  }

  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac("sha256", secret)
    .update(signedPayload)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(providedSignature, "hex"),
    Buffer.from(expectedSignature, "hex"),
  );
}

// Usage
const isValid = verifyKarlaSignature(
  JSON.stringify(webhookPayload),
  request.headers["karla-signature"],
  "your-webhook-secret",
);

Python#

import hmac
import hashlib
import time

def verify_karla_signature(payload: str, signature: str, secret: str) -> bool:
    elements = signature.split(',')
    timestamp = next((el.split('=')[1] for el in elements if el.startswith('t=')), None)
    provided_signature = next((el.split('=')[1] for el in elements if el.startswith('v1=')), None)

    if not timestamp or not provided_signature:
        return False

    signed_payload = f"{timestamp}.{payload}"
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        signed_payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(provided_signature, expected_signature)

# Usage
is_valid = verify_karla_signature(
    json.dumps(webhook_payload),
    request.headers.get('karla-signature'),
    'your-webhook-secret'
)

Retry strategy#

Karla sends data to your handler via POST. In case of an unsuccessful event (non 2xx response), or if your endpoint takes longer than 15s to respond, Karla attempts to deliver your webhooks for up to 15 times with an exponential back off. The event will be lost if all attempts are exhausted.

Auto-pause on dead endpoints#

If your endpoint repeatedly responds with 404 or 410 — the signature of a deleted or moved receiver — Karla automatically pauses the webhook: its status is set to inactive and deliveries stop. Server errors (5xx) never trigger a pause; they follow the retry strategy above. Any successful (2xx) delivery resets the failure count, so an endpoint that recovers is not paused.

Keep your endpoint URL alive. If you move or retire a receiver, update the webhook's url (or delete the webhook) instead of letting the old address return 404. A paused webhook stays inactive until you reactivate it in the Karla Portal or set its status back to active via the Update Webhook endpoint.

Specification#

Host: api.gokarla.io
Content-Length: 12345
User-Agent: KarlaWebhookClient/1.0
Karla-Signature: t=1710864000,v1=7f3a8b2c1d9e4f6a5b8c7d0e3f2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1
Content-Type: application/json

Body#

Body follows the format described in the Events reference.

IP Whitelisting#

If your service has a Firewall restricting public IPs, please add 34.77.48.225 to the allow list.

Sending traffic to your systems via a static IP is a premium service that has to be enabled in advance.

Was this helpful?