Skip to content

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
}
  • orderId is the order entity id as a string; the server resolves it numerically to enforce the refund guard.
  • An empty provider means "the active provider for this credit type"; the RMA settle paths pass "" because they have no provider code in hand.
  • code carries 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: 200 with success: false and Refund amount X exceeds the remaining refundable amount Y for this order.
  • Order id cannot be resolved: 200 with success: false and Order "X" not found - cannot validate the refund amount.
  • No provider installed for the credit type: 200 with success: false and No 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.

{
  "amount": 12.5,
  "paymentMethod": "cash",
  "reference": "S-T2-000432"
}

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.