DashboardDeveloper Portal

Webhooks

Receive event notifications from the Fly platform. When an event fires, Fly sends an HTTP POST request to your configured endpoint with a signed JSON payload.

Event delivery is rolling out. Today the only event that is actually delivered is test.ping (sent when you trigger a test from the API or dashboard). The business events in the catalog below — orders, payments, customers, inventory, loyalty — are the planned contract and are not yet emitted. Endpoint registration, HMAC signing, retries, and delivery logs all work today, so you can build and verify your handler now and it will start receiving events as each producer goes live.

All payloads on this page are example data for documentation purposes — not live values. To call the API against your own seeded test org, use the read-write sandbox. Each event below has an “Open in Sandbox” button that pre-fills a matching endpoint.

Overview

Subscribe

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

Receive

Fly makes an HTTP POST to your endpoint with a JSON payload. Today this means test.ping deliveries — business events go live per the catalog below.

Verify & Process

Validate the HMAC-SHA256 signature before processing. Return 2xx within 10 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 — two fields, nothing else:

{
  "event": "order.created",
  "data": {
    // Event-specific payload shown in the catalog below
  }
}

Delivery metadata is carried in the request headers:

X-ElevatedPOS-Event:     order.created            # event type, same as body "event"
X-ElevatedPOS-Signature: sha256=<hex-digest>      # HMAC-SHA256 of the raw body
X-ElevatedPOS-Delivery:  9f2c7d4e-…               # unique ID per delivery (stable across retries)

Use the X-ElevatedPOS-Delivery header for idempotency — retries of the same delivery carry the same ID, so if you see a value twice the second request can be safely ignored after the first was processed successfully. The JSON body itself has no id field.

Event Catalog

18 events. Click any row to expand its sample payload. The Status column shows what is delivered today (test.ping) versus what is planned — planned events describe the payload contract but are not yet emitted.

EventServiceStatusDescription
test.ping
webhooksLiveManual test delivery fired from POST /api/v1/webhooks/:id/test (or the dashboard).
order.created
ordersPlannedA new order was placed at any location.
order.completed
ordersPlannedOrder was marked as fully complete.
order.refunded
ordersPlannedA full or partial refund was processed.
payment.captured
paymentsPlannedPayment was successfully captured.
payment.failed
paymentsPlannedPayment attempt was declined or errored.
customer.created
customersPlannedNew customer profile was created.
customer.updated
customersPlannedCustomer profile fields were updated.
inventory.low_stock
inventoryPlannedProduct stock fell below the reorder point.
inventory.stockout
inventoryPlannedProduct stock reached zero.
loyalty.points_earned
loyaltyPlannedPoints were added to a member account.
loyalty.tier_changed
loyaltyPlannedMember moved to a new loyalty tier.
layby.created
ordersPlannedA new lay-by was opened for a customer.
layby.payment_received
ordersPlannedA payment was received against a lay-by.
layby.completed
ordersPlannedLay-by was fully paid off and goods collected.
layby.cancelled
ordersPlannedLay-by was cancelled and deposit refunded.
gift_card.issued
ordersPlannedA new gift card was issued.
gift_card.redeemed
ordersPlannedA gift card was used as payment.

Retry Policy

Webhooks are retried when your endpoint returns a non-2xx status code or times out (10-second timeout per attempt). Fly 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 delivery is marked as failed.

The last 50 delivery attempts per endpoint (including failures, with response code and body) are available via GET /api/v1/webhooks/:id/deliveries and in your dashboard, and a failed delivery can be replayed with POST /api/v1/webhooks/:id/logs/:logId/retry.

Endpoint Status

Failing endpoints are not automatically suspended today — deliveries simply keep following the 3-attempt schedule per event, and there are no email notifications. You can pause an endpoint yourself at any time by setting its status to inactive via PATCH /api/v1/webhooks/:id or from your dashboard, and monitor failures via the deliveries log. Automatic suspension of persistently failing endpoints is planned.

Testing Webhooks

Fire a signed test.ping delivery at your endpoint — this is the same pipeline live events will use:

curl -X POST https://api.useflyconnect.com/api/v1/webhooks/{webhookId}/test \
  -H "Authorization: Bearer <token>"

# Response includes the delivery result:
# { "data": { "deliveryId": "…", "success": true, "responseCode": 200, "responseBody": "…" } }

The test body is { "event": "test.ping", "data": { "timestamp": "…" } }. 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 Fly and the body was not tampered with in transit.

Return 200 quickly

Your endpoint should return a 2xx response within 10 seconds — that is the per-attempt delivery timeout. Move heavy processing to a background queue. If Fly 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 X-ElevatedPOS-Delivery header 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 dashboard (or GET /api/v1/webhooks/:id/deliveries) regularly and set up alerts for elevated failure rates — failing endpoints are not auto-suspended, so silent failures are yours to catch.

Rotate secrets periodically

Rotate your webhook secret regularly via POST /api/v1/webhooks/:id/rotate-secret. Rotation takes effect immediately — the old secret stops being used as soon as the new one is issued, so update your handler config in the same change window.

Use HTTPS endpoints

Always register HTTPS endpoints in production. Plain HTTP is accepted to support tunnelled dev endpoints (e.g. ngrok), but it exposes payloads in transit — never use it for live data. URLs pointing at IP literals or internal hostnames are rejected.