← All posts
Tutorial

Rotate a UnifyPort Webhook Signing Secret Safely

To rotate a UnifyPort webhook signing_secret, prepare every receiver to verify both the current and replacement secret before updating the endpoint. Confirm a fresh delivery verifies with the replacement, then retire the old verifier after your cutover checks. This is an application-managed transition: the public API documents one signing secret per endpoint, not a server-managed overlap period or a zero-loss rotation guarantee.

Key takeaways

  • Rotate the webhook secret separately from the REST API key.
  • Never send an empty signing_secret as an intermediate step: it disables signing.
  • Keep the receiver’s temporary key set scoped to one endpoint and environment.
  • Do not use delivery retries as a deployment grace period.

Identify the secret and the failure boundary

X-Api-Key authenticates your calls to UnifyPort. The endpoint’s signing_secret authenticates deliveries arriving at your application. Changing one does not replace the other. For REST callers, use the separate API key rotation runbook.

The webhook delivery reference defines X-Device-Signature as hex-encoded HMAC-SHA256 over the RFC 3339 X-Device-Timestamp, a literal dot, and the raw request body. Rotation changes the HMAC key, not that input or the event schema.

A mismatched receiver can reject authentic events. The current delivery contract retries connection errors and HTTP 408, 429, and 5xx responses immediately, without backoff; other 4xx responses are not retried. A 401 caused by a premature key change is therefore not something to expect a later deployment to repair automatically. Nor does returning 503 create a dependable maintenance queue.

Plan the transition as three configurations

This table is a proposed deployment sequence, not a built-in UnifyPort rotation feature.

PhaseEndpoint configurationReceiver verification
PrepareCurrent secretCurrent and replacement secrets
SwitchReplacement secretCurrent and replacement secrets
RetireReplacement secretReplacement secret only

Before starting, record the endpoint ID, URL, status, event subscriptions, retry policy, receiver instances, and change owner. Keep secret values in your secret manager, not the change record. Generate an independent replacement and distribute it through your normal protected deployment mechanism.

Keep the URL, subscriptions, and retry settings unchanged during this operation. Moving ingress and rotating authentication together makes failures harder to isolate. If the old secret may be exposed, do not use ordinary overlap: continued acceptance of that secret preserves the exposure. Follow an incident cutover with explicit availability and reconciliation decisions instead.

Prepare every receiver before changing the sender

Implement a small, temporary key set in trusted route configuration. Do not choose keys from an unverified provider, account_id, or invented key-version header. The documented delivery headers do not provide a signing-key identifier.

The following illustrative helper checks both candidates without returning early on the first match. It is not a complete HTTP receiver or reported test result. keys must be a nonempty, endpoint-scoped array of secret strings; maxAgeMs is your positive, finite freshness tolerance.

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyDuringRotation({
  rawBody, timestamp, signature, keys, maxAgeMs,
}) {
  if (!Array.isArray(keys) || keys.length === 0 ||
      keys.some(key => typeof key !== 'string' || key.length === 0) ||
      !Number.isFinite(maxAgeMs) || maxAgeMs <= 0) {
    throw new Error('Invalid webhook verification configuration');
  }
  if (!Buffer.isBuffer(rawBody) || typeof timestamp !== 'string' ||
      typeof signature !== 'string' || !/^[0-9a-f]{64}$/i.test(signature)) {
    return false;
  }
  const signedAt = Date.parse(timestamp);
  if (!Number.isFinite(signedAt) ||
      Math.abs(Date.now() - signedAt) > maxAgeMs) return false;

  const supplied = Buffer.from(signature, 'hex');
  let matches = 0;
  for (const key of keys) {
    const expected = createHmac('sha256', key)
      .update(timestamp + '.').update(rawBody).digest();
    matches |= Number(timingSafeEqual(supplied, expected));
  }
  return matches !== 0;
}

The Node.js Crypto reference documents these HMAC and comparison primitives. Preserve the raw bytes and reject missing signatures; do not add an unsigned fallback. Apply payload validation and durable acceptance after verification. The HMAC replay-protection guide explains why freshness and duplicate-safe processing remain necessary after a signature matches.

Test current-key and replacement-key requests on every receiver deployment. Also test a wrong key, a modified body, a missing signature, and a stale timestamp. These are proposed acceptance tests, not claims that this code has been run in your environment.

Update the endpoint without disabling signing

Read the current configuration using Get webhook endpoint. Then use Update webhook endpoint, PATCH /v1/webhook-endpoints/{endpoint_id}, authenticated with your REST API key.

Construct the update from the reviewed current URL, active status, subscriptions, and retry policy, with the replacement value in signing_secret. Do not copy the reference’s illustrative empty secret or inactive status into a live cutover. An empty secret disables signing; it is not a request for automatic rotation.

Read back the configuration and check signing_enabled: true along with the unchanged settings. This flag proves signing is enabled, not which secret is in use. Send a controlled message to a connected messaging account and trace its fresh message.received delivery through replacement-key verification, durable storage, and the expected internal processing. Never log either secret or use customer content as a test fixture.

If the update response is ambiguous, keep the dual verifier while inspecting configuration and testing a fresh delivery. Do not retire a key merely because the PATCH request was sent.

Retire deliberately; do not invent a waiting interval

The public reference does not specify whether already queued attempts retain an old signing configuration, nor a maximum rotation overlap interval. retry_policy.max_attempts counts retries, not seconds of grace. Confirm uncertain in-flight behavior with UnifyPort before promising uninterrupted rotation.

Use deployment completion, fresh replacement-key deliveries, receiver error observations, and your agreed handling of in-flight work as retirement gates. Keep only non-secret verification-version labels in operational metrics. A quiet receiver alone does not prove that old-key traffic has drained.

Once those gates pass, remove the current secret from every receiver and deployment source. In an isolated test, verify that the retired secret is now rejected. Keep the overlap bounded by an explicit operational decision, not permanent acceptance of historical keys.

If a rollback is necessary and the old key remains trusted, coordinate the endpoint setting and receiver key set together. Do not restore an old-only receiver while the endpoint still signs with the replacement. If exposure is suspected, do not restore the exposed key.

Scope and limitations

UnifyPort’s unofficial interface normalizes supported messaging-account events; this procedure secures its handoff to your receiver. It does not rotate Telegram-native or LINE-native credentials, provider sessions, or API keys.

Store authenticated events before returning 2xx. Ordinary event retries can be deduplicated by event ID; WhatsApp conversation.history needs message-level merging under the documented contract, not global suppression by the top-level ID. UnifyPort offers no REST message-history read API or guaranteed replay of missed payloads, so rotation failures must not be described as automatically recoverable.

FAQ

Does UnifyPort support two signing secrets on one endpoint?

The public endpoint contract exposes one signing_secret. The temporary two-key verifier described here belongs to your application; it is not a two-secret API setting.

Can I disable signing briefly to simplify the change?

No. Keep signing enabled and fail closed when authentication is missing. An empty secret removes the signature header rather than providing a transition mechanism.

How long should I accept the old key?

There is no documented universal interval. Use explicit deployment and delivery checks, resolve in-flight behavior, and set a bounded retirement decision appropriate to your risk model.

Next step and sources

Review Update webhook endpoint and rehearse the three configurations in an isolated environment before a production change.

References checked on 2026-09-27:

UnifyPort API

Turn messaging integration into a stable product pipeline.

Start by sending through one API, then bring every inbound message back into your business system with standard events.