Event types
Every event you post to POST /api/partner/v1/events uses the same envelope; only the eventType string and thepayload object change. This page lists all 16 event types with an example body — copy, edit, sign, send.
Each event is tagged with its current state:
- Availableprocessed today — post it and it takes effect.
- Plannedaccepted at the wire but the handler is not wired yet, so it returns a retryable
503. Build against it, but expect declines until it ships. - OutboundQuroosh sends this one to you (see Webhooks). Posting it to
/api/partner/v1/eventsreturns400 WRONG_EVENT_DIRECTION. The payload shape is documented here so you know what you will receive.
The envelope
Wraps every event. The tenant is derived from your API key, never from the body.
{
"eventId": "<Guid v7 — unique per event, used for dedup>",
"occurredAt": "<ISO-8601 UTC — when the event happened at the source>",
"entityType": "Invoice | Redemption | LoyaltyRule | Branch | Customer | Product | Coupon | Integration",
"entityId": "<string — id of the affected entity in your system>",
"sourceSystem": "Kosoor | Quroosh",
"schemaVersion": 1,
"correlationId": "<Guid — optional, ties related events across systems>",
"eventType": "<one of the 16 values below>",
"payload": { /* eventType-specific object, see each section below */ }
}Retries must carry the same eventId. Quroosh caches the response for 24 hours and replays it — the business handler never runs twice.
Transaction events
InvoiceEarned Available
Customer earned points or a stamp on a purchase.
{
"eventType": "InvoiceEarned",
"payload": {
"externalInvoiceId": "0195d1c7-8000-7000-8000-000000000123",
"externalCustomerId": "cust_012345",
"externalBranchId": "0195d0a0-0000-7000-8000-000000000001",
"subtotalAfterDiscountBeforeVat": 104.35,
"earnType": "Points",
"stampCardId": null,
"channel": "Pos"
}
}earnType:PointsorStamps. Points/stamp count is computed server-side from the merchant's active rules — never passed in.stampCardId: required only forearnType = "Stamps"; identifies which stamp card the earn applies to. Null forPoints.channel:PosorKiosk.- One benefit per invoice: an
externalInvoiceIdthat was already credited is rejected withPARTNER_INVOICE_ALREADY_EARNED, even under a neweventId. - A
PerVisitstamp card still inside its cooldown window returns200with zero stamps — acknowledged, not an error.
InvoiceRefunded Planned
An invoice was refunded. Quroosh reverses the original earn.
{
"eventType": "InvoiceRefunded",
"payload": {
"externalInvoiceId": "0195d1c7-8000-7000-8000-000000000124",
"externalOriginalInvoiceId": "0195d1c7-8000-7000-8000-000000000123",
"refundedAmount": 104.35,
"isOverride": false
}
}externalInvoiceId: the refund's own invoice id.externalOriginalInvoiceId: the invoice being refunded.isOverride:trueonly when a manager forced the refund past a validation block. Quroosh may applyUsedRewardsRefundPolicydifferently in that case.
RefundReversalCompleted Outbound
Quroosh's follow-up to InvoiceRefunded — reports what actually happened after policy was applied. You receive this; you never post it.
{
"eventType": "RefundReversalCompleted",
"payload": {
"externalInvoiceId": "0195d1c7-8000-7000-8000-000000000124",
"status": "Reversed",
"pointsReversed": 120,
"stampsReversed": null,
"newBalance": 380
}
}status:Reversed(clean),OverrideApplied, orNegativeBalanceByPolicy.- Delivered as an outbound webhook once refund reversal ships; not emitted yet.
Coupon usage
CouponUsed Available
A coupon was redeemed on an invoice.
{
"eventType": "CouponUsed",
"payload": {
"externalCouponId": "0195d1c7-9000-7000-8000-000000000010",
"externalCustomerId": "cust_012345",
"externalInvoiceId": "0195d1c7-8000-7000-8000-000000000123"
}
}CouponUsageCancelled Available
Reverses a previous CouponUsed.
{
"eventType": "CouponUsageCancelled",
"payload": {
"externalCouponId": "0195d1c7-9000-7000-8000-000000000010",
"externalInvoiceId": "0195d1c7-8000-7000-8000-000000000123"
}
}Sync events (single entity)
BranchSynced Available
{
"eventType": "BranchSynced",
"payload": {
"externalBranchId": "0195d0a0-0000-7000-8000-000000000001",
"name": "Riyadh — Al Olaya",
"isActive": true
}
}CustomerSynced Available
{
"eventType": "CustomerSynced",
"payload": {
"externalCustomerId": "cust_012345",
"phone": "+9665XXXXXXXX",
"firstName": "Ahmed",
"lastName": "Al-Otaibi"
}
}firstName and lastName are optional.
ProductSynced Available
{
"eventType": "ProductSynced",
"payload": {
"externalProductId": "sku_00042",
"name": "Cortado — 12oz",
"price": 18.00,
"isActive": true
}
}Send isActive: false when the product is deleted or disabled — any Quroosh reward mapped to it becomes unmappable and redemptions against it are declined. Omitting isActivedefaults to true.
CouponSynced Available
{
"eventType": "CouponSynced",
"payload": {
"externalCouponId": "0195d1c7-9000-7000-8000-000000000010",
"name": "Ramadan 20%",
"discountType": "Percentage",
"discountValue": 20.00,
"isLoyaltyProgramCoupon": false
}
}discountType:FixedAmount(a riyal amount) orPercentage(a percent of the invoice).isLoyaltyProgramCoupon:truefor a coupon Quroosh created for a loyalty redemption;falsefor the merchant's own coupons. Defaults tofalse.
Sync events (batch)
For an initial bulk import (backfill) or a periodic reconciliation — up to 500 items per event. Payload wraps the same per-entity shape initems.
BranchesSyncedBatch Planned
{
"eventType": "BranchesSyncedBatch",
"payload": {
"items": [
{ "externalBranchId": "0195d0a0-...-001", "name": "Riyadh — Al Olaya", "isActive": true },
{ "externalBranchId": "0195d0a0-...-002", "name": "Jeddah — Corniche", "isActive": true }
]
}
}CustomersSyncedBatch Planned
{
"eventType": "CustomersSyncedBatch",
"payload": {
"items": [
{ "externalCustomerId": "cust_012345", "phone": "+9665XXXXXXX1" },
{ "externalCustomerId": "cust_012346", "phone": "+9665XXXXXXX2", "firstName": "Sara" }
]
}
}ProductsSyncedBatch Planned
{
"eventType": "ProductsSyncedBatch",
"payload": {
"items": [
{ "externalProductId": "sku_00042", "name": "Cortado — 12oz", "price": 18.00, "isActive": true },
{ "externalProductId": "sku_00043", "name": "Latte — 16oz", "price": 22.00, "isActive": true }
]
}
}CouponsSyncedBatch Planned
{
"eventType": "CouponsSyncedBatch",
"payload": {
"items": [
{ "externalCouponId": "0195...010", "name": "Ramadan 20%", "discountType": "Percentage", "discountValue": 20.00 },
{ "externalCouponId": "0195...011", "name": "First-Time 5", "discountType": "FixedAmount", "discountValue": 5.00 }
]
}
}Rule changes
LoyaltyRuleChanged Outbound
The merchant changed how customers earn. Quroosh sends this to your webhook so you can update your cached rules; you never post it. The nestedpayload switches on ruleType. Apply a payload only when its ruleVersion is greater than the version you last stored — events have no global ordering.
Earn rate (ruleType = "EarnRate")
{
"eventType": "LoyaltyRuleChanged",
"payload": {
"externalStoreId": "0195d0a0-0000-7000-8000-000000000000",
"ruleType": "EarnRate",
"ruleVersion": 3,
"effectiveFrom": "2026-07-15T00:00:00Z",
"payload": {
"methodType": "OnPurchase",
"pointCostAmount": 1.00,
"pointCostPoints": 1,
"pointsPerVisit": null,
"minimumPurchase": 20.00,
"isActive": true
}
}
}methodType:OnPurchase(earn per riyal spent) orOnVisit(fixed earn per visit).- For
OnPurchase: setpointCostAmount+pointCostPoints; leavepointsPerVisitnull. - For
OnVisit: setpointsPerVisit; leave the point-cost fields null. FreeEntryis not a valid partner earn method — Quroosh rejects it.
Stamp rule (ruleType = "StampRule")
{
"eventType": "LoyaltyRuleChanged",
"payload": {
"externalStoreId": "0195d0a0-0000-7000-8000-000000000000",
"ruleType": "StampRule",
"ruleVersion": 2,
"effectiveFrom": "2026-07-15T00:00:00Z",
"payload": {
"stampCardId": "0195d1c7-a000-7000-8000-000000000100",
"name": "بطاقة الأختام — قهوة",
"mode": "PerAmount",
"minInvoiceAmount": 15.00,
"amountPerStamp": 15.00,
"cooldownDays": null,
"isActive": true
}
}
}mode:PerVisit(one stamp per qualifying visit) orPerAmount(one stamp peramountPerStampriyals).cooldownDays: optional wait between stamps from the same customer.
Lifecycle
RedemptionExpiredOrCancelled Outbound
A pending redemption dropped without fulfilment. You receive this from Quroosh; you never post it.
{
"eventType": "RedemptionExpiredOrCancelled",
"payload": {
"redemptionId": "0195d1c7-b000-7000-8000-000000000500",
"externalInvoiceId": null,
"reason": "Expired"
}
}reason:Expired(time ran out) orCancelled(staff cancelled).externalInvoiceId: set when the redemption was already attached to an invoice; null otherwise.
IntegrationStatusChanged Outbound
Merchant enabled, disabled, or disconnected the integration. Quroosh sends this to your webhook.
{
"eventType": "IntegrationStatusChanged",
"payload": {
"status": "Enabled"
}
}status:Enabled,Disabled, orDisconnected.- Stop sending events after
DisabledorDisconnected.
All 16 payload shapes are also visible in the API Referenceunder the request-body schema — use whichever view is more comfortable.