Gareth Wilson Gareth Wilson

Guide to Celitech Webhooks: Features and Best Practices

Published


Celitech sells eSIM data packages through an API, so travel apps, telecom resellers and other businesses can offer mobile data to their own customers. Its webhooks report two kinds of change: a customer has used 50% or 80% of a package's data, or the eSIM itself has changed state on the customer's phone, from download and install through to enable, disable and delete. Teams use them to send top-up offers before data runs out and to track whether a customer actually installed what they bought.

What are Celitech webhooks?

Webhooks aren't on by default. Celitech enables them per account when you ask, and the Developers > Webhooks page then lets you add endpoints and pick event types. Each delivery is a JSON POST.

Celitech's docs don't name a delivery platform, but the headers are svix-id, svix-timestamp and svix-signature, and the verification samples install the svix package. The signature is the Standard Webhooks HMAC-SHA256 construction that Svix uses.

Celitech webhook features

FeatureDetails
AvailabilityEnabled per account on request
ConfigurationDashboard, Developers > Webhooks
SignatureStandard Webhooks HMAC-SHA256
Signature headersvix-signature, with svix-id and svix-timestamp
SecretOne per endpoint, rotatable in the dashboard
BodyJSON
Event routingBody field type
Response window2xx within about 15 seconds
RetriesIncreasing delays over a period of hours, then the endpoint may be disabled
Test eventsSent from the dashboard, never retried
EndpointsPublic HTTPS only
Source IPsNot published

Events and payloads

Documented eventMeaning
consumption.threshold.reachedSent when an eSIM's data usage crosses 50% or 80% of its package's data limit.
esim.downloadedSent when an eSIM profile is downloaded to the user's device.
esim.installedSent when an eSIM is installed on the user's device.
esim.enabledSent when an eSIM is enabled on the user's device (turned on).
esim.disabledSent when an eSIM is disabled on the user's device (turned off).
esim.deletedSent when an eSIM is permanently deleted from the user's device.

Bodies are flat, with no data wrapper. consumption.threshold.reached carries purchaseId, dataLimitInGB, dataUsageRemainingInGB, actualPercentage and notificationPercentage, the threshold that fired. The five esim.* events carry only type and the eSIM's iccid, so you look the eSIM up by ICCID to find the customer.

Celitech's API reference documents each body as an OpenAPI schema with per-field examples rather than a complete sample, and Hookdeck's samples library has none yet. Send an example event from the dashboard to see a real body before you write parsing code.

Setting up Celitech webhooks

  1. Ask Celitech to enable webhooks on your account if Developers > Webhooks isn't available.
  2. In Developers > Webhooks, add an endpoint with an HTTPS URL and a description.
  3. Select the event types you need and save. There is no verification challenge.
  4. Copy the endpoint's signing secret and store it as CELITECH_WEBHOOK_SECRET.

Celitech's docs don't show the secret's format. Svix endpoint secrets start with whsec_, and the svix library, Hookdeck's source and the function below all base64-decode the part after that prefix. Check that your secret has it before you deploy.

To use Hookdeck, create a source with the Celitech type, paste the secret, and use the source URL as the endpoint URL. The Hookdeck CLI forwards the source to your machine for testing:

npx hookdeck-cli listen 3000 celitech --path /webhooks/celitech

Dashboard example events are a single attempt. If one fails, send it again by hand.

Securing Celitech webhooks

The signed content is <svix-id>.<svix-timestamp>.<raw body>, the key is the base64-decoded secret after whsec_, and the header can list several v1, signatures separated by spaces. The function below checks each one and uses webhook-* headers when svix-id isn't there:

const crypto = require('node:crypto');

function decodeBase64(value) {
  if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
  const bytes = Buffer.from(value, 'base64');
  return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
    ? bytes : null;
}

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const prefix = headers['svix-id'] ? 'svix' : 'webhook';
  const id = headers[`${prefix}-id`];
  const timestamp = headers[`${prefix}-timestamp`];
  const signature = headers[`${prefix}-signature`];
  if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
      !/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
  const key = decodeBase64(secret.replace(/^whsec_/, ''));
  if (!key) return false;
  const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
  const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    const received = decodeBase64(candidate);
    if (!received) return false;
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

