Quickstart

This guide walks the whole integration path: get credentials from the merchant's Quroosh dashboard, prove the connection works, sync and map branches, sync a customer, and send your first sale event. The coding part takes about 15 minutes; two short steps happen in the Quroosh dashboard on the merchant's side.

How the integration fits together

Two systems talk in both directions: your platform calls the Quroosh Partner API, and Quroosh pushes webhooks to your platform's receiver endpoint. Some steps are yours; some belong to the merchant in their Quroosh dashboard:

#WhereWhat happens
1Quroosh dashboard (merchant)Merchant activates the integration and gets an API key + signing secret — shown once — and hands them to you.
2Your platformStore both values, then call GET /connection to prove the connection works.
3Your platformSend a BranchSynced event for each of your branches.
4Quroosh dashboard (merchant)Merchant pairs each of your branches with a Quroosh branch. Sales are rejected until this is done.
5Your platformSend CustomerSynced for your customers, then InvoiceEarned on every sale. Points land automatically.
6Quroosh → your platformQuroosh pushes webhooks (loyalty rule changes, integration status) to your receiver endpoint, signed with the same secret. The X-TenantId header tells you which of your tenants the delivery belongs to.

1. Get your credentials (Quroosh dashboard)

Credentials are issued by the merchant, not by Quroosh support. In the Quroosh merchant dashboard, the merchant opensPartner Integrations (sidebar → Partner Integrations), selects your platform and which of your deployments to target (testing or production — your webhook receiver URL for each is registered with Quroosh when the partnership is set up), enters the merchant's tenant identifier on your side, and clicks activate. The dashboard then shows two values exactly once:

ValueLooks likeYou use it to
API keyqpk_9f2a…Send in the X-Api-Key header on every request. It identifies the merchant — never send a merchant ID in the body.
Signing secretrandom stringCompute the X-Signature header on POST /events, and verify the signature on webhooks Quroosh sends you.
Shown once — store them now

Quroosh keeps only a hash of the key and never displays the raw values again. Store both in your server-side secret store. If a value is lost, the merchant rotates from the same page: rotation issues a new value while the old one keeps working until it is explicitly revoked, so cut-over is zero-downtime.

Staging and production are separate: the merchant activates on each environment's dashboard and each issues its own credentials. A staging key only authenticates against api-staging.quroosh.io.

2. Test the connection

GET /connection is the connection test. A green response proves your key, tenant resolution, and the integration state end-to-end, and returns the merchant's store name — show it as "Connected to …" so a pasted key for the wrong account is caught immediately. No signature needed on GET requests — only the API key:

curl https://api-staging.quroosh.io/api/partner/v1/connection \
  -H "X-Api-Key: qpk_XXXXXXXXXXXX"

200 OK:

{
  "connected": true,
  "storeId": "0195d0a0-0000-7000-8000-0000000000aa",
  "storeName": "Karam Coffee",
  "platform": "Kosoor",
  "environment": "Production"
}

environment is the deployment on your side that Quroosh delivers webhooks to — if it says Testing when you expected production, the merchant picked the wrong deployment at activation.

If it fails instead, every error uses the same envelope ({ "code", "message", "correlationId" }):

StatusCodeFix
401MISSING_API_KEYThe X-Api-Key header is absent.
401INVALID_API_KEYKey unknown or revoked — re-check the value, or ask the merchant to rotate.
401INTEGRATION_NOT_CONNECTEDThe merchant hasn't activated the integration yet (step 1).
503INTEGRATION_PAUSEDThe merchant paused the integration. Credentials stay valid — retry later.

3. Sync your branches

Everything you push into Quroosh goes through one endpoint:POST /api/partner/v1/events. Each request is anevent envelope signed with your secret. Write the sender once and reuse it for every event type in this guide:

using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

async Task<EventAckResponse?> SendEventAsync(
    HttpClient client, string apiKey, string signingSecret,
    string eventType, string entityType, string entityId, object payload)
{
    // Serialize once so the bytes you sign are the exact bytes you send.
    var rawBody = JsonSerializer.SerializeToUtf8Bytes(new
    {
        eventId = Guid.CreateVersion7(),   // dedup key — one per business event
        occurredAt = DateTimeOffset.UtcNow,
        entityType,
        entityId,
        sourceSystem = "Kosoor",           // your platform identifier
        schemaVersion = 1,
        correlationId = Guid.CreateVersion7(),
        eventType,
        payload
    });

    // X-Signature: t=<unix_ts>,v1=<hex> — HMAC-SHA256 over "{timestamp}." + raw body.
    var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
    using var hmac = IncrementalHash.CreateHMAC(
        HashAlgorithmName.SHA256,
        Encoding.UTF8.GetBytes(signingSecret));
    hmac.AppendData(Encoding.UTF8.GetBytes($"{timestamp}."));
    hmac.AppendData(rawBody);
    var signature = $"t={timestamp},v1={Convert.ToHexStringLower(hmac.GetHashAndReset())}";

    using var content = new ByteArrayContent(rawBody);
    content.Headers.ContentType = new("application/json");
    using var request = new HttpRequestMessage(HttpMethod.Post, "api/partner/v1/events")
    {
        Content = content
    };
    request.Headers.Add("X-Api-Key", apiKey);
    request.Headers.Add("X-Signature", signature);

    using var response = await client.SendAsync(request);
    return await response.Content.ReadFromJsonAsync<EventAckResponse>();
}
Sign the raw bytes

