Hoppa till innehållet
Utvecklare

Kassaintegration, flöde för flöde.

Allt en kassa gör med Custas, i den ordning det händer: känna igen eller registrera en medlem, prissätta kundkorgen, ta emot presentkort och poäng, slutföra köpet med kvittot och hantera returen.

Den här guiden handlar om ordning och syfte. Referensen gäller för varje enskilt fält. Exemplen är nedkortade till det som spelar roll för steget, och beloppen är bara exempel.

Före första köpet

En API-nyckel med kassans scopes

Skapa en nyckel per butik eller kassainstallation i portalen och skicka den som Authorization: Bearer cust_live_…. En cust_test_…-nyckel fungerar mot en helt separat testmiljö där inga meddelanden skickas på riktigt, så bygg och certifiera mot den.

Scope Används till
members:read Uppslag, medlemspanel, poäng, stämpelkort, Wallet-länkar
members:write Registrering, nya identifierare, registrering av samtycke
carts:evaluate Prissättning
redemptions:read Hitta ett köp vid retur, status för digitalt kvitto
redemptions:write Reservera, slutföra, släppa, returnera, återföra; tjäna och lösa in poäng; skicka ett Kivra-kvitto
giftcards:read Saldo på presentkort
giftcards:write Utfärda och fylla på kort; fristående inlösen och återbetalning
settings:read Identitetsregler och samtyckestexter, läses vid start
messaging:write (valfri) Mejl för dubbelt samtycke (double opt-in), mejla ett presentkort som sålts

Konventioner som kassan måste följa

  • Belopp anges som heltal i minsta enhet. 19900 är 199,00 kr. Procent anges i baspunkter: 2500 är 25 %. Skicka eller räkna aldrig med decimaler.
  • Priser är inklusive moms om inte handlaren har ställt om till netto (GET /v1/tenant).
  • Skriv ut den momsfördelning som Custas returnerar. Räkna inte om netto och moms per rad: två oberoende avrundningar kommer förr eller senare att skilja sig åt, och skillnaden hamnar i en momsdeklaration.
  • Idempotency-Key (8 till 255 tecken, en per logisk operation) krävs vid reservation, slutförande, släpp, återföring och returer, och när presentkort utfärdas eller mejlas. Ett nytt försök med samma nyckel och body spelar upp det första svaret igen. Skapa nyckeln när kassören trycker på knappen, och behåll den tills du får ett svar.
  • Fel är RFC 9457 problem+json. Förgrena på code, aldrig på title.

Läs in handlarens konfiguration vid start

GET /v1/tenant/identity-rules
GET /v1/consent-wordings

Identitetsreglerna anger vilka identifierare handlaren accepterar (email, phone, personnummer, member_no, card_no, pan_alias, …), vilka som är unika och vilka som krävs vid registrering. Bygg registreringsformuläret utifrån dem: en typ utan regel nekas.

Samtyckestexterna är de exakta texter en kund får visas när hen tillfrågas om samtycke till marknadsföring. Cacha den aktiva texten per kanal, ändamål och språk. Du behöver dess id när du registrerar samtycket.

1. Identifiera kunden

Kunden skannar ett medlemskort, ett Wallet-kort eller en QR-kod i appen, eller ger kassören sitt telefonnummer eller personnummer.

POST /v1/members/lookup

{ "keyType": "phone", "value": "070-123 45 67" }

# → 200
{
  "id": "0199a4f0-1c2d-7e3f-8a4b-5c6d7e8f9a0b",
  "status": "active",
  "displayName": "Anna Svensson",
  "identities": [
    { "keyType": "phone", "displayHint": "+4670*****67", "isPrimary": true },
    { "keyType": "personnummer", "displayHint": "19811218-****", "isPrimary": false }
  ],
  "tags": [{ "name": "VIP", "color": "gold" }],
  "pointsBalance": 1240,
  "orderCount": 17,
  "lifetimeSpendMinor": 1245000
}
  • Uppslaget är en POST, så att ett personnummer aldrig syns i en URL eller en åtkomstlogg. Alla skrivsätt matchar: 811218-9876 och 198112189876 är samma person, och telefonnummer normaliseras.
  • 404: inte medlem. Erbjud registrering.
  • 429 på personnummer: för många missar från den här nyckeln den senaste timmen. Använd en annan identifierare i stället.
  • status:
    • active: en vanlig medlem.
    • blocked: prissätts som gäst och varken tjänar eller använder något. Behandla medlemmen som anonym.
    • restricted: fortfarande medlem i kassan, men får inga meddelanden eller erbjudanden.
  • Identifierare returneras bara som maskerade displayHint. Visa dem; be aldrig om hela värdet.