Celitech's own example uses new Webhook(secret).verify(...) from the svix package, which also rejects a timestamp it considers "too far in the past or future". Celitech doesn't give a number. Svix's libraries use five minutes, and an event Hookdeck retries or replays keeps the original svix-timestamp, with no timestamp tolerance applied by Hookdeck. The function above skips the age check so those deliveries still verify.

For threshold events the useful work is a customer notification, which can wait for a worker:

const express = require('express');

// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
  try {
    const value = JSON.parse(rawBody.toString('utf8'));
    return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
  } catch {
    return null;
  }
}

// jobs.enqueue(name, payload) resolves once the job is stored.
function createCelitechReceiver(jobs) {
  const app = express();
  app.post('/webhooks/celitech', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.CELITECH_WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

    const event = parseJsonObject(req.body);
    if (!event) return res.sendStatus(400); // signed, but not a JSON object
    const deliveryId = req.headers['svix-id'] || req.headers['webhook-id'];
    const job = event.type === 'consumption.threshold.reached' ? 'offer-top-up' : 'sync-esim-state';
    try {
      await jobs.enqueue(job, { deliveryId, event });
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200); // well inside the 15-second window
  });
  return app;
}

When the request comes through a Hookdeck Celitech source, a bad signature doesn't produce a 401. Hookdeck answers 200 and marks the request as failed verification, so it never reaches the receiver.

Make Celitech webhooks production-ready. Hookdeck Event Gateway verifies the Svix signature on every eSIM and data-usage event, records each delivery, and holds events for your notification service while it is unavailable.

Celitech webhook limitations and pain points

Access depends on Celitech

The Problem: The webhook settings aren't there when you go to build the integration.

Why It Happens: Webhook access is enabled per account by Celitech, on request.

Workarounds:

  • Ask for webhooks early in the project, and poll the usage API until they are on.

How Hookdeck Can Help: Once you have access, a Hookdeck source gives you a stable URL and a request log from the first delivery, which helps when you are seeing Celitech's bodies for the first time.

The endpoint can be disabled

The Problem: After a long outage, Celitech stops sending events to your endpoint.

Why It Happens: Failed deliveries are retried with increasing delays "over a period of hours", and after that the endpoint may be disabled. Celitech doesn't publish the attempt count or intervals.

Workarounds:

  • Check the endpoint's status in Developers > Webhooks after any outage.
  • Reconcile usage through the API for purchases that should have crossed a threshold.

How Hookdeck Can Help: Hookdeck acknowledges Celitech straight away, so your downtime doesn't count against the endpoint, and holds the events until your service can take them.

Thin eSIM events

The Problem: An esim.deleted event tells you which eSIM, but not which customer or purchase.

Why It Happens: The esim.* bodies carry only type and iccid.

Workarounds:

  • Store the ICCID against the customer when you create the purchase, and look it up on each event.

How Hookdeck Can Help: A transformation can't add your customer data, but it can rename or reshape fields so every Celitech event reaches your service in the format it already expects.

The secret format is undocumented

The Problem: You can't confirm from the docs how to use the secret.

Why It Happens: Celitech never prints a secret value. It is most likely a Svix whsec_ secret, but the docs don't say.

Workarounds:

  • Look at the secret in the dashboard. If it doesn't start with whsec_, contact Celitech before relying on any verifier.

How Hookdeck Can Help: Send a dashboard example event to a Hookdeck source and check the verification result in the request log before you go live.

Best practices

Deduplicate on svix-id

Every retry of a delivery carries the same svix-id. Store it before acting, so a retried threshold event doesn't send two top-up emails. There is more on this in implementing webhook idempotency.

Use both thresholds

The 50% and 80% notifications arrive separately for the same purchaseId. Check notificationPercentage and send a gentle reminder at 50% and a stronger one at 80%, or act only on the one that fits your product.

Track installation, not only purchase

esim.installed and esim.enabled tell you the customer actually set the eSIM up. Purchases with no install event after a day or two are a good trigger for a support message.

Acknowledge quickly

Return 2xx well inside the 15-second window and send notifications from a worker. See why to process webhooks asynchronously.

Conclusion

Celitech's six events track data running low and eSIM state changes on a device. Request access early, and keep the endpoint healthy so it isn't disabled after hours of failures.

Hookdeck Event Gateway gives Celitech a target that is always up and gives you a record of every eSIM event. Try it free.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.