The HMAC covers the exact byte sequence Quroosh receives. Re-serializing the JSON between hashing and sending (whitespace, key order) breaks verification — hold onto the byte[] and reuse it for both.

Now announce each of your branches with a BranchSynced event:

var branchId = Guid.Parse("7c9e6679-7425-40de-963d-0d5f8b1a2c11"); // your branch's ID

var ack = await SendEventAsync(client, apiKey, signingSecret,
    eventType: "BranchSynced",
    entityType: "Branch",
    entityId: branchId.ToString(),
    payload: new
    {
        externalBranchId = branchId,
        name = "Olaya Branch",
        isActive = true
    });

200 OK:

{
  "accepted": true,
  "eventId": "0195d1c7-6e40-7f8e-9a2b-000000000001",
  "alreadyProcessed": false
}

accepted means the event was recorded. Resending the sameeventId replays the cached acknowledgement withalreadyProcessed: true — the handler never runs twice, so retries are always safe.

4. The merchant maps the branches (Quroosh dashboard)

Each synced branch arrives in Quroosh as Unmapped. Quroosh does not prompt anyone — the merchant opens Partner Integrations → Branch mappings, picks the matching Quroosh branch for each of your branches, and confirms. The mapping is edited only there, so the two sides can never disagree about which branch is which.

You can watch the status from your side at any time:

curl https://api-staging.quroosh.io/api/partner/v1/branch-mappings \
  -H "X-Api-Key: qpk_XXXXXXXXXXXX"
{
  "mappings": [
    {
      "externalBranchId": "7c9e6679-7425-40de-963d-0d5f8b1a2c11",
      "externalBranchName": "Olaya Branch",
      "status": "Mapped",
      "mappedStoreId": "0195d0a0-0000-7000-8000-0000000000aa",
      "mappedStoreName": "Karam Coffee",
      "mappedBranchId": "0195d0a0-0000-7000-8000-000000000001",
      "mappedBranchName": "Olaya"
    }
  ]
}
Sales are rejected until the branch is mapped

An InvoiceEarned for an unknown branch fails withBRANCH_MAPPING_NOT_FOUND; for a synced-but-unconfirmed branch, BRANCH_MAPPING_UNCONFIRMED. Failed events are never cached — once the merchant confirms the mapping, resend the same event (same eventId) and it succeeds.

5. Sync a customer

Quroosh matches earns to loyalty members by your customer ID, so a customer must be synced before their first invoice. Same sender, different payload:

var ack = await SendEventAsync(client, apiKey, signingSecret,
    eventType: "CustomerSynced",
    entityType: "Customer",
    entityId: "cust_012345",
    payload: new
    {
        externalCustomerId = "cust_012345",
        phone = "+966501234567",
        firstName = "Sara",
        lastName = "Alqahtani"
    });

Send one on customer create and update. The phone number is how Quroosh links your customer to their loyalty account.

6. Send your first sale

The moment you've been building toward — a customer earns points on a 104.35 SAR invoice:

var invoiceId = Guid.CreateVersion7();

var ack = await SendEventAsync(client, apiKey, signingSecret,
    eventType: "InvoiceEarned",
    entityType: "Invoice",
    entityId: invoiceId.ToString(),
    payload: new
    {
        externalInvoiceId = invoiceId,
        externalCustomerId = "cust_012345",
        externalBranchId = branchId,
        subtotalAfterDiscountBeforeVat = 104.35m,
        earnType = "Points",
        stampCardId = (Guid?)null,
        channel = "Pos"
    });

Console.WriteLine($"accepted={ack!.Accepted} alreadyProcessed={ack.AlreadyProcessed}");

accepted: true and the customer's balance moves. From here, fire InvoiceEarned on every completed sale.

7. What to build next

  • Checkout lookupGET /customers/lookup?phone=…returns the customer's balance, tier, redeemable rewards, and stamp progress live at the till. See Customer lookup.
  • Products and coupons — send ProductSynced andCouponSynced the same way you synced branches, plusCouponUsed / CouponUsageCancelled on use. The merchant then maps Quroosh rewards to your products from their reward editor. See Event types for every payload.
  • Receive webhooks — Quroosh pushesLoyaltyRuleChanged and IntegrationStatusChangedto your registered receiver endpoint, signed with the sameX-Signature scheme; route each delivery by itsX-TenantId header. See Webhooksfor the verification snippet and retry ladder.

Go-live checklist

  • GET /connection returns the merchant's store name on staging.
  • Every branch shows Mapped in GET /branch-mappings.
  • Customers sync on create and update.
  • An InvoiceEarned is accepted, and resending the same eventId returns alreadyProcessed: true.
  • Your webhook endpoint verifies X-Signature and responds 2xx.
  • The merchant activates on production and you swap in the production credentials.

Keep reading

  • Authentication — the signature scheme in detail, key rotation, error responses.
  • Event types — the full catalogue of 16 events and their payload shapes.
  • Errors — every error code, including the event-processing codes.
  • API Reference — request/response schemas with a try-it playground.