Nexus Gateway
EnglishRomână
OpenAPI

Register one callback for your tenant. The gateway sends signed order updates, restaurant and product availability, menu publications and POS connection changes through this one callback. New connections enabled later in Retail are discovered automatically.

The gateway URL is where your backend sends API requests. The callback URL belongs to your backend and is where Nexus sends notifications. Replace https://shop.example.com/nexus/events below with your own HTTPS receiver. Each tenant has its own callback configuration; all of that tenant’s POS connections share it. If several websites use the same tenant key, they share this subscription: another PUT /v1/webhook updates it rather than adding a second receiver. Route events to those websites in your backend if needed.

Register your callback

Use your tenant’s website key from your backend. Your receiver must use public HTTPS on port 443, with a valid certificate. Private addresses, redirects, URL credentials, query strings and fragments are not supported.

curl --fail-with-body -X PUT "https://website.api.pionier.ro/v1/webhook" \
  -H "Authorization: Bearer $NEXUS_WEBSITE_KEY" \
  -H "Content-Type: application/json" \
  --data '{"url":"https://shop.example.com/nexus/events","enabled":true}'

The response contains url, enabled and a signing_secret beginning with whsec_. Save the secret securely on your server. It is returned only on creation, URL change or explicit rotation; identical PUTs and GETs return null. If you lose the first response, rotate the secret.

503 webhooks_not_configured means Nexus must enable webhook signing on this gateway. Your existing menu, order and polling endpoints remain available.

Understand the event

webhook-event.json
{
  "id": "evt_0123456789abcdef0123456789abcdef0123456789abcdef",
  "type": "order.status_changed",
  "sequence": 1,
  "observed_at": "2026-10-19T10:00:00Z",
  "connection_id": "website-example-pos",
  "data": {
    "external_order_id": "shop-1001",
    "status": "accepted",
    "total": 3700,
    "currency": "RON"
  }
}

order.status_changed reports a confirmed status observed by the gateway. It checks watched orders approximately every 10 seconds after a successful read, with backoff on failure. Queue load and outages can delay delivery; this is not a latency guarantee. Fast intermediate states may be missed. observed_at is the observation time, not the time of the restaurant’s action.

Only new orders confirmed while your webhook is enabled are watched. No historical backfill or initial receipt event is sent. Watching stops at completed, rejected or cancelled. There are no payment-collection events. Explicit delivery and refund reports also emit the events described below, including after the order reaches a terminal state. The payload contains no customer contact details.

Use connection_id and data.external_order_id together to identify the order. sequence increases per order: do not overwrite a newer status with an older sequence. Use the status endpoint after downtime or when you need the latest confirmed state.

order.updated carries an updated ready estimate or other order metadata without a status transition. Both event types use the same per-order sequence and contain scheduled_for, estimated_ready_at and reason when available. Process both types and reconcile using GET status.

Restaurant availability

connection.availability_changed reports checkout availability for a POS. Handle it separately from order events: it has no external_order_id. Its data contains accepting_orders, reason (accepting, paused, closed or menu_not_published) and a nullable pause_until timestamp. Pause, manual resume, automatic pause expiry, and opening/closing hours can change this state. An expired pause only enables checkout when opening hours allow it.

Use the same callback and signing secret. An initial snapshot is sent when watching starts, and again after re-enabling the webhook or rotating its secret. Connections become watched automatically through discovery, or when the gateway first sees them through publication or an authenticated request; no customer order is required. Observation runs approximately every 10 seconds per watched connection, with backoff on registry failure. Load and outages can delay it, and rapid intermediate changes can be coalesced. A registry outage does not invent a closed/open transition.

Track the highest sequence separately per connection_id for availability; do not compare it with an order sequence. Apply accepting_orders to checkout for immediate orders. For a scheduled time, read the menu with fulfillment_at to evaluate that slot. Reconcile with GET menu after downtime; order submission always revalidates current availability. These events share signed delivery, retry, deduplication and replay rules with order events. Delivery history returns external_order_id: null for them. Pending availability events are ordered within their own connection stream and do not block order events.

Menus, products and options

menu.published means a new menu revision was published: refresh GET /v1/connections/{connection_id}/menu for prices, products, options, bundles and translations. Identical publication retries do not repeat the event.

menu.availability_changed means the menu availability inputs changed: stock, product/option restrictions or product schedules, including a scheduled opening or closing. It also sends an initial snapshot notification. Refresh the same menu endpoint; use fulfillment_at for a future slot. This is an invalidation, not a list of changed items: it can include a change that leaves an item unavailable for another reason. Publication may produce both menu events; coalesce refreshes.

Both types contain data: {"revision":"menu-2026-12-25"}. Availability can change without a new revision. Keep separate sequences for each (connection_id, type). A menu may already be newer than the notification when fetched.

Operating settings

connection.settings_changed tells you to refresh GET /v1/connections/{connection_id}/menu when weekly opening hours, holiday exceptions, the restaurant time zone or preparation time change. It fires even if accepting_orders remains unchanged. Read opening_hours and scheduling from the refreshed menu to update future checkout slots and preparation estimates.

The payload is data: {"settings_version":"OPAQUE_64_CHARACTER_HEX_VALUE"}. This value identifies the observed settings; use the event sequence, not the hash, for ordering. The event is an invalidation, not a settings snapshot: fetch current values rather than treating the hash as a menu revision. It has an independent sequence per connection/type, uses the existing signed callback and retries, and returns external_order_id: null in delivery history.

Observation starts once the connection has a published menu. An initial notification is sent and repeats after callback re-enablement, URL change or signing-secret rotation. The existing availability observer detects changes approximately every 10 seconds, subject to load and outage delays; rapid edits may be coalesced. No menu republication is needed for live operating-policy changes. Reconcile through GET menu after missed notifications.