Spara id under resten av transaktionen. Du kan också hoppa över uppslaget och skicka den skannade identifieraren direkt i varukorgen, men om du slår upp först kan kassören hälsa på kunden och se saldot.

Medlemspanelen

GET /v1/members/{id}/points        # saldo, vad det är värt nu, nivå, poäng som snart förfaller
GET /v1/members/{id}/stamp-cards   # stämplar per kort och oanvända belöningskoder

Stämpelkortsbelöningar med status: "active" är koder som medlemmen har tjänat in. Visa dem så att kassören kan erbjuda ”du har en gratis kaffe, vill du använda den nu?”. För att använda en lägger du till dess code i varukorgen.

Personliga erbjudandekoder tillämpas inte automatiskt. Medlemmen visar dem i medlemsappen, på sitt Wallet-kort eller i ett mejl, där var och en har en QR-kod och en Code 128-streckkod. Skanna in dem i varukorgens codes.

2. Registrera en ny medlem i kassan

Registrering: kassan slår upp kunden, får 404, skapar medlemmen, registrerar samtycke om det ges och visar en QR-kod för Wallet-kortet. Kund Kassa Custas POST /v1/members/lookup 404 not_found telefon, e-post, samtycke? POST /v1/members 201 medlem PUT …/consents/email/marketing GET …/wallet/google Wallet-QR på displayen bara om kunden sa ja

Skapa medlemmen

POST /v1/members

{
  "displayName": "Anna Svensson",
  "locale": "sv-SE",
  "country": "SE",
  "identities": [
    { "keyType": "phone", "value": "0701234567", "isPrimary": true },
    { "keyType": "email", "value": "Anna.Svensson@example.com" }
  ],
  "attributes": { "signupStore": "uppsala-01", "signupTill": "4" }
}
  • Skicka minst en identifierare, och alla identifierare som identitetsreglerna anger som obligatoriska.
  • Ett personnummer sätter också birthDate, som födelsedagskampanjer använder. Annars kan du skicka birthDate själv.
  • 409 identity_conflict: en unik identifierare tillhör redan någon. Svaret säger medvetet inte vem. Slå upp kunden med samma identifierare och fortsätt med den medlemmen; skapa inte en till.
  • 422: en identifierartyp som handlaren inte accepterar, eller ett felaktigt värde. errors[].field pekar ut det.

Välkomstautomationer som handlaren har satt upp (ett välkomstmejl, en välkomstkod) körs av sig själva. Kassan behöver inte göra något mer.

Registrera samtycke till marknadsföring, bara om kunden sa ja

Medlemskap och samtycke till marknadsföring är skilda saker: att gå med i programmet får inte innebära samtycke. Fråga, och registrera bara ett uttryckligt ja.

PUT /v1/members/{id}/consents/email/marketing

{
  "state": "granted",
  "source": "pos",
  "sourceDetail": "till 4, Uppsala",
  "wordingId": "0199a4f1-…",   # samtyckestexten som visades på kunddisplayen
  "actor": "staff:1043"        # kassören
}
  • Kanalen är email, sms, push, post eller phone; ändamålet är marketing eller profiling. Varje par är en egen post.
  • Skicka alltid wordingId. Ett samtycke som inte kan visa vad personen fick veta är ett svagt bevis.
  • För det starkaste beviset skickar du i stället ett dubbelt samtycke (double opt-in), och kunden bekräftar via en länk: POST /v1/members/{id}/consent-confirmations med channel, purpose, templateKey och wordingId.
  • Ett återkallande ("state": "withdrawn") nekas aldrig. Erbjud det i kassan också.

Ge kunden sitt kort

GET /v1/members/{id}/wallet/google   # → { "saveUrl": "https://pay.google.com/…" }
GET /v1/members/{id}/wallet/apple    # → .pkpass-fil

Visa Googles saveUrl som en QR-kod på kunddisplayen. Kortet uppdaterar sig självt när poäng, nivå och namn ändras, och det är dess streckkod du skannar nästa gång.

3. Prissätt varukorgen

