Skip to main content

Custom POS Webhook

The custom POS webhook lets you push orders into Harlyy from any ordering system — an in-house platform, a POS we don't natively integrate with, or a middleware layer you control. You send each order to a single endpoint in a standardised JSON format, and Harlyy creates (or updates) the corresponding order, syncs the line items against your menu, and triggers any feedback flows you have configured.

Before you can send orders, you need to connect the Custom integration to obtain your webhook URL and secret. See Setting up a POS Webhook for the dashboard walkthrough.

Endpoint

Custom Order Webhook
POST https://api.harlyy.com/webhooks/custom/{business}
Path parameterDescription
businessThe ID of the business the order belongs to (bus_…).

Authentication

Every request must include the secret you were given when the Custom integration was connected, sent as a bearer token:

Authorization: Bearer <your-webhook-secret>
Content-Type: application/json
warning

The secret is shown once, at the moment the integration is connected. Store it securely and never expose it in client-side code. If a secret is lost or leaked, reconnect the Custom integration from the dashboard to rotate it.

Request Body

FieldTypeRequiredDescription
externalOrderIdstringYesYour system's unique order identifier. Used for deduplication — see Idempotency.
totalstringYesThe grand total of the order.
currencystringYesISO 4217 currency code the order amounts are expressed in (e.g. PKR, AED).
metadataobjectYesArbitrary key-value pairs (string to string) for integration-specific identification. At least one entry is required; values must be strings.
externalBrandIdstringNoYour brand identifier, stored on the order for reporting.
externalLocationIdstringNoYour location/branch identifier, stored on the order for reporting.
customerNamestringNoCustomer's name.
customerEmailstringNoCustomer's email. Sanitised on ingestion.
customerPhoneNumberstringNoCustomer's phone number. Sanitised on ingestion.
subtotalstringNoOrder subtotal before delivery charges.
deliveryChargestringNoDelivery charge applied to the order.
channelstringNoOrdering channel — see Channels.
paymentbooleanNotrue marks the order as accepted; false or omitted leaves it pending.
itemsarrayNoOrder line items — see Items.
info

Monetary fields (total, subtotal, deliveryCharge, and item prices) are accepted as strings and parsed into numbers; any non-numeric characters (currency symbols, separators) are stripped. The required currency field sets the currency those amounts are recorded in. Send a valid ISO 4217 code (e.g. PKR, AED); a missing or unrecognised value is rejected.

Channels

channel identifies the source an order came from. Harlyy supports a small, cohesive set of sources, the values below, matched case-insensitively, with spaces, hyphens and underscores treated as equivalent. Any other value is recorded as OTHER, and omitting the field leaves the channel unset.

WEBSITE · ANDROID · IOS · CALL-IN · QR · FOODPANDA · GOLOOTLO

info

The channel is the source only, not the fulfilment type. For a more specific case like foodpanda-delivery or golootlo-takeaway, still set channel to the base source (FOODPANDA, GOLOOTLO) and record the specifics in the integrator-controlled metadata field, for example "order-source": "foodpanda-takeaway". metadata is open for you to add anything Harlyy doesn't model natively but that is useful for visibility and tracking on the portal; its keys and values are stored verbatim on the order.

Items

Each entry in items describes a single line item. Items are only stored the first time an order is ingested — see Idempotency.

FieldTypeRequiredDescription
itemIdstringNoYour product identifier. Stored on the order line and kept as a reference on the synced menu item.
namestringNoDisplay name of the item. This is what the line is matched against your Harlyy menu on.
quantityintegerNoQuantity ordered.
unitPricenumberNoPrice per unit.
totalPricenumberNoLine total for this item.
salesTaxPercentnumberNoSales tax rate applied to the item.
salesTaxAmountnumberNoSales tax amount for the item.
itemCommentstringNoFree-text note or modifier for the item.
info

Line items are matched to your Harlyy menu by name, not by itemId, so a product whose id changes between orders will not create duplicate menu entries. Send name where you can. A line with neither name nor itemId carries nothing to identify it and is ignored; a line with only one of the two is stored and matched on whichever you sent.

Idempotency

Orders are deduplicated on externalOrderId and externalLocationId within a business:

  • The first request for a given externalOrderId at a given externalLocationId creates a new order.
  • Supplying only an externalOrderId is still supported, but is not recommended as we cannot guarantee consistency.
  • Any subsequent request with the same externalOrderId and externalLocationId updates the existing order's status (for example, moving it from pending to accepted as payment changes), rather than creating a duplicate.
  • The same externalOrderId sent with a different externalLocationId is a different order and creates a new one.
  • Line items are only written when the order is first created. Once an order has items, later requests will not overwrite them.

