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.

Availability

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/events returns400 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 */ }
}
Same envelope, dedup on eventId

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: Points or Stamps. Points/stamp count is computed server-side from the merchant's active rules — never passed in.
  • stampCardId: required only for earnType = "Stamps"; identifies which stamp card the earn applies to. Null for Points.
  • channel: Pos or Kiosk.
  • One benefit per invoice: an externalInvoiceId that was already credited is rejected with PARTNER_INVOICE_ALREADY_EARNED, even under a new eventId.
  • A PerVisit stamp card still inside its cooldown window returns 200 with 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: true only when a manager forced the refund past a validation block. Quroosh may apply UsedRewardsRefundPolicy differently 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, or NegativeBalanceByPolicy.
  • 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) or Percentage (a percent of the invoice).
  • isLoyaltyProgramCoupon: true for a coupon Quroosh created for a loyalty redemption; false for the merchant's own coupons. Defaults to false.

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) or OnVisit (fixed earn per visit).
  • For OnPurchase: set pointCostAmount + pointCostPoints; leave pointsPerVisit null.
  • For OnVisit: set pointsPerVisit; leave the point-cost fields null.
  • FreeEntry is 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) or PerAmount (one stamp per amountPerStamp riyals).
  • 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) or Cancelled (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, or Disconnected.
  • Stop sending events after Disabled or Disconnected.

All 16 payload shapes are also visible in the API Referenceunder the request-body schema — use whichever view is more comfortable.