This guide explains how to submit (save) sales transactions to the bLoyal Grid Service from a partner system. It covers the endpoint to call, the authentication model, the minimum required fields, the request/response shapes, validation rules, and patterns for replacing or voiding previously-submitted sales.
Intended audience: a technical integration developer (or a bLoyal integration agent) building a connector, POS integration, or back-office export into the Grid Service. This guide only covers submitting sales transactions. Change synchronization (pulling/acking outbound changes) is out of scope.
1. What the Grid Service does with a sales transaction
A sales transaction represents a completed sale at a store/device. When you post one to the Grid:
- The Grid resolves the store and device from the identifiers you provide (or from your access key, if it is a device key).
- The Grid looks up (or creates) the customer associated with the sale.
- The Grid validates the transaction is in balance (payments match line totals + tax + shipping − discounts).
- The Grid hands the transaction to the Loyalty Engine, which computes loyalty effects (points earned/redeemed, coupons, club/program impacts, etc.), then persists the final sales record.
- A
CommandResponseis returned with the resulting transactionUid.
If the transaction was preceded by a cart-commit flow (bLoyal calculated the cart and committed it at checkout), the Grid will merge the submitted sale with the pending sales transaction created by that commit. If no pending commit exists, the Grid asks the Loyalty Engine to calculate one on the fly from the submitted sale.
2. Endpoints
The Grid exposes two endpoints for submitting sales.
Important - The Grid is a Tier-2 service and should not be called "In-flow" of the transaction like the Loyalty Engine which is a Tier-1 service. The Grid Apis are intended for asynchrounous backoffice connectors or from a non-blocking webhook.
| Purpose | Method + Route | Request Body | Notes |
|---|---|---|---|
| Submit a single sale via a command wrapper | POST /api/v4/{accessKey}/SalesTransactions/Commands/Saves |
Command envelope wrapping one Transaction — see §2.1 |
Intended for partner webhooks — one sale per call, as it happens. Returns a command response with the resulting EntityUid. The example JSON in this guide uses this endpoint. |
| Submit one or more sales as a change batch | POST /api/v4/{accessKey}/SalesTransactions/Changes |
Array of change envelopes, each wrapping one Entity — see §2.2 |
Intended for back-office connectors syncing multiple sales transactions as part of a scheduled integration batch. Errors for individual entities may be written to the entity exception queue instead of returned inline. |
Both endpoints apply the same validation and accept the same SalesTransaction payload — the difference is only the envelope.
{accessKey}is the bLoyal API access key provisioned for your integration. It must have rights to submit sales transactions. If the key is not a device-scoped key, you must also supplyStoreCodeandDeviceCodeon the command (or on the transaction itself).
2.1 Request envelope — Commands/Saves
{
"Transaction": { /* sales transaction — see §5 */ },
"StoreCode": "MyStore", // optional — only required if access key is not store/device scoped
"DeviceCode": "2", // optional — only required if access key is not device scoped
"ChannelCode": "POS", // optional — overrides the device default channel
"ReferenceNumber": null, // optional — your reference, logged on the command
"Status": "Pending",
"RetryCount": 0,
"Id": 0,
"Uid": "00000000-0000-0000-0000-000000000000",
"Created": "2026-04-22T14:45:26.0011348Z",
"Updated": "2026-04-22T14:45:26.0011348Z"
}
Status, RetryCount, Id, Uid, Created, and Updated are standard envelope fields shared by all Grid commands. They are accepted on input for consistency, but you do not need to populate them meaningfully for a new submission — the values shown above are safe defaults.
2.2 Request envelope — Changes
[
{
"ChangeType": "Modified",
"ExternalId": "POS-12345",
"Entity": { /* sales transaction — see §5 */ }
}
]
ChangeType is always Modified for a submission. Deleted is not supported for sales transactions — see §7 for how to void.
3. Authentication
All Grid endpoints authenticate via the {accessKey} URL segment. The access key determines:
- Client (tenant) scope — which bLoyal environment the sale lands in.
- Store / device scope — if the key is provisioned as a device key, the Grid will default
StoreCode/DeviceCodefor you; if it is a back-office connector key, you must supply them on the transaction (or on the command envelope). - Permissions — the key must be allowed to submit sales transactions.
Contact bLoyal to have an access key provisioned for your integration. Access keys are long-lived — treat them as secrets.
4. Identifying entities — ExternalId, Code, and Uid
A key concept for any bLoyal integration is how entities are identified. Every entity in bLoyal has three potential identifiers, and understanding which one to use for what is central to getting sales submission right.
| Identifier | Who owns it | When to use it |
|---|---|---|
ExternalId |
You. It is your system's identifier for the entity — whatever id your application already assigns (e.g. your receipt number, your customer id, your order id). | This is what integrators should submit. The only rule is that an ExternalId must be unique within your system for a given bLoyal client. |
Code |
The user (configured inside bLoyal). A user-changeable business identifier (e.g. a product SKU, a store code, a customer account number). Only some entity types have a Code. |
Use when the bLoyal-side code is the stable, natural handle for an entity — e.g. StoreCode, DeviceCode, ProductCode, TenderCode, which are typically set up in bLoyal by the client. |
Uid |
bLoyal. A globally-unique id (GUID) that bLoyal assigns when it first sees the entity. | You do not need to send it when submitting a sale, and you do not need to save it. Your ExternalId is all you need to reference a sale later. |
Why ExternalId matters for sales transactions: when the Grid receives a sales transaction, it uses the ExternalId to decide whether it has already seen that sale. If the same ExternalId arrives twice, the Grid treats the second call as a duplicate (or as a modify/void, depending on the flags you set — see §7). This makes submission idempotent and retry-safe: if your call times out, you can simply re-send it.
Rule of thumb for this guide:
- On the sales transaction itself — send
ExternalId(your id for the sale). - On nested references to entities that are configured in bLoyal (store, device, product, tender, channel) — send the
Code. - On the customer — send
Customer.ExternalId(your customer id) and/orCustomer.EmailAddress; bLoyal will look up or create the customer for you. - You never need to send
Uidfor a new submission.
The rest of this guide is written with that rule of thumb in mind.
5. The SalesTransaction payload
This section describes the JSON shape of the SalesTransaction object that goes inside the request envelope (as Transaction for Commands/Saves or as Entity for Changes). The schema is also available in the Grid's Swagger document.
The sections below group fields by role. Fields marked Required must be present for a submission to succeed. Everything else is optional — send what you have.
5.1 Identity — ExternalId is required
Send your system's identifier for the sale as ExternalId.
| Field | Type | Description |
|---|---|---|
ExternalId |
string | Required. Your id for the sale. Must be unique within your system for the bLoyal client you are integrating with. The Grid uses this to dedupe retries and to find the sale later for modify/void. |
That's it for identity. You do not need to send Uid, Id, CartUid, or any of the other identifier fields on the entity — the Grid will assign a Uid internally. You do not need to capture or store that Uid; use your own ExternalId whenever you need to reference the sale later (for modify, void, or lookup).
If
ExternalIdis missing on submission the Grid will reject the request with abLoyalExceptionindicating no transaction identifier was found.
5.2 Store & device — required
Provide the Code for the store and device the sale belongs to:
| Field | Type | Description |
|---|---|---|
StoreCode |
string | Required (typical). The store code as configured in bLoyal. |
DeviceCode |
string | Required (typical). The device code (POS, web, kiosk, etc.) as configured in bLoyal. |
If your access key is a device-scoped key, the Grid fills in StoreCode / DeviceCode for you and you may omit them. Otherwise you must supply them — either on the transaction or on the command envelope (§2.1). If the device cannot be resolved, the Grid responds with NotFoundException: Unable to resolve device for sales transaction.
CashierCode and ChannelCode are optional. ChannelCode defaults to the device's channel.
5.3 Customer
Customer is a nested object. It is optional but highly recommended — a sale without a customer will not contribute to loyalty, membership, or engagement for that shopper. If the shopper is truly anonymous, leave Customer null or set GuestCheckout = true.
To identify an existing bLoyal customer, the simplest approach is to send Customer.ExternalId (your customer id). You can also identify by email, phone number, or loyalty card number. The Grid will look up or create the customer as needed.
Typical customer block for a new signup during checkout:
"Customer": {
"ExternalId": "CUST-98765",
"FirstName": "Joe",
"LastName": "Tester",
"EmailAddress": "joetester@bloyal.com",
"MobilePhone": "111-222-3333",
"Address1": "704 228th Ave NE #842",
"City": "Sammamish",
"State": "WA",
"PostalCode": "98074",
"Country": "US"
}
You do not need to populate Customer.Uid, Customer.Id, or the various membership / loyalty arrays — the Grid fills them in.
5.4 Lines — required (at least one)
Lines is a JSON array of line objects. The Grid expects at least one line; line-less sales fail balance validation.
| Field | Type | Required | Description |
|---|---|---|---|
Number |
string | Yes | Line number within the sale (e.g. "1", "2"). Unique within the transaction. |
ProductCode |
string | Yes | Product code (SKU) as configured in bLoyal. |
Quantity |
number | Yes | Positive for a sale, negative for a return line. |
Price |
number | Yes | Per-unit price (excluding tax if TaxInclusive is not set). Multiplied by Quantity when computing the line total. |
ProductName |
string | No | Descriptive name, for logging/reporting. |
ShipmentNumber |
string | No | Matches a Number in Shipments if this line ships. |
Weight |
number | No | Per-unit weight. |
Discount |
number | No | Per-unit line discount. Multiplied by Quantity when computing the line total. |
DiscountReasonCode |
string | No | Your reason code — does not need to pre-exist in bLoyal. |
TaxDetails |
array | No | Tax breakdown for the line. The Amount values are line-total amounts (not per-unit) and are summed as-is. Required if your system computed tax externally (ExternalTaxCalculation on the shipment). Each entry: { Code, Rate, Amount }. |
OrderDiscount |
number | No | Per-unit portion of the header OrderDiscount allocated to this line. Multiplied by Quantity when computing the line total. Only needed for systems that distribute order discounts down to the line — if your system supports true order-level discounts, just provide OrderDiscount in the header instead. |
SerialNumber |
string | No | For serialized products. |
Cost, Cost2 |
number | No | Your cost figures. |
DepartmentCode, CategoryCode, SalesRepCode, ParentNumber (for assembly/kit items), and AssemblyItems are all optional and used only when relevant.
5.5 Shipments
Shipments is a JSON array of shipment objects. Use one entry per physical or digital fulfillment. For a walk-out-with-product POS sale, a single shipment with Charge = 0 is common; you can also omit Shipments entirely if your product is not delivered.
| Field | Type | Description |
|---|---|---|
Number |
string | Shipment number (matches Lines[].ShipmentNumber). |
Charge |
number | Shipping charge on this shipment. |
Discount |
number | Discount against the shipping charge. |
TaxDetails |
array | Tax computed on the shipping charge. Each entry: { Code, Rate, Amount }. |
ExternalShippingCalculation |
boolean | Set true if your system calculated the shipping amount (bLoyal will not recalculate). |
ExternalTaxCalculation |
boolean | Set true if your system calculated tax (bLoyal will not recalculate). |
CarrierCode, ServiceCode |
string | Optional carrier/service identifiers. |
Recipient |
object | Optional ship-to party; defaults to the customer. |
GiftPackage, GiftComment, Instructions |
— | Optional. |
5.6 Payments — required (at least one for a non-zero sale)
Payments is a JSON array of payment objects. The sum of Amount across payments must match the computed total of the transaction (see §6).
| Field | Type | Required | Description |
|---|---|---|---|
TenderCode |
string | Yes | The tender code as configured in bLoyal (e.g. cash, credit, gift card). |
Amount |
number | Yes | Amount tendered. Positive on a sale. |
External |
boolean | No | true if the payment was authorized outside bLoyal (typical for POS integrations). |
TransactionCode |
string | No | Processor/authorization transaction id. |
PaymentToken |
string | No | Tokenized payment reference returned by your payment gateway. |
5.7 Accounts
Accounts is a JSON array of account objects — used for tip-out-to-account, store credit applied, gratuity, or any "side" amount the sale created that is not a line or a shipment. Usually empty for basic integrations.
5.8 Discounts
OrderDiscount— a discount applied at the order (header) level, independent of any line discount. Fields:Amount,External, optionalReasonCode,CouponCode,DiscountRuleCode,Name.- Line-level discounts live on each
Lines[].Discount.
5.9 Totals — computed automatically
TotalPrice, TotalDiscount, TotalTax, TotalAmount, TotalPayment, TotalQuantity, TotalWeight, and ShipmentCount are computed properties. The getter sums the detail collections; any value you send for them is ignored. You can safely include them (as the example payload does) — they are useful for logging and for asserting your sender's math matches the Grid's.
5.10 Optional metadata
| Field | Description |
|---|---|
Title |
Display title shown in bLoyal UI (e.g. "Test Order - Do Not Ship!!!"). |
Comment |
Free-form comment on the order. |
ReferenceNumber / LoyaltyReferenceNumber |
Your reference identifiers. |
Number / OrderNumber |
Your POS receipt / order numbers. |
ReferredBy, Sponsor |
Referral tracking — only used for programs that support it. |
Tip |
Tip amount, if captured separately from payments. |
TaxInclusive |
Set to true if your amounts already include tax (common outside the US). |
BatchNumber |
Your batch/day-part identifier. |
6. Validation — the balance rule
Before processing, the Grid computes the transaction total and verifies that the sum of Payments[].Amount matches it. Remember that Price, Discount, and OrderDiscount on a line are all per-unit amounts — they have to be multiplied by Quantity when totaling the line.
Per line:
LineTotal = (Price - Discount - OrderDiscount) * Quantity + sum(TaxDetails.Amount)
Per shipment:
ShipmentTotal = Charge - Discount + sum(TaxDetails.Amount)
Transaction total:
TransactionTotal = sum(LineTotal across all Lines)
+ sum(ShipmentTotal across all Shipments)
+ Tip
+ sum(Accounts[].Amount)
- OrderDiscount.Amount // header-level order discount, if not already distributed to the lines
Balance rule:
sum(Payments[].Amount) == TransactionTotal
The sum of Payments[].Amount must equal the computed TransactionTotal. Always submit a balanced sales transaction. Out-of-balance sales may be flagged for review and may not merge cleanly with a pending cart commit.
If you distribute the order discount across lines via
Lines[].OrderDiscount, leave the headerOrderDiscount.Amountat0(or omit it). Otherwise you will double-count the discount. The sum ofLines[].OrderDiscount * Quantityshould equal the headerOrderDiscount.Amountthat you would have otherwise sent.
The balance check operates on the raw amounts you sent, so make sure TaxInclusive is set correctly if your prices already include tax.
6.1 Rounding — keep the details full-precision
Rounding on the detail fields is the most common cause of out-of-balance sales. The rule is simple:
- Do not round the detail fields. Send
Price,Discount,OrderDiscount, line and shipmentTaxDetails[].Amount, andAccounts[].Amountat full decimal precision — whatever precision your internal calculations produce. Do not pre-round to 2 decimal places. - Round only at the end, when you compute the figures the customer actually pays: the total tax presented on the receipt and the
Payments[].Amountvalues (what you actually charged the card / collected as cash). Those totals should match what your payment processor captured, which is normally rounded to the currency's minor unit (cents for USD).
This ordering guarantees that sum(Payments[].Amount) equals the TransactionTotal the Grid computes, because the Grid sums the unrounded details the same way you do and the single rounding step happens after the sum. If you round each line's tax or discount individually before summing, the accumulated rounding error will push the transaction out of balance.
In the full example in §10, you can see this pattern in action: individual TaxDetails[].Amount values carry six decimal places (1.816875, 1.514625, 3.08794), and the single Payments[0].Amount of 67.34444 is the rounded-to-the-collected-amount total. The details are full precision; only the payment is "customer-facing precision."
7. Resubmitting, modifying, and voiding
Because the Grid looks transactions up by ExternalId, resending the same sale is safe — by default the Grid dedupes on the first match and returns the existing Uid.
Three boolean flags on SalesTransaction let you tell the Grid what to do if it already has a transaction matching your ExternalId:
| Flag | Behavior if the transaction exists |
|---|---|
ReplaceIfExists |
Grid generates a return transaction for the original and inserts the new one. Loyalty commitments (points used/earned) are moved to the new transaction. Use when the original was wrong end-to-end. |
ModifyIfExists |
Grid updates the existing transaction in place — no return is generated, loyalty commitments remain on the same transaction. Use for late-arriving detail (e.g. tip added after the sale). |
VoidIfExists |
Grid voids the existing transaction by generating a return from it. No other transaction detail is needed in the request — just the ExternalId (plus store/device if your access key requires them). |
If none of these flags are set and the ExternalId already exists, the Grid will:
- Treat it as a duplicate and ignore the resubmission (idempotent retry), unless
- The resubmission adds a tip or new identifiers not on the original — in which case the Grid silently promotes it to a
ModifyIfExists.
This is what makes submission safe to retry on network errors: if your client times out, just re-send the same payload with the same ExternalId.
8. Response format
Both endpoints return a standard server-response envelope wrapping a command response:
{
"Success": true,
"Response": {
"CommandUid": "...",
"EntityUid": "1db93833-8855-4654-843e-6d39e0a3b7ff",
"IsNewEntity": true,
"Message": "Sales transaction queued for processing"
}
}
EntityUid— theUidbLoyal assigned to the resulting sales transaction. Returned for informational / logging purposes; you do not need to store it, since yourExternalIdis sufficient to reference the sale later.IsNewEntity—trueif a new transaction was created,falseif an existing one was updated/deduped.Message— informational; may indicate the sale was queued for Loyalty Engine calculation rather than finalized synchronously.
On error the response is a non-2xx status with Success: false and an error message describing the problem. Common errors:
| Error message | Cause |
|---|---|
| "No data was provided on your request or invalid json provided." | Empty body or unparseable JSON. |
| "No transaction identifier was found. You must provide a sales transaction ExternalId …" | ExternalId was missing from the transaction. |
| "Unable to resolve device for sales transaction …" | StoreCode / DeviceCode do not match any configured bLoyal store/device. |
| "SalesTrans out of balance …" | Balance check failed (logged only — does not block acceptance but indicates a bug in your sender). |
| "… CartCommitment not yet received from Loyalty Engine …" | Submission references a cart that bLoyal has not finished committing; the Grid retries automatically, but after 4 retries surfaces this. |
9. Minimum viable payload
This is the smallest request body that will submit successfully on POST .../SalesTransactions/Commands/Saves for a non-device-scoped access key. Everything else is optional.
{
"Transaction": {
"ExternalId": "POS-12345",
"StoreCode": "MyStore",
"DeviceCode": "2",
"Customer": {
"ExternalId": "CUST-98765",
"EmailAddress": "joetester@bloyal.com"
},
"Lines": [
{
"Number": "1",
"ProductCode": "1010",
"Quantity": 1,
"Price": 25.50
}
],
"Payments": [
{
"TenderCode": "1",
"Amount": 25.50
}
]
}
}
To void an existing transaction, the minimum payload is even smaller:
{
"Transaction": {
"ExternalId": "POS-12345",
"StoreCode": "MyStore",
"DeviceCode": "2",
"VoidIfExists": true
}
}
10. Full example
The following is a realistic request body for POST .../SalesTransactions/Commands/Saves — adapted from a payload the Grid received from a partner's test application. It demonstrates a two-line sale with shipping, tax, order-level and line-level discounts, and a single tokenized payment.
{
"Transaction": {
"ExternalId": "POS-TEST-0001",
"StoreCode": "MyStore",
"DeviceCode": "2",
"Title": "Test Order - Do Not Ship!!!",
"ChannelCode": "POS",
"Customer": {
"ExternalId": "CUST-98765",
"FirstName": "Joe",
"LastName": "Tester",
"CompanyName": "bLoyal",
"Address1": "704 228th Ave NE #842",
"Address2": "Ste 305",
"City": "Sammamish",
"State": "WA",
"PostalCode": "98074",
"Country": "US",
"EmailAddress": "joetester@bloyal.com",
"MobilePhone": "111-222-3333"
},
"OrderDiscount": {
"External": false,
"Amount": 10.0
},
"Shipments": [
{
"Charge": 10.0,
"GiftPackage": false,
"Discount": 0.0,
"ExternalShippingCalculation": false,
"ExternalTaxCalculation": false,
"TaxDetails": [
{ "Code": "TestRate", "Rate": 0.075, "Amount": 0.75 }
]
}
],
"Lines": [
{
"ProductCode": "1010",
"ProductName": "1010 TEST PRODUCT",
"Number": "1",
"ShipmentNumber": "1",
"Quantity": 1.0,
"Price": 25.5,
"Discount": 1.275,
"DiscountReasonCode": "MyReasonCode",
"OrderDiscount": 4.03,
"TaxDetails": [
{ "Code": "TestRate", "Rate": 0.075, "Amount": 1.816875 },
{ "Code": "TestRate", "Rate": 0.075, "Amount": 1.514625 }
]
},
{
"ProductCode": "10001",
"ProductName": "2015 Chardonnay",
"Number": "2",
"ShipmentNumber": "1",
"Quantity": 1.0,
"Price": 35.95,
"Discount": 0.0,
"OrderDiscount": 5.97,
"TaxDetails": [
{
"Code": "State: WA, County: King, City: Bellevue, Zip: 98007-3825, FIPS: 05210 BELLEVUE,53 WA, LOC: 1704",
"Rate": 0.103,
"Amount": 3.08794
}
]
}
],
"Payments": [
{
"TenderCode": "1",
"External": false,
"Amount": 67.34444,
"TransactionCode": "1234567890",
"PaymentToken": "xyzToken1234"
}
]
}
}
POST this body to:
POST https://{grid-host}/api/v4/{accessKey}/SalesTransactions/Commands/Saves
Content-Type: application/json
11. Integration checklist
Before going live, confirm each of the following:
- [ ] You have a production access key and know whether it is store/device scoped.
- [ ] Your
StoreCodeandDeviceCodevalues match what bLoyal has configured. - [ ] Every sale includes an
ExternalId, and your system guaranteesExternalIduniqueness per bLoyal client. - [ ] Your sender only submits once per completed sale, but is safe to retry on network failure (the Grid dedupes by
ExternalId). - [ ] Every submitted sale balances (payment total vs. computed total).
- [ ] Tender codes on
Payments[].TenderCodemap to tenders configured in bLoyal. - [ ] Product codes on
Lines[].ProductCodeexist in bLoyal. - [ ] For returns / modifications, you send the appropriate flag (
ReplaceIfExists,ModifyIfExists, orVoidIfExists) along with the sameExternalId.
Comments
0 comments
Article is closed for comments.