Rotate an endpoint’s secret when it leaked (or you suspect it leaked), when someone with access leaves the team, when you lost it, or periodically according to your security policy. Rotation generates a new secret and shows it only once, just like when you create the endpoint.

The previous secret stops working instantly

There’s no window in which Compense signs with both. From the moment you call

POST/v1/gateway/webhooks/endpoints/{endpoint_id}/rotate-secret/ , every delivery goes out signed with the new secret. If your receiver doesn’t know it yet, it will reject those deliveries with 401 until you deploy it.

The strategy: verify against a list #

Every SDK verification function accepts several secrets and treats the delivery as valid if it matches any of them (comparing against all of them, without short-circuiting, so as not to leak which one matched). While you rotate, pass [new, old]:

Verify against the new and the previous secret
from compense_gateway.webhooks import verify_webhook

event = verify_webhook(crudo, headers, secret=[SECRETO_NUEVO, SECRETO_VIEJO])

Empty secrets in the list are ignored, so you can read the previous one from a variable that is almost always empty (COMPENSE_WEBHOOK_SECRET_OLD) without touching the code. If the list ends up with no secrets at all, the SDKs treat it as a configuration error (not as an invalid signature): see the errors table.

Step by step #

sequenceDiagram
    autonumber
    participant T as Your team
    participant G as Compense Gateway
    participant V as Secrets manager
    participant R as Your receiver
    T->>R: Deploy the receiver that accepts [current, previous]
    T->>G: rotate-secret
    G-->>T: data.secret = whsec_new (only once)
    T->>V: current = new, previous = old
    V-->>R: The receiver reloads the secrets
    Note over G,R: Deliveries that failed in the meantime are retried automatically
    T->>G: Check the history (deliveries status=failed)
    T->>V: Once everything verifies with the new one, delete the previous one
  1. Prepare the receiver. Deploy (before rotating) a receiver that reads two secrets, the current one and the previous one, from your secrets manager or from environment variables, and verifies against both. If it can reload them without restarting, even better.
  2. Rotate. Call rotate_secret; the response carries the new secret in data.secret.
  3. Store the new one immediately as current and move the existing one to “previous”. Do it in the same process that rotates, to shorten the window in which the receiver doesn’t know the new one.
  4. Have the receiver load it (hot reload or a deployment). During a rolling deployment, some instances will have the new list and others the old one: that’s why the list exists.
  5. Check the deliveries. Those that failed with 401 in the meantime are retried automatically (the first retry goes out after 30 seconds); if any ended up failed or abandoned, retry it manually.
  6. Retire the previous one once the history shows successful deliveries with the new one. Leave the list with a single secret until the next rotation.

Rotate through the API #

import os
from compense_gateway import CompenseClient

client = CompenseClient(api_key=os.environ["COMPENSE_API_KEY"], environment="sandbox")

tx = client.webhooks.endpoints.rotate_secret(
    "3f1a2b3c-0000-4a1b-9c2d-000000000001",
    idempotency_key="nova-wh-erp-001-rota-2026-11",
)
nuevo = tx.data["secret"]          # only once: store it NOW as "current"
print(tx.data["id"], tx.data["rotated_at"])

Response:

JSON
{
  "status": "success",
  "transaction_id": "a1b2c3d4-0000-4a1b-9c2d-000000000020",
  "module": "webhooks",
  "data": {
    "id": "3f1a2b3c-0000-4a1b-9c2d-000000000001",
    "secret": "whsec_...",
    "rotated_at": "2026-11-02T14:00:00Z"
  }
}

Repeating the rotation with the same Idempotency-Key doesn't return the secret

If the response got lost and you retry with the same Idempotency-Key, the gateway returns the original response with "idempotent_replay": true but without secret (it doesn’t store it). In that case the secret that was generated is unrecoverable: rotate again with a new key.

The compense CLI has no command for rotating: use the SDK or cURL.

Typical receiver configuration #

Store two entries in your secrets manager or in environment variables, and read both:

VariableContentWhen it has a value
COMPENSE_WEBHOOK_SECRETCurrent secret (whsec_...)Always
COMPENSE_WEBHOOK_SECRET_OLDPrevious secretOnly during a rotation

In ASP.NET Core, the convention in the SDK examples is a list in configuration (Compense:WebhookSecrets:0 and Compense:WebhookSecrets:1, which as environment variables are written Compense__WebhookSecrets__0 and Compense__WebhookSecrets__1); in Laravel, an array in config/services.php ('webhook_secrets' => array_values(array_filter([...]))). The complete receivers are in Verify the signature.

If you lost the secret #

The gateway doesn’t store secrets, so there’s no way to recover it: no read returns it, and neither does the idempotent replay of create or rotate. The only way out is to rotate it:

  1. Rotate the secret (above) and store the new one.
  2. Configure the receiver with only the new one (the lost one is of no use to you) and deploy.
  3. Deliveries that failed in the meantime are retried automatically; you can bulk-reprocess the ones left failed or abandoned.

If the secret leaked #

Rotate immediately, even if it costs you a few 401s while you deploy: a leaked secret lets anyone send your receiver fake events with a valid signature. Then review in your system the events accepted since the leak and cross-check them against the delivery history: a genuine delivery always shows up there with its event_id.

In the sandbox #

With a sandbox key, rotating returns another fixed secret (whsec_sbx_…), different from the one at creation but always the same for that endpoint. It’s for practicing the flow; there are no real deliveries to be rejected. See Test your webhooks.