Anropa POST /v1/carts/evaluate varje gång kundkorgen ändras: en rad läggs till eller tas bort, ett antal ändras, en medlem identifieras eller en kod skannas. Anropet läser bara och reserverar ingenting, så det är ofarligt att anropa ofta.

POST /v1/carts/evaluate?explain=true

{
  "currency": "SEK",
  "channel": "pos",
  "country": "SE",
  "storeId": "uppsala-01",
  "member": { "memberId": "0199a4f0-1c2d-7e3f-8a4b-5c6d7e8f9a0b" },
  "codes": ["WELCOME50"],
  "lines": [
    {
      "id": "L1", "sku": "TSH-SPORT-2PK", "quantity": 2,
      "unitPriceMinor": 19900, "taxRateBp": 2500,
      "categories": ["clothing"], "brands": ["Acme"],
      "description": "Sport T-shirt (2-pack)"
    },
    {
      "id": "L2", "sku": "COF-LATTE", "quantity": 1,
      "unitPriceMinor": 4500, "taxRateBp": 1200,
      "categories": ["cafe"],
      "description": "Caffe latte"
    }
  ]
}

Identifiera medlemmen

Använd { "memberId": … } efter ett uppslag, eller skicka den skannade identifieraren direkt som { "keyType": "card_no", "value": "…" }. För att känna igen en medlem på kortet hen betalar med skickar du betalleverantörens kortalias (Adyens alias) som { "keyType": "pan_alias", "value": "M469509594859802" } — aldrig själva kortnumret, som nekas. Kampanjer för bara medlemmar, för nivåer och för födelsedagar gäller bara när en medlem är identifierad.

Regler för rader

  • id måste vara stabilt så länge kundkorgen finns. Det är fördelningsnyckeln: om det ändras mellan anropen kan en rabatt flytta mellan rader.
  • Skicka allt som kampanjer kan rikta sig mot: categories, collections, brands, tags, isSale, productId, variantId. En kampanj på ”kläder” kan inte matcha en rad som inte säger att den är kläder.
  • excludeFromDiscounts: true gäller tobak, pant och allt annat som lagen eller butiken håller utanför rabatter.
  • isGiftCard: true markerar ett presentkort som säljs. Det rabatteras aldrig.
  • description krävs om handlaren skickar digitala kvitton.
  • Varor som säljs per vikt: quantity: 1, unitPriceMinor är priset för den vägda mängden, och measure: { "value": 0.532, "unit": "kg" }.

Offerten

{
  "quoteHash": "7f3a9c…e21b",          # spara den: du reserverar mot den
  "memberId": "0199a4f0-1c2d-7e3f-8a4b-5c6d7e8f9a0b",
  "subtotalMinor": 44300,
  "totalDiscountMinor": 8980,
  "totalMinor": 35320,
  "amountDueMinor": 35320,
  "lines": [
    { "lineId": "L1", "baseMinor": 39800, "discountMinor": 8422, "totalMinor": 31378,
      "netMinor": 25102, "vatMinor": 6276,
      "allocations": [
        { "promotionId": "…", "amountMinor": 3980, "netMinor": 3184, "vatMinor": 796 },
        { "promotionId": "…", "codeId": "…", "amountMinor": 4442, "netMinor": 3554, "vatMinor": 888 }
      ] },
    { "lineId": "L2", "baseMinor": 4500, "discountMinor": 558, "totalMinor": 3942,
      "netMinor": 3520, "vatMinor": 422, "allocations": [ … ] }
  ],
  "appliedPromotions": [
    { "reference": "Members: 10% off clothing", "klass": "product", "amountMinor": 3980 },
    { "reference": "Welcome 50 kr", "codeId": "…", "klass": "order", "amountMinor": 5000 }
  ],
  "rejectedCodes": [],
  "vatBreakdown": [
    { "taxRateBp": 2500, "netMinor": 25102, "vatMinor": 6276, "grossMinor": 31378 },
    { "taxRateBp": 1200, "netMinor": 3520, "vatMinor": 422, "grossMinor": 3942 }
  ]
}
Fält Kassan
lines[].totalMinor, discountMinor Visar varje rads pris efter rabatt, på skärmen och kvittot
appliedPromotions[] Skriver ut en rad per erbjudande: ”Welcome 50 kr −50,00”
rejectedCodes[] Berättar för kassören varför en kod inte gällde. Resten av varukorgen prissätts ändå.
vatBreakdown Skriver ut den ordagrant som kvittots momssammanställning
amountDueMinor Debiterar det som den del som betalas med pengar
quoteHash Sparar det till reservationen