POS connections

connection.changed reports the initial state and subsequent enable/disable or location/POS name changes. Its data contains enabled, location_id, location_name, pos_id and pos_name. Refresh GET /v1/connections to update your locations. If disabled, remove that connection from checkout; menu and order requests for it are no longer authorized.

New POS connections are discovered automatically, without a website request or a menu publication. Discovery reads pages of at most 100 connections, restarting approximately 10 seconds after a completed scan. Availability observations also run approximately every 10 seconds. Load, retries and outages can delay these notifications; fast intermediate states may be coalesced. A registry outage never invents a disabled state.

The final POS-disable notification still goes to the tenant callback. Disabling the tenant or its webhook stops all subsequent delivery attempts. Connection and menu events use the same signature, retry and replay contract as orders, with external_order_id: null in delivery history. Each (connection_id, type) has an independent ordered sequence; initial snapshots are sent again after webhook re-enablement, URL change or secret rotation. After downtime, reconcile both the connection list and each active menu.

Verify the signature

Read X-Nexus-Signature: t=TIMESTAMP,v1=SIGNATURE. Compute HMAC-SHA256 over the timestamp, a literal period and the exact raw request body bytes. Use the entire whsec_... string as the UTF-8 key. Compare in constant time and reject timestamps more than five minutes away from your server clock. Keep clocks synchronized. Each delivery attempt has a fresh signature.

verify_webhook.py
"""Verify before parsing JSON. Supply the unmodified HTTP request body and header."""
import hashlib
import hmac
import time


def verify_webhook(body: bytes, header: str, signing_secret: str, now=None):
    if len(header) > 256 or len(body) > 16384:
        raise ValueError("Invalid webhook")
    try:
        parts = header.split(",")
        if len(parts) != 2 or not parts[0].startswith("t=") or not parts[1].startswith("v1="):
            raise ValueError()
        stamp, actual = parts[0][2:], parts[1][3:]
        if not stamp.isascii() or not stamp.isdigit() or len(stamp) > 12:
            raise ValueError()
        current = time.time() if now is None else now
        if abs(current - int(stamp)) > 300:
            raise ValueError()
        expected = hmac.new(signing_secret.encode("utf-8"), stamp.encode("ascii") + b"." + body, hashlib.sha256).hexdigest()
        if not hmac.compare_digest(expected, actual):
            raise ValueError()
    except (ValueError, TypeError):
        raise ValueError("Invalid webhook") from None
    return True

Verify before parsing JSON. After verification, validate the event type and required fields. X-Nexus-Event-Id repeats the signed body’s id; trust the signed body. The callback never includes your website API key.

Acknowledge and deduplicate

  1. Verify the signature and persist the event in your own queue, with a unique constraint on id.
  2. Return any 2xx promptly, including for an already stored event. Process it asynchronously.
  3. Apply status updates using the order’s highest processed sequence. Retain deduplication records for as long as you allow manual replays.

Delivery is at least once, so duplicates are possible even after a successful response. Timeouts, connection failures and all non-2xx responses cause retries with exponential backoff and jitter. Numeric Retry-After seconds are honored up to one hour. Automatic attempts stop after 30 attempts or a failure at least 24 hours after queueing, whichever comes first. A gateway outage can extend the wall-clock time before that final attempt.

Later updates for the same order wait while an earlier event is pending. Once it succeeds or exhausts retries, later events can proceed. A manual replay may therefore arrive after a newer status.

Inspect and retry failures

curl --fail-with-body "https://website.api.pionier.ro/v1/webhook/deliveries" \
  -H "Authorization: Bearer $NEXUS_WEBSITE_KEY"

curl --fail-with-body -X POST "https://website.api.pionier.ro/v1/webhook/deliveries/$NEXUS_EVENT_ID/retry" \
  -H "Authorization: Bearer $NEXUS_WEBSITE_KEY"

The list returns the latest 100 events, with states pending, delivered, failed or cancelled, attempt counts, last HTTP status and safe failure codes. Receiver response bodies are never exposed. Events remain in the gateway ledger; the 100-entry list is a display limit, not a retention period.

Retry applies only to failed events for your current enabled callback. It returns 204 and preserves the original event ID, body and sequence, resetting the attempt window. Other IDs return 404 failed_delivery_not_found. First fix the receiver and reconcile current order status.

Disable or rotate

PUT the same URL with enabled: false to pause watching and cancel pending deliveries. Re-enabling resumes previously watched non-terminal orders; orders confirmed while disabled are not backfilled. Changing the URL or POSTing to /v1/webhook/rotate-secret generates a new signing secret and cancels queued events for the old configuration. An HTTP request already in flight may still arrive.

Disabling a POS blocks order and availability callbacks; its final connection.changed event can still be delivered. Disabling the tenant blocks all callbacks. Rotating the website API key does not rotate the webhook secret. See the API reference for all request and response fields.

Delivery and refund reports

order.delivery_updated contains external_order_id, request_id, the submitted delivery status and optional estimates. refund.reported contains the order ID, request ID, outcome and amount. These use their own payloads, not OrderReceipt. Courier contacts and processor references are excluded. Process by type; fetch current order status when reconciling. A report may arrive after later progress.

Explicit report notifications work for completed orders too, without restarting status polling. They are queued before a successful report response and deduplicated by request ID within the current webhook configuration. After a timeout or 503, retry the report unchanged: a retry also repairs an interrupted notification enqueue. Notifications require an enabled webhook at report confirmation; they do not backfill reports made while disabled. Status observation may separately emit order.updated.

Nexus GatewayWebsite Ordering API · v1.0.0

Type to search the integration guides.