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.
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. All orders are recorded in PKR.

Channels

channel is matched case-insensitively against the following values. Any other value is recorded as OTHER, and omitting the field leaves the channel unset.

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

Items

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

FieldTypeDescription
itemIdstringYour product identifier. Used to sync the line item against your Harlyy menu.
namestringDisplay name of the item.
quantityintegerQuantity ordered.
unitPricenumberPrice per unit.
totalPricenumberLine total for this item.
salesTaxPercentnumberSales tax rate applied to the item.
salesTaxAmountnumberSales tax amount for the item.
itemCommentstringFree-text note or modifier for the item.

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

Errors

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