?explain=true lägger till ett spår för varje kampanj: tillämpad, hoppad över med en anledning, eller inte prövad (till exempel needs_code eller wrong_channel). Använd det för att svara på ”varför gällde inte mitt erbjudande?” och för att driva merförsäljningsförslag. Utelämna det i den prestandakritiska vägen om du inte använder det.

Koder kan skannas eller skrivas in. Versaler, mellanslag, bindestreck och de förväxlingsbara I/L/O normaliseras, så welcome-50 och WELCOME 5O matchar båda WELCOME50.

Referens: prissätt en varukorg

4. Presentkort och poäng som betalning

Presentkort och poäng är betalmedel, inte rabatter. De dras efter alla rabatter och minskar bara amountDueMinor, aldrig en rad eller dess moms. Lägg dem i samma varukorg:

POST /v1/carts/evaluate

{
  …the same cart…,
  "tenders": [
    { "type": "gift_card", "code": "GC-7Q4M-XK2P-9TND" },
    { "type": "points", "points": 500 }
  ]
}

# → i offerten
"amountDueMinor": 20320,
"tenders": [
  { "position": 0, "type": "gift_card", "status": "applied", "lastFour": "9TND",
    "amountMinor": 10000, "remainingMinor": 0 },
  { "position": 1, "type": "points", "status": "applied", "points": 500,
    "amountMinor": 5000, "remainingPoints": 740 }
]
  • Betalmedel dras i den ordning de skickas, upp till fem. Utelämna amountMinor eller points för att använda så mycket som återstår att betala.
  • Ett nekat betalmedel betalar ingenting och anger varför i reason: not_found, expired, no_balance, no_member, below_step med flera. Köpet kan ändå genomföras.
  • Skriv ut remainingMinor och remainingPoints på kvittot.
  • För att kontrollera ett kort innan det används som betalning anropar du POST /v1/gift-cards/lookup med { "code": "…" }. Det som går att använda just nu är balanceMinor minus heldMinor.
  • Vad medlemmens poäng är värda: redeemablePoints och redeemableMinor i GET /v1/members/{id}/points.

Presentkortskoder fungerar som kontanter: den som har koden kan använda den. Logga dem aldrig, och skriv aldrig ut dem i sin helhet.

5. Betalning: reservera, betala, slutför

Betalning: kassan prissätter, reserverar mot offertens hash, debiterar via betalterminalen och slutför sedan. Ett 409 innehåller en ny offert att reservera mot igen; en misslyckad betalning släpper reservationen. Betalterminal Kassa Custas POST /v1/carts/evaluate offert · quoteHash POST /v1/redemptions 201 held · amountDueMinor 409? Visa den bifogade offerten, reservera igen på dess quoteHash debitera amountDueMinor godkänd POST …/{id}/commit 200 committed Nekad? POST …/{id}/release

Reservera när kassören trycker på ”Betala”

POST /v1/redemptions
Idempotency-Key: till4-20261020-000731-reserve

{
  "cart": { …exactly the cart you last evaluated, tenders included… },
  "quoteHash": "7f3a9c…e21b",
  "externalRef": "UPS01-T4-000731",
  "ttlSeconds": 600,
  "staff": { "id": "1043", "name": "Erik" }
}
  • Custas prissätter varukorgen på nytt på servern och jämför med quoteHash. Din offert används aldrig som indata.
  • Vid 201 (status: "held") läggs varje kodanvändning, kampanjbudget, presentkortsbelopp och poäng i offerten undan, och ingen annan kassa kan ta dem. Debitera reservationens amountDueMinor.
  • externalRef är ditt kvitto- eller transaktionsnummer. Det är unikt per handlare, och det är så du hittar köpet igen vid en retur.
  • ttlSeconds är 30 till 3 600, 900 som standard. Om kassan kraschar löper reservationen ut och allt återställs av sig självt.

En och samma väg tillbaka för varje 409

quote_stale, code_exhausted, budget_exhausted, member_limit_reached och tender_unavailable betyder alla att varukorgen inte längre prissätts som i offerten. Alla innehåller den aktuella offerten:

{
  "type": "https://app.custas.se/docs/problems/code-exhausted",
  "status": 409,
  "code": "code_exhausted",
  "title": "Code has no redemptions left",
  "quote": { "quoteHash": "9b21d4…07ac", "totalMinor": 40320, "rejectedCodes": [ … ], … }
}

