If your store runs on software you build yourself, or on a platform without a ready connector, it can send signed events to a store connection. This page is for the developer who adds the sending side: it gives the address, the signature, the event formats and the answers.
Before you start
- Who can create the connection: owners and administrators of the company.
- Plan: available when your plan includes Store connections.
- A pipeline must exist: quote requests become deals in the stage you choose. See Deals and pipelines.
- Your store sends; Rutba CRM does not fetch. Nothing is imported automatically from a custom store, so past orders arrive only if your store sends them.
Create the connection
- In the sidebar open Products, then click Store connections.
- Under Connect a store, choose Our store platform or custom as Store platform. Store currency is optional for this platform.
- In New quote requests start in, choose the stage for quote requests, and in Assign new deals to, the person who receives them.
- Click Connect a store. Copy the Signing secret now: it is shown only once.
- Copy the address in Address for your store.
Keep both in your store's environment settings, not in its code:
STORE_EVENTS_URL=ADDRESS_FOR_YOUR_STORE
STORE_EVENTS_SECRET=YOUR_SIGNING_SECRET
Sign every request
Send POST with a JSON body and two headers:
| Header | Value |
|---|---|
X-CRM-Timestamp |
The current time in Unix seconds |
X-CRM-Signature |
sha256= followed by the hex HMAC-SHA256 of TIMESTAMP.BODY, keyed with the signing secret |
BODY is the exact bytes you send. A request whose time is more than 5 minutes away from the server's clock is refused, so keep your server's clock in sync.
$body = json_encode(['event' => $event, $key => $document], JSON_THROW_ON_ERROR);
$time = (string) time();
$signature = 'sha256='.hash_hmac('sha256', $time.'.'.$body, getenv('STORE_EVENTS_SECRET'));
// POST $body to STORE_EVENTS_URL with Content-Type: application/json,
// X-CRM-Timestamp: $time and X-CRM-Signature: $signature
Send from a background job after the order is saved, so a slow answer never delays a purchase. Retry only on a 5xx answer or a timeout.
Events
| Event | Key in the body | Effect |
|---|---|---|
order.created |
order |
Saves the order and finds or creates the customer |
order.updated |
order |
The same, for any change. Send a newer updated_at each time |
quote.requested |
quote |
Creates a deal in the chosen stage with the lines, once per id |
product.upserted |
product |
Creates or updates a product |
Order and quote fields
{"event": "order.updated", "order": {
"id": "1001", "number": "A-1001", "status": "paid", "currency": "SAR",
"total": "1250.50", "placed_at": "2026-11-06T10:00:00Z", "updated_at": "2026-11-06T10:05:00Z",
"customer": {"name": "Sample Customer", "email": "customer@example.com", "phone": "+966500000001", "company": "Sample Cafe"},
"items": [{"sku": "M-1", "name": "Espresso machine", "quantity": 1, "total": "1250.50"}],
"attribution": {"source": "google", "medium": "cpc", "campaign": "autumn-sale"}}}
| Field | Rule |
|---|---|
id |
Required, your own order ID, up to 100 characters |
number |
Optional, the number customers see, up to 60 characters |
status |
Optional, up to 40 characters. Defaults to pending |
currency |
Required: USD, EUR, GBP, TRY, SAR or AED |
total |
Required, a string such as "1250.50", with a dot and at most two decimals |
placed_at, updated_at |
placed_at is required. Both are dates such as ISO 8601 |
customer.name |
Required, up to 160 characters |
customer.email, customer.phone |
At least one is required. A phone needs 6 to 15 digits, ideally with its country code |
customer.company |
Optional |
items |
Optional, up to 200 lines. Each needs name, a whole quantity of at least 1 and a string total; sku is optional |
attribution |
Optional source, medium and campaign, up to 120 characters each. The campaign appears in reports |
recovery_url |
Optional https:// link where the customer can still pay an unpaid order |
A quote request uses the same fields under the key quote. Its deal amount is the sum of its lines, or the total when there are no lines.
Statuses
paid, processing, completed and delivered count as sales, and the first of them makes a lead a customer. Any other status is saved as it is and does not count. Use cancelled or refunded to take an order out of sales.
Product fields
{"event": "product.upserted", "product": {"id": "P-77", "name": "Espresso machine", "sku": "M-1", "price": "1250.50", "currency": "SAR", "active": true, "updated_at": "2026-11-06T10:00:00Z"}}
id and name are required. A product is matched by id, then by sku. "active": false archives it. A price is saved only with a currency. An update older than the saved one is ignored.
Answers
| Answer | Meaning |
|---|---|
200 {"status": "received"} |
Saved, or a repeat of something already saved |
200 {"status": "paused"} |
The company or its plan does not accept store events right now. Nothing was saved |
403 |
Wrong signature, or a time more than 5 minutes off |
404 |
The connection does not exist or is turned off |
422 {"errors": [...]} |
The document is not valid. The list names the fields. Sending it again unchanged will not help |
Each sending server can make up to 600 requests a minute.
Test it
- Send an
order.updatedwith the statuspaidand a campaign inattribution. - In Rutba CRM, check that the contact exists, the order shows on their record, and the store card shows Last event:.
- Send the same order again with the status
cancelledand a laterupdated_at. In Reports, the order leaves sales and the campaign total drops.
Good to know
- Repeats are safe. An order update older than the saved one, or identical to it, is ignored; a quote request with an
idalready seen is ignored. - Customer matching: the email decides first. A phone only joins a contact that has no other email. Details of an existing contact are never overwritten.
- Replace the secret on the store card gives a new secret at once, and the old one stops working. Update your store's settings straight away.
Last updated 2026-11-08