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 within a business:

  • The first request for a given externalOrderId creates a new order.
  • Any subsequent request with the same externalOrderId updates the existing order's status (for example, moving it from pending to accepted as payment changes), rather than creating a duplicate.
  • 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.

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…"
}

Linking to Survey Forms

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.

Use this URL pattern, replacing the placeholders with actual values:

https://www.harlyy.com/survey/<business_id>/<survey_id>?location:metadata=branchId.<branch_id>&custom.customOrderId=<external_order_id>
PlaceholderSourceExample
<business_id>Harlyy business IDbus_69a2c7103b4e4257e29066bf
<survey_id>Harlyy survey IDsur_6a3a98463e8de03e758b2dce
<branch_id>Your branch identifier (from location metadata)branch-001
<external_order_id>The externalOrderId you sent in the webhookORD-90871

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?location:metadata=branchId.branch-001&custom.customOrderId=ORD-90871
  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 links the feedback submission to it

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.