DashboardDeveloper Portal

Webhooks

Receive real-time event notifications from the ElevatedPOS platform. When an event occurs, ElevatedPOS sends an HTTP POST request to your configured endpoint with a signed JSON payload.

Overview

Subscribe

Register one or more HTTPS endpoints in your integration dashboard. Choose which event types to subscribe to.

Receive

ElevatedPOS makes an HTTP POST to your endpoint within seconds of an event occurring, with a JSON payload.

Verify & Process

Validate the HMAC-SHA256 signature before processing. Return 2xx within 30 seconds to acknowledge receipt.

How HMAC-SHA256 Signing Works

Every webhook delivery includes an X-ElevatedPOS-Signature header containing an HMAC-SHA256 digest of the raw request body, keyed with your webhook secret:

X-ElevatedPOS-Signature: sha256=<hex-digest>

# Computed as:
HMAC-SHA256(key=WEBHOOK_SECRET, message=rawRequestBody)
Always use a constant-time comparison (e.g. crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python) to prevent timing-based attacks.

Signature Verification

The header format is sha256=<hex-digest>. Compute HMAC-SHA256(rawBody, webhookSecret), prepend sha256=, then compare with a constant-time equality function.

Node.js
import crypto from 'crypto';

export function verifyWebhookSignature(
  rawBody: Buffer,
  signature: string,
  secret: string
): boolean {
  const hmac = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  const expected = `sha256=${hmac}`;
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'utf8'),
    Buffer.from(signature, 'utf8')
  );
}

// Express.js usage
app.post('/webhooks/elevatedpos', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-elevatedpos-signature'] as string;
  if (!verifyWebhookSignature(req.body, sig, process.env.ELEVATEDPOS_WEBHOOK_SECRET!)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  const event = JSON.parse(req.body.toString());
  // Enqueue for async processing
  queue.push(event);
  res.sendStatus(200);
});
Python
import hmac, hashlib

def verify_webhook_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# Flask usage
@app.route('/webhooks/elevatedpos', methods=['POST'])
def elevatedpos_webhook():
    sig = request.headers.get('X-ElevatedPOS-Signature', '')
    if not verify_webhook_signature(request.get_data(), sig, ELEVATEDPOS_WEBHOOK_SECRET):
        return jsonify(error='Invalid signature'), 401
    event = request.get_json()
    task_queue.enqueue(process_event, event)
    return '', 200
PHP
<?php
function verifyWebhookSignature(string $rawBody, string $signature, string $secret): bool {
    $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $signature);
}

$rawBody = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_ELEVATEDPOS_SIGNATURE'] ?? '';

if (!verifyWebhookSignature($rawBody, $sig, getenv('ELEVATEDPOS_WEBHOOK_SECRET'))) {
    http_response_code(401);
    die(json_encode(['error' => 'Invalid signature']));
}

$event = json_decode($rawBody, true);
// Dispatch to background worker
dispatch_job('process_webhook', $event);
http_response_code(200);

Payload Envelope

All webhook deliveries share this outer envelope:

{
  "id": "evt_01HXXXXXXXXXXXXXXXX",
  "event": "order.created",
  "orgId": "org_uuid",
  "timestamp": "2024-09-15T10:30:00.000Z",
  "apiVersion": "2024-09-01",
  "data": {
    // Event-specific payload shown in the catalog below
  }
}

The id field is a unique, stable identifier for each delivery attempt. Use it for idempotency — if you receive the same id twice, the second delivery is a retry and can be safely ignored after the first was processed successfully.

Event Catalog

17 events across 5 services. Click any row to expand its sample payload.

EventServiceDescription
order.created
ordersA new order was placed at any location.
order.completed
ordersOrder was marked as fully complete.
order.refunded
ordersA full or partial refund was processed.
payment.captured
paymentsPayment was successfully captured.
payment.failed
paymentsPayment attempt was declined or errored.
customer.created
customersNew customer profile was created.
customer.updated
customersCustomer profile fields were updated.
inventory.low_stock
inventoryProduct stock fell below the reorder point.
inventory.stockout
inventoryProduct stock reached zero.
loyalty.points_earned
loyaltyPoints were added to a member account.
loyalty.tier_changed
loyaltyMember moved to a new loyalty tier.
layby.created
ordersA new lay-by was opened for a customer.
layby.payment_received
ordersA payment was received against a lay-by.
layby.completed
ordersLay-by was fully paid off and goods collected.
layby.cancelled
ordersLay-by was cancelled and deposit refunded.
gift_card.issued
ordersA new gift card was issued.
gift_card.redeemed
ordersA gift card was used as payment.

Retry Policy

Webhooks are retried when your endpoint returns a non-2xx status code or times out (30-second timeout per attempt). ElevatedPOS makes 3 total delivery attempts with the following schedule:

1
Immediate (first delivery)
2
1 minute after failure
3
5 minutes after failure

After all 3 attempts fail

The event is marked as failed.

Failed events are visible in your integration dashboard for 72 hours and can be manually replayed.

The final retry delay (30 minutes) is not used in this 3-attempt schedule. After attempt 3 fails the event enters the failed state immediately.

Endpoint Suspension

If an endpoint accumulates 10 consecutive delivery failures, ElevatedPOS will automatically suspend webhook delivery to that endpoint to protect platform throughput. You will receive an email notification when suspension occurs. Re-enable the endpoint from your integration dashboard after resolving the issue.

Testing Webhooks

Use the integrations service to fire a test payload to your endpoint:

curl -X POST https://api.elevatedpos.com.au/api/v1/integrations/{integrationId}/webhooks/test \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "order.created",
    "webhookId": "wh_01HXXXXXXXXXXXXXXXX"
  }'

Test deliveries use synthetic data. Signature verification works identically to live events.

Best Practices

Always verify signatures

Never process a webhook payload without first verifying the HMAC-SHA256 signature. This ensures the request originated from ElevatedPOS and the body was not tampered with in transit.

Return 200 quickly

Your endpoint should return a 2xx response within 30 seconds. Move heavy processing to a background queue. If ElevatedPOS receives a timeout, it will retry the event.

Process events asynchronously

Acknowledge receipt immediately with a 200 response, then process the event in a background worker or queue. This prevents timeouts and decouples delivery from processing.

Handle duplicate deliveries idempotently

The same event may be delivered more than once during retries. Use the event's id field to deduplicate and ensure your handler is idempotent.

Subscribe only to what you need

Select only the event types relevant to your integration. This reduces payload volume and avoids unnecessary processing.

Monitor delivery health

Review the webhook delivery log in your integration dashboard regularly. Set up alerts for elevated failure rates before they accumulate toward the 10-failure suspension threshold.

Rotate secrets periodically

Webhook secrets should be rotated every 90 days. ElevatedPOS supports overlapping secrets during rotation — your old secret remains valid for 24 hours after a new one is set.

Use HTTPS endpoints only

ElevatedPOS only delivers to HTTPS endpoints with a valid TLS certificate. HTTP endpoints are rejected to prevent data exposure.