Visa den nya totalsumman, låt kunden godkänna den och reservera igen mot quote.quoteHash med en ny Idempotency-Key. Förgrena inte på vilken av de fem koderna du fick: när två kassor tävlar om den sista användningen av en kod är det tajmingen som avgör vilken förloraren ser, inte betydelsen.

Ta betalt, eller släpp reservationen

Debitera amountDueMinor med kort, kontanter, Swish och så vidare. Om betalningen misslyckas eller kunden går därifrån släpper du reservationen direkt i stället för att vänta på att den löper ut. Då blir den sista användningen av en kod ledig för nästa kund i kön.

POST /v1/redemptions/{id}/release
Idempotency-Key: till4-20261020-000731-release

{ "reason": "cancelled" }

Slutför (commit) när betalningen har gått igenom

POST /v1/redemptions/{id}/commit
Idempotency-Key: till4-20261020-000731-commit

{
  "receipt": {
    "number": 731,
    "purchasedAt": "2026-10-20T12:04:11+02:00",
    "store": {
      "name": "Acme Uppsala",
      "organizationNumber": "556677-8899",
      "unitId": "uppsala-01",
      "address": { "streetAddress": "Kungsgatan 1", "postalCode": "753 21",
                   "locality": "Uppsala", "country": "SE" }
    },
    "register": { "id": "T4", "serialNumber": "CR-0042-1187" },
    "controlUnit": { "id": "KE-118833", "code": "A1B2C3D4E5" },
    "cashier": { "id": "1043", "name": "Erik" },
    "roundingMinor": -20
  },
  "payments": [
    { "method": "card", "amountMinor": 20300,
      "card": { "maskedPan": "535583******7361", "brand": "Mastercard",
                "approvalCode": "123456", "terminalId": "TID0042",
                "token": { "scheme": "par", "value": "5001…" } } }
  ]
}
  • payments måste summera till amountDueMinor plus receipt.roundingMinor (öresavrundning), annars får du 422 och ingenting slutförs. Presentkort och poäng finns redan bland reservationens betalmedel; lista dem inte igen.
  • roundingMinor ligger mellan -100 och 100: högst en hel krona (eller annan valutaenhet) åt vardera hållet.
  • Skicka aldrig ett fullständigt kortnummer: det nekas. Skicka maskerat PAN eller BIN, och PAR-token om din terminal ger en. Med en PAR-token kan ett digitalt kvitto hitta en kund som betalade med kort utan att identifiera sig.
  • receipt och payments ändrar inte priset, men det är av dem det digitala kvittot (Kivra) och medlemmens kvittohistorik byggs. Skicka dem.
  • 409 reservation_not_held: reservationen har löpt ut eller släppts. Prissätt och reservera igen innan du skriver ut.
  • Tappat svar? Gör ett nytt försök med samma Idempotency-Key. Samtidiga slutföranden av samma reservation leder till exakt ett lyckat.

Identifierad medlem, ingen rabatt

Reservera och slutför ändå. Det är så medlemmen tjänar poäng och stämplar, och så köpet räknas in i medlemmens antal köp och totala köpbelopp. Bara ett anonymt köp utan inlösen nekas (409 conflict); för sådana anropar du inte Custas.

Referens: redemptions

6. Vad som händer efter slutförandet

Kassan gör ingenting för något av detta. Det sker när köpet slutförs:

Vad Var det syns
Poäng på den del som betalas med pengar (inte på använda poäng, inte på köpta presentkort) GET /v1/members/{id}/points, points.earned
Uppgraderad nivå tier.changed
Stämplar läggs till, och en belöningskod utfärdas när ett kort blir fullt GET /v1/members/{id}/stamp-cards, stamps.rewarded
Kodanvändningar och kampanjbudget förbrukas redemption.committed
Digitalt kvitto till medlemmens Kivra-brevlåda, om handlaren använder Kivra och medlemmen har ett läsbart personnummer GET /v1/redemptions/{id}/kivra
Kvittot i medlemsappen Medlemmens egen kvittolista

För att skriva ut ”Du har nu 1 740 poäng” eller ”2 stämplar kvar till en gratis kaffe” läser du GET /v1/members/{id}/points och GET /v1/members/{id}/stamp-cards efter slutförandet.

Om ett Kivra-kvitto misslyckades eller hoppades över köar POST /v1/redemptions/{id}/kivra det igen. Ett kvitto som redan har skickats nekas, så kunden får aldrig två exemplar.