This means you can safely send the same order multiple times — for instance, once when it is placed and again when payment is confirmed.

caution

Because the location is part of the key, an externalOrderId that repeats across your locations is stored correctly, but the survey link needs the location too or Harlyy cannot tell which of those orders the customer is giving feedback on. See Linking the Survey.

Example Request

cURL Example
curl -X POST https://api.harlyy.com/webhooks/custom/bus_123 \
-H "Authorization: Bearer <your-webhook-secret>" \
-H "Content-Type: application/json" \
-d '{
"externalOrderId": "ORD-90871",
"total": "23.50",
"currency": "PKR",
"subtotal": "21.00",
"deliveryCharge": "2.50",
"channel": "WEBSITE",
"payment": true,
"customerName": "Oliver Bennett",
"customerEmail": "oliver.bennett@example.com",
"customerPhoneNumber": "+447700900123",
"externalBrandId": "brand-burgers",
"externalLocationId": "branch-shoreditch",
"metadata": {
"source": "in-house-app",
"posOrderRef": "90871"
},
"items": [
{
"itemId": "SKU-CHZ-001",
"name": "Classic Cheeseburger",
"quantity": 2,
"unitPrice": 8.50,
"totalPrice": 17.00,
"salesTaxPercent": 20,
"salesTaxAmount": 3.40,
"itemComment": "No pickles"
},
{
"itemId": "SKU-FRY-002",
"name": "Loaded Fries",
"quantity": 1,
"unitPrice": 4.00,
"totalPrice": 4.00,
"salesTaxPercent": 20,
"salesTaxAmount": 0.80
}
]
}'

Response

On success the endpoint returns 200 OK with the created or updated order object and a link to retrieve it:

200 OK
{
"data": {
"id": "ord_8f2c…",
"object": "order",
"business": "bus_123",
"source": "custom",
"channel": "WEBSITE",
"status": "ACCEPTED",
"currency": "PKR",
"subtotal": 21.0,
"deliveryCharge": 2.5,
"total": 23.5,
"customerName": "Oliver Bennett",
"metadata": {
"source": "in-house-app",
"posOrderRef": "90871"
}
},
"url": "/v2/businesses/bus_123/orders/ord_8f2c…"
}

A common use case is embedding survey links in receipts so customers can provide feedback on their order.

Important: The webhook endpoint does not provide a survey link. Instead, your integration must construct the survey link using the externalOrderId you send in the webhook.

info

The survey link format and every query parameter it supports, including custom.customOrderId, custom.customLocationId and location:metadata, are documented in the Survey Links guide.

Example Flow

  1. Customer completes an order in your POS system with ID ORD-90871 at branch branch-001
  2. Your backend sends the order to Harlyy via the webhook with externalOrderId: "ORD-90871"
  3. Your system generates the survey link:
    https://www.harlyy.com/survey/bus_123/sur_456?custom.customOrderId=ORD-90871&custom.customLocationId=branch-001
  4. You embed this link as a QR code in the receipt
  5. Customer scans the QR code (now or later) and provides feedback
  6. Harlyy automatically looks up the order using the externalOrderId and externalLocationId from the link, then links the feedback submission to it

Which location parameter to send

custom.customLocationId scopes the order lookup. location:metadata resolves which Harlyy location the submission belongs to. Which you send depends on the survey.

Default. The survey has no location step, so only the order lookup needs the location:

https://www.harlyy.com/survey/bus_123/sur_456?custom.customOrderId=ORD-90871&custom.customLocationId=branch-001

The survey has a location step. Use location:metadata so the customer's location is resolved and the step is skipped. The order lookup falls back to that same value, so there is no need to send the location twice:

https://www.harlyy.com/survey/bus_123/sur_456?custom.customOrderId=ORD-90871&location:metadata=branchId.branch-001

The fallback only works when your location metadata is identical to the externalLocationId you send on the webhook. If the two differ, send both parameters.

The order lookup happens entirely on Harlyy's servers, so customers don't need to wait at the POS terminal for the receipt — they can scan the QR code later at their convenience.

Errors

StatusReason
400The payload failed validation — typically a missing externalOrderId, total, currency, or empty metadata.
401The bearer token is missing or invalid, or no Custom integration has been connected for this business.