Skip to main content
Webhooks are HTTP callbacks that send real-time POST requests to your configured endpoints when specific events occur in Stora. When an event happens — such as an invoice being paid or a unit becoming occupied — Stora immediately notifies all endpoints subscribed to that event type.

Getting started

1

Create a webhook endpoint

Use the Developer Portal to manage webhook endpoints for a test operator or connected operator account. You can also use the Webhook Endpoints API to register a publicly accessible HTTPS URL, the event types you want to subscribe to, and the API version.
2

Store your secret key

When you create an endpoint, Stora generates a secret key. Store it securely — you’ll use it to verify incoming requests.
3

Handle incoming events

Build a handler at your URL that verifies the signature, processes the event, and returns a 2xx response.

Payload structure

Every webhook delivers a JSON payload with this structure:
{
  "event": {
    "id": "evt_1234567890",
    "type": "invoice.paid",
    "api_version": "2025-09",
    "created_at": "2025-01-15T10:30:00Z",
    "data": {
      "invoice": {
        // ... full invoice resource
      }
    }
  }
}
FieldDescription
event.idUnique identifier for the event — use this for idempotency
event.typeThe event type (e.g. invoice.paid, contact.created)
event.api_versionMatches your endpoint’s API version — determines the data structure
event.created_atISO 8601 timestamp of when the event occurred
event.dataThe resource that triggered the event, keyed by resource type

Headers

Every webhook request includes these headers:
HeaderDescription
Content-Typeapplication/json
User-AgentStora-Webhooks/1.0
X-Stora-SignatureHMAC signature for verification (see below)
X-Stora-Request-IdUnique ID for this delivery attempt — useful for debugging

Signature verification

All webhook requests are signed using HMAC SHA256. Always verify the signature before processing. The signature is in the X-Stora-Signature header:
t={timestamp},v1={signature}
To verify:
  1. Extract the timestamp (t) and signature (v1) from the header
  2. Reconstruct the signed payload: {timestamp}.{raw_request_body}
  3. Compute the HMAC SHA256 using your endpoint’s secret key
  4. Compare the computed signature with v1
  5. Optionally, check the timestamp is recent to prevent replay attacks
def verify_webhook_signature(request_body, signature_header, secret)
  parts = signature_header.split(',').map { |p| p.split('=').last }
  timestamp = parts[0]
  signature = parts[1]

  signed_payload = "#{timestamp}.#{request_body}"
  computed = OpenSSL::HMAC.hexdigest('SHA256', secret, signed_payload)

  computed == signature
end
import hmac
import hashlib

def verify_webhook_signature(request_body, signature_header, secret):
    parts = signature_header.split(',')
    timestamp = parts[0].split('=')[1]
    signature = parts[1].split('=')[1]

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

    return hmac.compare_digest(computed, signature)
function verifyWebhookSignature(string $requestBody, string $signatureHeader, string $secret): bool
{
    $parts = explode(',', $signatureHeader);
    $timestamp = explode('=', $parts[0])[1];
    $signature = explode('=', $parts[1])[1];

    $signedPayload = "{$timestamp}.{$requestBody}";
    $computed = hash_hmac('sha256', $signedPayload, $secret);

    return hash_equals($computed, $signature);
}
const crypto = require('crypto');

function verifyWebhookSignature(requestBody, signatureHeader, secret) {
  const parts = signatureHeader.split(',');
  const timestamp = parts[0].split('=')[1];
  const signature = parts[1].split('=')[1];

  const signedPayload = `${timestamp}.${requestBody}`;
  const computed = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(computed),
    Buffer.from(signature)
  );
}

Retries

Stora automatically retries failed deliveries up to 6 times:
AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry6 hours
6th retry12 hours
A delivery is retried when your endpoint returns a non-2xx status code, a network error occurs, or the request times out (20-second limit). After 6 failed attempts, the delivery is marked as failed. If the endpoint is deleted or disabled before a scheduled retry, pending retries are cancelled.

Best practices