7. Returer och makuleringar

Hitta det ursprungliga köpet

Skanna kvittots streckkod och slå sedan upp dess nummer:

GET /v1/redemptions?externalRef=UPS01-T4-000731

Köpets lines[] innehåller fördelningen som den var vid köpet (rabatt, netto och moms per rad), plus returnedQuantity hittills.

Delretur: några enheter lämnas tillbaka

POST /v1/redemptions/{id}/returns
Idempotency-Key: till2-20261102-000088-return

{
  "lines": [{ "lineId": "L1", "quantity": 1 }],
  "reason": "wrong size",
  "receipt": { "number": 88, "purchasedAt": "2026-11-02T15:30:00+01:00",
               "store": { "name": "Acme Uppsala", "unitId": "uppsala-01" },
               "register": { "id": "T2" }, "cashier": { "id": "2001" } },
  "payments": [{ "method": "card", "amountMinor": 15689,
                 "card": { "maskedPan": "535583******7361" } }]
}
  • Varje enhet återbetalas med sin andel av raden som den såldes, rabatt inräknad, aldrig till dagens pris. Läs beloppet, med netto och moms, från den nya posten i svarets returns; räkna inte ut det själv.
  • Poängen som enheterna gav dras tillbaka, och medlemmens totala köpbelopp minskar.
  • En delretur tar inte tillbaka koden eller kampanjen, eller de presentkort och poäng som användes för betalningen. Återbetala det värdet via payments, eller sätt tillbaka det på kortet med POST /v1/gift-cards/{id}/refund.
  • 422: fler enheter tillbaka än som såldes. 409: köpet har redan återförts.

Fullständig återföring: en makulering eller retur av hela kvittot

POST /v1/redemptions/{id}/reverse
Idempotency-Key: till2-20261102-000088-reverse

{ "reason": "return, receipt 88" }

Det här tar tillbaka allt. Kodanvändningar kommer tillbaka (en förbrukad kod blir aktiv igen), kampanjbudgeten krediteras, och presentkort och poäng som användes för betalningen återbetalas till kortet och medlemmen. En andra återföring ändrar ingenting, och efter delreturer tar en återföring bara tillbaka det som återstår.

8. Sälja och fylla på presentkort

Sälj ett nytt kort

  1. Lägg kortet i kundkorgen som en rad med "isGiftCard": true, och prissätt, reservera, betala och slutför som vanligt. Ingen kampanj ger rabatt på det.
  2. Efter slutförandet utfärdar du kortet för det sålda beloppet. Svaret innehåller code en enda gång: skriv ut den eller koda in den på kortet. Ett nytt försök med samma nyckel returnerar samma kort i stället för att ladda ett till.
  3. Du kan också mejla det som en present med POST /v1/gift-cards/{id}/deliveries.
POST /v1/gift-cards
Idempotency-Key: till4-20261020-000731-gc-L3

{
  "amountMinor": 50000,
  "memberId": "0199a4f0-…",
  "note": "receipt UPS01-T4-000731",
  "code": "6012345678901234"   # bara för ett förtryckt kort; utelämna för att generera en kod
}

Fyll på ett befintligt kort

Sälj påfyllningen som en isGiftCard-rad, slutför och sedan:

POST /v1/gift-cards/lookup        { "code": "…" }   # → id
POST /v1/gift-cards/{id}/top-up   { "amountMinor": 20000, "externalRef": "UPS01-T4-000732" }

externalRef gör det säkert att försöka igen: samma kvitto fyller på en gång. Ett inaktiverat kort svarar 409, ett utgånget 410.

9. Kassor som bara kan göra ett anrop

Vissa kassor kan bara anropa en gång, efter köpet. De kan använda de här fristående endpoints, som var och en är säker att anropa igen med samma kvittonummer. De reserverar ingenting, så en annan kassa kan använda samma saldo i samma ögonblick: välj reservera och slutför där du kan.

POST /v1/members/{id}/points/earn     { "amountMinor": 35320, "externalRef": "UPS01-T4-000731" }
POST /v1/members/{id}/points/redeem   { "points": 500, "externalRef": "UPS01-T4-000731" }
POST /v1/gift-cards/redeem            { "code": "…", "amountMinor": 10000,
                                    "externalRef": "UPS01-T4-000731", "allowPartial": true }
POST /v1/gift-cards/{id}/refund       { "amountMinor": 10000, "externalRef": "UPS01-T4-R88" }

