Credits
Hyvä POS is in closed beta
Hyvä POS is currently in a closed beta (pilot phase) with a small group of merchants. It is not yet generally available: the App Store release follows the pilot, and features and configuration may still change, possibly in backwards-incompatible ways, before the general release. Want to take part? Sign up at hyva.io/pos.
| Method | Route | Purpose |
|---|---|---|
GET |
/V1/pos/credits/balance/:customerId |
All balances: store credit, gift cards, reward points |
GET |
/V1/pos/credits/giftcard/:code |
Validate a gift-card code |
POST |
/V1/pos/credits/applicable |
What a cart may spend of one credit kind (since module 1.3.0) |
POST |
/V1/pos/credits/refund |
Refund an amount back to a credit balance |
POST |
/V1/pos/orders/:orderId/due-payments |
Collect the credit due an order still owes (since module 1.3.0) |
GET |
/V1/pos/orders/:orderId/points-earned |
The reward points the vendor credited for an order (since module 1.3.0) |
All routes require the Hyva_Pos::locations ACL resource. The service aggregates every registered credit provider. A provider is a bridge module implementing CreditProviderInterface (Adobe Commerce store credit and gift cards, Amasty, or your own), registered with the CreditProviderPool via di.xml.
Get Balances
GET /V1/pos/credits/balance/:customerId: all credits for a customer across every active provider. The POS calls it when a customer is attached to the cart, to show redeemable balances at the register.
Response: one row per credit. Currency-based credits (store credit, gift cards) carry balance in currency and leave the points fields null; point-based credits carry balance in points plus the conversion fields:
[
{
"type": "store_credit",
"provider": "adobe_commerce",
"label": "Store Credit",
"balance": 25.0,
"currency": "EUR",
"code": null,
"is_redeemable": true,
"currency_value": null,
"points_per_currency": null,
"expires_at": null,
"details": null
},
{
"type": "loyalty_points",
"provider": "adobe_commerce",
"label": "Reward Points",
"balance": 480.0,
"currency": null,
"code": null,
"is_redeemable": true,
"currency_value": 4.8,
"points_per_currency": 100.0,
"expires_at": "2026-12-31T00:00:00+00:00",
"details": null
}
]
Error behavior: a provider that throws is skipped and the remaining providers still report. The route itself only fails on auth or transport errors.
Check a Gift Card
GET /V1/pos/credits/giftcard/:code: validates a gift-card code through the provider that declares the gift_card feature. The POS calls it when the cashier scans or types a card at checkout. URL-encode the code segment.
Response: a single balance row in the same shape as above, with type: "gift_card" and code set to the card code.
Error behavior: 404 with Gift card with code "X" not found. when no gift-card provider is installed, the code does not exist, or the provider lookup fails.
Applicable Credit
POST /V1/pos/credits/applicable answers what a cart may spend of one credit kind, from the active provider of that kind, before the register takes the tender and again just before payment. Module 1.3.0 added it and removed POST /V1/pos/credits/apply, which nothing called: credit is applied inside the order the register places, and the module debits the wallet during the order save through the provider's debit methods. The provider side is described under Building Bridges.
Request: the cart basis the register rang, in the cart's currency. The register creates no quote to price a cart, so this is plain cart data.
| Field | Meaning |
|---|---|
credit_type |
store_credit, gift_card or reward_points |
customer_id |
The customer on the cart |
code |
The gift-card code, for gift_card |
store_id |
The store view |
subtotal, subtotal_incl_tax, discount_amount, discount_tax_compensation_amount, shipping_amount, shipping_incl_tax, shipping_discount_amount, shipping_discount_tax_compensation_amount, tax_amount, grand_total |
The cart totals |
lines[] |
Per line: sku, qty, row_total, row_total_incl_tax, discount_amount |
applied_credits[] |
Credits already on the cart, so the maximum is capped at what the cart still owes |
Answer: one of three outcomes. supported carries the figures; unsupported (no provider for the kind, or one that does not answer this kind's rules) and failed (the read errored) allow no spending.
| Field | Meaning |
|---|---|
outcome |
supported, unsupported or failed |
credit_type, provider_code, code |
What was asked, and which provider answered |
applies_as |
tender or discount: how the kind reduces what the customer pays |
balance |
The wallet balance, in the cart's currency |
max_amount, min_amount |
What may be spent on this cart, after the store's rules and the amount still owed |
min_points, max_points, points_rate, points_granularity, amount_step, round_rule |
Reward points: the thresholds, the rate, whole or fractional points, and the step an amount must land on |
is_redeemable |
Whether anything may be spent at all |
reason |
The store's reason when a rule reduces or refuses the spend |
lines[] |
Per line: sku, is_eligible (whether the line may be paid with this kind) and reason (why not, or null) |
Error behavior: the route answers 200 with outcome set to unsupported or failed rather than an HTTP error, so the register can say why and offer another tender.
Refund to Credit
POST /V1/pos/credits/refund: pushes an amount back onto a credit balance through CreditServiceInterface::refund, for integrations that need to hand value back outside a return. Nothing in the module or the register calls it: a return's refund to store credit is booked on the return's credit memo through ReturnMemoSettlement, which the settle, the built-in provider and the RMA bridges share, and the store-credit provider confirms it after the refund call.
Request:
{
"orderId": "2043",
"creditType": "store_credit",
"provider": "",
"amount": 25.0,
"customerId": 1287,
"reason": "Return 000000018 refunded to store credit",
"code": null
}
orderIdis the order entity id as a string; the server resolves it numerically to enforce the refund guard.- An empty
providermeans "the active provider for this credit type"; the RMA settle paths pass""because they have no provider code in hand. codecarries the gift-card code for gift-card refunds.
Response: a CreditApplyResultInterface with success, amount_applied, remaining_balance, transaction_id, error_message.
Over-Refund Guard
Credit issued back for an order is cumulative across all calls and may never exceed the order's total_refunded (which Magento itself caps at total_paid). The module tracks issued credit in a per-order ledger (pos_credit_refunded); credit issued through other paths (for example the memo-time refund-to-credit bridge) is folded into the same ledger. The guard fails closed:
- Amount exceeds the remaining refundable amount:
200withsuccess: falseandRefund amount X exceeds the remaining refundable amount Y for this order. - Order id cannot be resolved:
200withsuccess: falseandOrder "X" not found - cannot validate the refund amount. - No provider installed for the credit type:
200withsuccess: falseandNo credit provider is installed for "store_credit".
A ledger write failure after a successful provider refund is logged but does not fail the call; the provider's own transaction record is the authoritative trail.
Collect Credit Due
POST /V1/pos/orders/:orderId/due-payments: records a payment towards the remainder a register order owes because a wallet gave less than the sale asked when the order was booked (an offline sale whose store credit, gift card or points came up short at the order save; the order's pos_credit_due). The register calls it when the cashier takes that remainder on the same order.
Under the order mutex the remaining due is read again (the lower of pos_credit_due and total_due), and an amount above it is refused. Only this payment is recorded: total_paid and base_total_paid rise by the amount, pos_credit_due falls by it, and one status-history comment names the payment, its reference and what is left. When nothing is left the open invoice is marked paid without adding its total to the paid total again, and the order's state is Magento's. Partial collection is allowed. No sale, invoice, shipment, stock movement, wallet debit or layaway transition comes of it, and a failure anywhere in the booking rolls it back whole.
reference is required and is the idempotency key: the same reference again, after a lost answer or sent at the same time, books nothing and answers the first booking.
{
"order_id": 2043,
"order_increment_id": "000002043",
"reference": "S-T2-000432",
"amount": 12.5,
"is_replay": false,
"remaining_due": 0.0,
"paid_total": 118.9,
"total_due": 0.0,
"is_invoice_paid": true
}
Errors: 400 (InputException) for a refused request (no reference, an amount above what is due); 404 for an unknown order.
Points Earned on an Order
GET /V1/pos/orders/:orderId/points-earned: the active reward-points provider's answer for what the order earned. Nothing is earned by this read.
{
"order_id": 2043,
"provider_code": "amasty_rewards",
"status": "earned",
"points": 119.0,
"points_earned": 119.0,
"balance_after": 599.0,
"currency_value": 1.19,
"points_voided": 0.0,
"points_granularity": "whole",
"reason": null
}
status is earned (with the points the vendor credited), pending (0 before the vendor's earning ran: Amasty earns when the order completes, Adobe Commerce when it is paid in full) or unsupported (0, no provider can read it back; a provider that throws answers unsupported with the reason). points_voided counts earned points taken back since by refunds. balance_after and currency_value are null when they cannot be read. Errors: 404 for an unknown order.