Panasonic POS integration
This guide is for Panasonic engineers connecting a Panasonic POS to Spoonity loyalty and promotions. The POS sends Spoonity each check. Spoonity works out the discounts and returns them item by item. When the check closes, Spoonity records the sale in the customer's loyalty account.
The POS never calculates a Spoonity discount, never chooses a reward, and
never calls any other Spoonity API. Three calls cover the whole integration:
check, close and void.
Base URL: https://panasonic.spoonity.com
Contents
- Status
- The flow
- Authentication
- Identifiers
- The check: items, modifiers and combos
- POST /partners/pos/check
- Applying the discounts
- POST /partners/pos/close
- POST /partners/pos/void
- Repeating calls safely
- Timeouts and failures
- Errors
- Testing
- Checklist
1. Status
| Item | Status |
|---|---|
check, close and void, item-level discounts, combos, holds, repeat-safe calls | Live |
Authorization: Bearer with the merchant's API key | Live |
| Rewards the customer picked in the app, found from the scanned code | Live |
| Spoonity posts every closed check to the loyalty program (points and reward redemption) | Live |
| Voiding a closed check also reverses it in the loyalty program | Live |
Everything in this guide is built and live. Testing on a Panasonic VM against a test merchant is the next step (section 13).
2. The flow
- Before the till. The customer opens the merchant's app and picks the reward they want to use. The app tells Spoonity directly. The POS is not involved and never sees a list of rewards.
- Scan. At any point while the order is being taken, the cashier scans the customer's code from the app. The POS recognises it as a Spoonity code by its prefix (see Identifiers) and holds it on the check. No call is made at scan time.
- Pay Now. The POS sends the whole check to
POST /partners/pos/checkwith the scanned code. Spoonity finds the customer and the reward they picked, applies the merchant's promotions, holds the reward for this check, and returns each discount with the item it applies to. - Cancel and change. If the cashier cancels out of Pay Now, the POS
removes the Spoonity discounts. No call is needed. When Pay Now is pressed
again, the POS sends
checkagain with the full new check, and that answer replaces the previous one. - Close. When the check is paid and closed, the POS sends
POST /partners/pos/close. Spoonity answers at once, so the POS can finish. Spoonity then posts the sale to the loyalty program in the background: the customer earns points and the reward is used up.
Send check and close for every check, including checks with no code
scanned. A check with no code is priced for a guest: promotions open to
everyone still apply, and no points are earned.
Customer app POS terminal Spoonity
| | |
| pick a reward ---------------------------------->| saved for today
| | |
| show code -------->| scan: code held on check |
| | Pay Now: POST /check ------->| find the pick, price, hold
| |<-----------------------------| discounts by item
| | cancel, change, Pay Now: |
| | POST /check ---------------->| re-price, replace
| |<-----------------------------| discounts by item
| | apply discounts, take payment|
| | check closed: POST /close -->| commit, answer at once
| |<-----------------------------| { committed: true }
| | | post sale to loyalty
3. Authentication
Send the merchant's API key as a bearer token on every request:
Authorization: Bearer MERCHANT_API_KEY
- Spoonity provides the key when the merchant is set up. The key decides the merchant; a merchant id in the body is never read.
- The base URL and the key are the only settings that change per merchant.
- A missing or unknown key is
401 UNAUTHORIZED. X-Spoonity-POS-Key: MERCHANT_API_KEYis also accepted, for POS software that cannot setAuthorization.- Keep the key in POS configuration, never in logs or receipts.
Content-Type: application/jsonon every request. Unknown fields are rejected with400, so a misspelled field is never silently ignored.
4. Identifiers
| Field | Rule |
|---|---|
check_id | The POS order number prefixed with the business date: YYYYMMDD-<order number>, for example 20261103-104202. Letters, digits, _ . -, at most 60 characters, no colon. Spoonity needs it to be unique per store for as long as the check can be refunded, and the date prefix guarantees that. |
terminal_id | Optional. Logged only. |
pos_transaction_id | Your unique id for the closed transaction. Required on close and void. 1 to 128 characters. |
customer.customer_id | The code scanned from the customer's app, exactly as scanned, prefix included. |
No store id is needed. Each store has its own API key, and the key tells Spoonity which store (Spoonity vendor) the check belongs to.
Recognising a Spoonity code. Spoonity codes start with a fixed prefix that Spoonity gives you during setup. Configure the POS to treat any scanned code with that prefix as a loyalty code. What follows the prefix is Spoonity's business: the POS sends the whole code and never interprets it.
5. The check: items, modifiers and combos
All money is integer cents (1149 is $11.49) and before tax. A decimal
is a 400. Spoonity does not calculate tax.
Items. One line per item on the check:
{ line_id, sku, name?, price, quantity, categories?, modifiers?, discounts? }.
line_idis unique within the check. Use the same ids on everycheckfor that check and onclose.skuis the item's PLU.priceis the unit price the item sold at, without modifiers.
Modifiers. List each modifier on the item it belongs to, with its own
unit price and quantity: { id, name?, price, quantity }. A modifier
with no charge has price: 0.
Combos. Send a combo as one line with its parts in components:
| Field | Notes |
|---|---|
price | The combo price, per combo. |
quantity | Number of combos. |
components[].component_id | Your id for the part, unique within the combo. Spoonity uses it to say which part a discount applies to. |
components[].sku | The part's PLU. |
components[].price | The part's a la carte price. It is not charged; Spoonity uses it to split the combo price across the parts. |
components[].quantity | Units of the part in one combo. |
components[].modifiers | Modifiers on that part. A combo line carries no modifiers of its own. |
Spoonity splits the combo price across the parts in proportion to their a la carte prices, and the parts always add up to the combo price exactly. A reward on a part inside a combo takes off that part's share of the combo price, not its a la carte price.
Your own discounts. Discounts the POS already applied (employee, senior,
manager) go on the line as discounts: [{ id, name?, amount }], whole-line
cents. By default Spoonity does not add its own item discount to a line you
already discounted, and order-level discounts are calculated after yours, so
the same money is never discounted twice.
Limits (a request over any of them is a 400):
- 200 lines per check, 2000 units per check (each part of a combo counts),
1 to 1000 units per line. Leave a line off rather than sending
quantity: 0. - Any single amount at most 100,000,000 cents, never negative.
- Per line or part: at most 50
modifiers, 50categories, 50discountsand 50components. - Ids are 1 to 128 characters,
namefields at most 256.
6. POST /partners/pos/check
Send when Pay Now is pressed, and again every time Pay Now is pressed after a change.
| Field | Type | Notes |
|---|---|---|
check_id | string, required | See Identifiers. |
terminal_id | string | Logged only. |
customer | object | { "customer_id": "<scanned code>" }. Omit when no code was scanned. |
codes | string[] | Coupon or promo codes the cashier entered or scanned. At most 50. |
lines | array, required | See section 5. |
Example: a combo with a large drink, and a cookie. The customer picked a free taters reward in the app, and the merchant runs $5 off checks of $12 or more.
{
"check_id": "20261103-104202",
"terminal_id": "02",
"customer": { "customer_id": "SPN6040123412341234" },
"lines": [
{ "line_id": "101", "sku": "2PCCOMBO", "name": "2 Pc Combo", "price": 1149, "quantity": 1,
"components": [
{ "component_id": "101-1", "sku": "2PC", "name": "2 Pc Chicken", "price": 899, "quantity": 1 },
{ "component_id": "101-2", "sku": "TATERS", "name": "Taters", "price": 399, "quantity": 1 },
{ "component_id": "101-3", "sku": "FOUNTAIN", "name": "Fountain Drink", "price": 279, "quantity": 1,
"modifiers": [{ "id": "LRG", "name": "Large", "price": 50, "quantity": 1 }] }
] },
{ "line_id": "102", "sku": "COOKIE", "name": "Cookie", "price": 199, "quantity": 1 }
]
}
Response 200:
{
"evaluation_id": "2112901:20261103-104202",
"check_id": "20261103-104202",
"customer": {
"customer_id": "2114003",
"balances": [{ "currency_id": "loyalty", "balance": 4200 }]
},
"subtotal": 1398,
"subtotal_discount": 791,
"total": 607,
"discounts": [
{ "pos_reference": "SPN-FREETATERS", "offer_id": "reward_free_taters", "name": "Free Taters (reward)",
"amount": 291, "scope": "item", "line_id": "101", "component_id": "101-2", "quantity": 1 },
{ "pos_reference": "SPN-5OFF", "offer_id": "promo_5_off", "name": "$5 off $12",
"amount": 500, "scope": "order",
"allocations": [
{ "line_id": "101", "component_id": "101-1", "amount": 296 },
{ "line_id": "101", "component_id": "101-3", "amount": 114 },
{ "line_id": "102", "amount": 90 }
] }
],
"offers": [
{ "offer_id": "reward_free_taters", "name": "Free Taters (reward)", "status": "applied",
"source": { "type": "redemption", "vendor_perk_redemption_id": 501 },
"pos_reference": "SPN-FREETATERS", "discount_total": 291 },
{ "offer_id": "promo_5_off", "name": "$5 off $12", "status": "applied",
"source": { "type": "promotion_code" }, "pos_reference": "SPN-5OFF", "discount_total": 500 }
],
"receipt_lines": ["Free Taters (reward) applied, you saved $2.91", "$5 off $12 applied, you saved $5.00"],
"redemption_id": "ored_5a58c78e-fbb4-4674-8bc5-827d72884dea",
"hold_expires_at": "2026-11-03T17:45:00.000Z"
}
subtotalis the check before discounts.subtotal_discountis the sum of every Spoonity discount.totalissubtotal, minus your own line discounts, minussubtotal_discount. All before tax.offers[]lists every offer the merchant has and what happened to it:applied,applicable(would apply if selected),skippedorinapplicable. An offer that did not apply has areasonwith a stablecode(for exampleMIN_SPEND_NOT_MET,RESERVED,LOCALLY_DISCOUNTED) and amessagea cashier can read.receipt_linesare ready to print. Treat them as display text: the wording can change.customerisnullwhen no code was scanned or the code is not recognised. The check is then priced with the promotions open to everyone.hold_expires_at: the customer's reward is held for this check until then (15 minutes), so it cannot be used at another store at the same time. If payment is not finished by then, sendcheckagain.external_discount_totalappears only when you sent your own linediscounts.
7. Applying the discounts
Every Spoonity discount is applied to an item, so tax is calculated on the discounted item. There can be more than one Spoonity discount on a check.
scope: "item": applyamountto the lineline_id, or to the combo partcomponent_idof that line when it is present.scope: "order": apply each entry inallocationsto its line (and combo part) for that entry'samount. The allocations always add up to the discount'samount.
How the POS represents the discount is up to you. One generic "Spoonity
Discount" item, sold as a negative-priced modifier on the item it discounts,
works for every Spoonity discount. You do not need an item per offer.
pos_reference identifies the offer for close and for reporting.
8. POST /partners/pos/close
Send once the check is paid and closed, for every check, even when check
failed, timed out or was never sent.
| Field | Type | Notes |
|---|---|---|
check_id | required | Same as on check. |
pos_transaction_id | string, required | Your unique id for this closed transaction. A repeated close with the same id is answered without doing anything twice. |
terminal_id | string | Logged only. |
closed_at | ISO 8601 with an offset (Z or +hh:mm) | When the POS closed the check, in the store's local time with its offset. The 15-minute hold is judged at this time (accepted up to 24 hours back and 5 minutes ahead of Spoonity's clock), and it dates the sale in the loyalty program. |
receipt.lines | array, required | The final lines, same shape and limits as on check. |
receipt.discounts | array, required | The Spoonity discounts you applied: { pos_reference, amount, name?, line_id?, component_id? }. For an order discount, send one entry per allocation or one entry for the whole amount. Empty array if you applied none. |
receipt.total | integer cents, required | The receipt total before tax. |
Example, for the check above:
{
"check_id": "20261103-104202",
"pos_transaction_id": "1042-20261103-104202",
"terminal_id": "02",
"closed_at": "2026-11-03T12:36:10-05:00",
"receipt": {
"lines": [
{ "line_id": "101", "sku": "2PCCOMBO", "name": "2 Pc Combo", "price": 1149, "quantity": 1,
"components": [
{ "component_id": "101-1", "sku": "2PC", "name": "2 Pc Chicken", "price": 899, "quantity": 1 },
{ "component_id": "101-2", "sku": "TATERS", "name": "Taters", "price": 399, "quantity": 1 },
{ "component_id": "101-3", "sku": "FOUNTAIN", "name": "Fountain Drink", "price": 279, "quantity": 1,
"modifiers": [{ "id": "LRG", "name": "Large", "price": 50, "quantity": 1 }] }
] },
{ "line_id": "102", "sku": "COOKIE", "name": "Cookie", "price": 199, "quantity": 1 }
],
"discounts": [
{ "pos_reference": "SPN-FREETATERS", "amount": 291, "line_id": "101", "component_id": "101-2" },
{ "pos_reference": "SPN-5OFF", "amount": 296, "line_id": "101", "component_id": "101-1" },
{ "pos_reference": "SPN-5OFF", "amount": 114, "line_id": "101", "component_id": "101-3" },
{ "pos_reference": "SPN-5OFF", "amount": 90, "line_id": "102" }
],
"total": 607
}
}
Response 200:
{ "committed": true, "redemption_id": "ored_5a58c78e-fbb4-4674-8bc5-827d72884dea", "evaluation_id": "2112901:20261103-104202" }
Spoonity answers before it posts the sale to the loyalty program, so close
stays fast. The POS does not need to wait for, or check, the loyalty side.
The customer is the one scanned on the check's check calls; a check with no
scanned code is recorded but not posted, since there is no one to credit.
If what you applied differs from what check returned, Spoonity still
commits (the money has already left the till) and reports the difference in
a mismatch object with the expected and received references and totals.
committed: false is still a 200, so the POS never retries it. The
reason says why:
reason | Meaning |
|---|---|
NO_PENDING_EVALUATION | Spoonity never priced this check, for example because the POS timed out and went ahead without a discount. No reward is used. The sale is still posted for points when an earlier check for it carried the scanned code. |
EVALUATION_EXPIRED | The 15-minute hold had ended when the check closed (by closed_at). The reward was not used. The sale is still posted for points. |
EVALUATION_CANCELLED | The check was voided before it closed. |
EVALUATION_ROLLED_BACK | The transaction was already voided. |
9. POST /partners/pos/void
Optional. Use it if the merchant's POS can cancel a check after check, or
refund a sale that had Spoonity discounts. Partial refunds are not
supported.
{ "check_id": "20261103-104202", "pos_transaction_id": "1042-20261103-104202" }
Response 200:
{ "voided": true, "redemption_id": "ored_5a58c78e-fbb4-4674-8bc5-827d72884dea", "evaluation_id": "2112901:20261103-104202", "state": "rolled_back" }
- Open check (
state: "cancelled"): the hold is released and the customer's reward is available again. You do not need this for a cancel out of Pay Now: the nextcheckreplaces the previous answer. - Closed check (
state: "rolled_back"): the reward and any usage limits are restored, and Spoonity reverses the sale in the loyalty program. state: "expired": the hold had already ended, so there was nothing to release.- An unknown check answers
{ "voided": false, "redemption_id": null, "reason": "NO_PENDING_EVALUATION" }.
10. Repeating calls safely
checkcan be repeated. The samecheck_idalways refers to the same check, and each call replaces the previous answer. If the new check no longer qualifies, the held reward is released.closecan be repeated with the samepos_transaction_id. A retry gets the same answer and nothing is committed or posted twice.voidcan be repeated with the samepos_transaction_id. A void may use the same transaction id as the close.- A closed check cannot be checked again (
409 CHECK_CLOSED). A new sale needs a newcheck_id. - After 10 restarts of one check (holds that expired, or voids while open),
checkanswers409 CHECK_REOPEN_LIMIT; use a newcheck_id.
11. Timeouts and failures
- Never block the cashier on Spoonity. If
checktimes out, returns a5xxor fails on the network, close the check without a Spoonity discount. - Timeout. Spoonity aims to answer
checkin under 300 ms. Your existing loyalty timeout works; a shorter one makes the cashier wait less when something is wrong. - Always send
close, even after a failed or skippedcheck. It is how Spoonity records the sale and posts the customer's points. Aclosethat never arrives means the customer earns no points for that sale. - Retrying
close. Bothcloseandvoidare safe to repeat. If the POS can retry them on a5xxor network error, it should. If it cannot,closeis still sent once and the cashier moves on. - Do not retry a
4xxexcept409 OFFERS_RESERVED(once) and429 RATE_LIMITED, which you retry after theRetry-Afterinterval.
12. Errors
Error bodies are { "error": { "code": "...", "message": "..." } }.
| Status | code | When | POS action |
|---|---|---|---|
| 400 | INVALID_REQUEST | The body failed validation (issues[] says where): a decimal amount, a duplicate line_id or component_id, an unknown field, a colon in check_id, modifiers on a combo line, a limit exceeded. | Fix the request. Close without a discount. |
| 401 | UNAUTHORIZED | Missing or unknown API key. | Configuration problem. Close without a discount. |
| 409 | CHECK_CLOSED | check for a check that already closed or was refunded. | Use a new check_id for a new sale. |
| 409 | CHECK_REOPEN_LIMIT | The check was restarted too many times. | Use a new check_id. |
| 409 | OFFERS_RESERVED | Another check is using the same reward at that moment. Rare. | Send check once more, then close without a discount. |
| 413 | PAYLOAD_TOO_LARGE | The request body is over 256 KB. | Fix the request. Close without a discount. |
| 429 | RATE_LIMITED | Too many requests with this key. The Retry-After header gives the seconds to wait. | Slow down and retry after Retry-After. If it does not clear, close without a discount. |
| 503 | OFFERS_UNAVAILABLE | Spoonity could not read the merchant's promotions or look up the customer. | Close without a discount. |
| 500 | INTERNAL | An error on Spoonity's side. | Close without a discount. |
13. Testing
Spoonity provides a test merchant and its API key on
https://panasonic.spoonity.com, with a test customer code and rewards set
up for the scenarios below.
- Guest check with no code:
check, thenclose. - Code scanned, reward picked in the app: discount returned on the right
item and applied,
closecommits. - Reward on a part of a combo: discount lands on that part.
- Order discount: allocations applied to their items and add up.
- Cancel out of Pay Now, change the order, Pay Now again: the second
checkreplaces the first. - Spoonity unreachable: the check closes without a discount, and
closeis still sent. - Modifiers with and without a charge.
- A POS discount (employee or manager) on a line next to a Spoonity discount.
14. Checklist
- Base URL and merchant API key in POS configuration, key sent as
Authorization: Bearer. - Spoonity code prefix configured as a loyalty code.
-
check_idisYYYYMMDD-<order number>. -
checksent at every Pay Now, with the whole check. - Combos sent with
components; modifiers on their item or part. - Every Spoonity discount applied to its item or part.
-
closesent for every check, with the discounts actually applied. - Amounts in integer cents, before tax.
For AI coding assistants
This guide is also available as plain markdown at /docs/index.md, and as one file at /docs/llms-full.txt. An index for language models is at /llms.txt.