Kvittonummer delas med reservationer: ett kvitto som redan har slutförts via reservera och slutför har fått sina poäng, och skickas det till points/earn returneras den intjäningen i stället för att poängen tjänas två gånger.

Kampanjer och koder kan inte tillämpas på det här sättet. De kräver prissättning, reservation och slutförande.

10. Backoffice: webhooks

Kassan själv behöver inga webhooks. Ett backoffice för kassasystemet vill oftast spegla det som händer i Custas:

POST /v1/webhook-endpoints

{
  "url": "https://pos.example.com/hooks/custas",
  "eventTypes": ["redemption.committed", "redemption.reversed", "redemption.returned",
                 "member.created", "member.updated", "member.merged", "member.deleted",
                 "points.earned", "tier.changed", "gift_card.issued", "gift_card.redeemed"]
}
  • Signeringshemligheten returneras en gång. Verifiera varje leverans: custas-signature: v1=<hex> är HMAC-SHA256 över <custas-timestamp>.<raw body>, beräknad på de bytes du tog emot.
  • Leverans sker minst en gång, så ta bort dubbletter med custas-event-id.
  • member.merged och member.deleted spelar roll om du cachar medlems-id:n. Ett sammanslaget id svarar 404 med mergedInto, det id som ska användas i stället. En raderad medlem måste försvinna ur din cache också.

11. Fel och offline

Svar Var Kassan
404 not_found Uppslag Erbjuder registrering
404 med mergedInto Hämta medlem Använder det nya id:t
409 identity_conflict Skapa medlem Slår upp med samma identifierare och använder den medlemmen
409 med en quote Reservera Visar den nya totalsumman, reserverar igen på quote.quoteHash
409 reservation_not_held Slutför Prissätter och reserverar igen
409 idempotency_request_in_progress Alla anrop med nyckel Väntar retryAfter, försöker igen med samma nyckel
422 idempotency_key_reused Alla anrop med nyckel En bugg: en ny operation återanvände en gammal nyckel
422 validation_failed Alla errors[].field anger vilket fält som ska rättas
429 rate_limited Alla Väntar under Retry-After
402 feature_not_in_plan Alla Döljer funktionen för den här handlaren
401, 403 insufficient_scope Alla Flaggar ett konfigurationsproblem, inte ett kassörsproblem
5xx, timeout Alla Försöker igen med samma nyckel för anrop med nyckel

Om Custas inte går att nå säljer du till hyllpris och sparar medlemmens identifierare och kvittonumret. När anslutningen är tillbaka skickar du kvittot till POST /v1/members/{id}/points/earn så att medlemmen ändå får sina poäng. Koder och presentkort kan inte godtas offline, eftersom ingenting kan reservera dem.

12. Checklista för integrationen

En kassa som gör allt detta använder allt som Custas erbjuder vid disken.

  • Identitetsregler och samtyckestexter läses in vid start
  • Medlemsuppslag via skanning (kort, Wallet-kort, QR-kod i appen) och via inskriven identifierare
  • Registrering med handlarens obligatoriska identifierare; en konflikt leder till uppslag
  • Samtycke till marknadsföring registreras med text, källa och kassör; återkallande är möjligt
  • Wallet-kort erbjuds vid registrering
  • Varukorgen prissätts om vid varje ändring, med stabila rad-id:n och produktattribut
  • Koder skannas eller skrivs in; nekade koder visas med anledning
  • Stämpelkortsbelöningar erbjuds när medlemmen har en
  • Saldokontroll för presentkort och presentkort som betalmedel
  • Poäng visas och erbjuds som betalning
  • Reservation vid ”Betala”, med en enda 409-väg som visar den nya totalsumman
  • Reservationen släpps vid misslyckad eller avbruten betalning
  • Slutförande med kvitto, betalningar (maskerat kort och PAR), personal och öresavrundning
  • Identifierade köp utan erbjudanden reserveras och slutförs ändå
  • Moms och rabattrader per rad skrivs ut från offerten, inte omräknade
  • Uppdaterade poäng och stämplar skrivs ut på kvittot
  • Delretur via kvittonummer; fullständig återföring vid makulering
  • Presentkort säljs och fylls på
  • Varje anrop som flyttar pengar görs om med samma nyckel
  • Offline-reserv som ger poäng i efterhand
  • Certifierad mot en cust_test_-nyckel före driftstart