Return a 2xx response as quickly as possible — even if you process the event asynchronously. This prevents unnecessary retries.
  • Implement idempotency — use event.id to ensure you don’t process the same event twice. Store processed event IDs and check before processing.
  • Process asynchronously — for time-consuming operations, queue the webhook for background processing after returning a success response.
  • Log the request ID — use X-Stora-Request-Id to correlate retry attempts when debugging.
  • Validate signatures — always verify the HMAC signature before processing.
  • Monitor your endpoint — extended downtime may exhaust all retry attempts.

Available events

EventDescription
contact.churnedTriggered when a contact churns.
contact.convertedTriggered when a contact converts to a customer.
contact.createdTriggered when a contact is created.
contact.updatedTriggered when the contact is updated.
EventDescription
contract.createdTriggered when a contract is created.
contract.signedTriggered when a contract is signed.
EventDescription
coupon.createdTriggered when a coupon is created.
coupon.updatedTriggered when the coupon is updated.
EventDescription
credit_note.createdTriggered when a new credit is created.
credit_note.updatedTriggered when a credit note is updated.
EventDescription
deal.createdTriggered when a deal is created.
deal.lostTriggered when a deal is lost.
deal.reopenedTriggered when a deal is reopened
deal.stage_changedTriggered when a deal’s stage changes. Fires on every stage transition, including the first time a deal enters its initial stage on creation. In that case, this event is emitted alongside deal.created.
deal.updatedTriggered when a deal is updated.
deal.wonTriggered when a deal is won.
EventDescription
identity_verification.cancelledTriggered when an identity verification is cancelled
identity_verification.failedTriggered when an identity verification failed
identity_verification.processingTriggered when an identity verification is processing
identity_verification.succeededTriggered when an identity verification succeeded
EventDescription
invoice.createdTriggered when a new invoice is created.
invoice.finalizedTriggered when an invoice is finalized.
invoice.marked_uncollectibleTriggered when an invoice is marked as uncollectible.
invoice.paidTriggered when an invoice is mark as paid.
invoice.updatedTriggered when an invoice is updated.
EventDescription
note.createdTriggered when a note is created.
note.updatedTriggered when a note is updated.
EventDescription
order.abandonedTriggered when an order is abandoned.
order.completedTriggered when the order is completed.
order.createdTriggered when a new order is created.
order.finalizedTriggered when a new order is finalized (ready to be paid).
EventDescription
protection_level.createdTriggered when a protection level is created.
protection_level.updatedTriggered when a protection level is updated.
EventDescription
subscription.cancelledTriggered when the subscription is cancelled.
subscription.createdTriggered when the subscription is created.
subscription.endedTriggered when the subscription is ended.
subscription.resumedTriggered when a cancellation is undone and the subscription is resumed.
subscription.startedTriggered when the subscription is started.
EventDescription
task.completedTriggered when the task is completed.
task.createdTriggered when a task is created.
task.reopenedTriggered when a completed task is reopened.
task.updatedTriggered when a task is updated.
EventDescription
tenancy.auto_reservation.failedTriggered when no requested units can be automatically reserved for a tenancy.
tenancy.auto_reservation.partially_succeededTriggered when some, but not all, requested units are automatically reserved for a tenancy.
tenancy.auto_reservation.succeededTriggered when all requested units are automatically reserved for a tenancy.
tenancy.createdTriggered when a tenancy is created.
tenancy.startedTriggered when the tenancy is started.
EventDescription
unit.availableTriggered when a unit is made available.
unit.createdTriggered when a unit is created.
unit.deallocatedTriggered when a unit is deallocated.
unit.occupiedTriggered when a unit is occupied.
unit.overlockedTriggered when a unit is overlocked.
unit.repossessedTriggered when a unit is repossessed.
unit.reservedTriggered when a unit is reserved.
unit.unavailableTriggered when a unit is made unavailable.
unit.updatedTriggered when a unit is updated.
EventDescription
unit_type.createdTriggered when a unit type is created.
unit_type.updatedTriggered when a unit type is updated.
For full payload schemas, see the Webhooks section in the API reference.