Nōwn Meal Plan System

REST API Guide

Last updated: 2026-05-14

This guide documents the REST API for the Nōwn Meal Plan System (MPS). It is written for external integrators who need to look up customer meal plans and process transactions against them.


Contents

  1. Getting started
  2. Conventions
  3. Patron lookup
  4. Transactions
  5. Other endpoints
  6. End-to-end walkthrough
  7. Error code reference
  8. Changelog

Getting started

1. Request credentials

Contact support@nownpos.com to receive credentials for your environment. You will be given two values:

Treat the auth key as a secret. We recommend storing it in a configuration file or secret manager keyed per location, so that transactions can be uniquely attributed to a location in the MPS dashboard.

Migrating from the v1.1 PDF guide? Previous versions of this guide instructed integrators to base64-decode a wrapper blob to extract the host and auth key. The new onboarding flow gives you both values directly. No decoding needed.

2. Make your first request

Replace {HOST} and {AUTH_KEY} with the values you received.

curl '{HOST}/payment/gateway/version' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {AUTH_KEY}'

Expected response:

{
    "success": true,
    "version": "1.0.0"
}

If you receive 401 Unauthorized, recheck the Authorization header. See Authentication below.


Conventions

Base URL

All endpoints are mounted under /payment/gateway on the host you were issued. Throughout this guide, {HOST} is a placeholder for your assigned host (for example, https://api.example.com).

Authentication

Every request must include an Authorization header with the Bearer scheme:

Authorization: Bearer {AUTH_KEY}

The auth key is a single token. It is not a base64-encoded user:password pair. Send it verbatim.

Note for integrators upgrading from the v1.1 PDF: Earlier versions of this guide instructed using Authorization: Basic <auth-key>. That instruction was incorrect. The server validates the token against its AccessToken table via the Bearer / OAuth auth path; the Basic-auth path expects a different format (user:password base64-encoded) and will reject your token. If you have an existing integration using Basic, switch to Bearer. The token value does not change, only the scheme keyword.

Response envelope

Most endpoints return a JSON envelope of the form:

{
    "success": true,
    "data": { ... },
    "error": null
}

A few endpoints (last-transaction, version) return the payload directly without the envelope. These are noted in their individual sections.

Error model

When success is false, the response body includes an error object:

{
    "success": false,
    "error": {
        "reason": "Insufficient Tender Amount",
        "code": 100002
    }
}

Endpoints that do not use the envelope (last-transaction, version) return errors as non-2xx HTTP status codes — see HTTP status codes for the body shape.

HTTP status codes

Most operations return HTTP 200 with the response envelopebusiness-logic failures (validation, patron not found, insufficient balance) still return 200. Check success in the envelope, not the HTTP status.

A few classes of error use real HTTP status codes:

Status Cause Body shape
200 Request reached the endpoint. Includes business-logic failures. {success, data, error} envelope.
400 Server-state precondition failed before business logic ran (e.g. no active cashier shift, no POS station configured at this location). {error: "<message>", exception: {appCode: <code>, message: "<reason>"}}
401 Missing or invalid Authorization header. Same as 400 for most endpoints; {code: <code>, message: "<reason>"} for /last-transaction.
500 Uncaught server error. {error: "<message>"}

Recommended client behavior:

  1. Check the HTTP status code first.
  2. On 200, inspect the envelope's success field.
  3. On non-2xx, parse the appropriate error body. The numeric error code lives at exception.appCode (most endpoints) or code (/last-transaction).

Note: The error body shape on non-2xx responses differs from the envelope's error object. The envelope uses {reason, code}; non-2xx responses use {error, exception: {appCode, message}} (or {code, message} for /last-transaction). Plan your error-parsing code accordingly.

Idempotency

POST /payment/gateway/direct-transaction is idempotent on transactionUuid. If a request with the same transactionUuid (and same authenticated user) has already been processed, the server returns the original response without re-executing the transaction. There is no double-charge and no error.

You should:

Field naming gotchas

A few details that catch integrators by surprise:


Patron lookup

GET /payment/gateway/patron

Looks up a patron by their Nōwn customer-app identifier (QR code value). This endpoint also decrypts the rotating QR format used by the Nōwn customer app.

Side effect: This endpoint may write to the patron's meal plan service period if one is active. This is intentional and ensures subsequent direct-transaction calls do not contend on the patron row.

Query parameters

Name Type Required Description
patronKey string Yes The patron identifier scanned from the customer's app. URL-encode this if it contains +, /, =, or spaces.

Request

curl '{HOST}/payment/gateway/patron?patronKey={URL_ENCODED_PATRON_KEY}' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {AUTH_KEY}'

Response: success

{
    "success": true,
    "data": {
        "valid": true,
        "patronId": 10001,
        "companyId": 1,
        "patronKey": "PATRON-0001",
        "patronCompanyKey": "PATRON-0001",
        "emailAddress": "patron@example.com",
        "firstName": "Pat",
        "lastName": "Patron",
        "password": "",
        "status": 0,
        "birthDate": null,
        "gender": null,
        "homeTown": null,
        "homeState": null,
        "homeCountry": null,
        "academicYear": null,
        "major": null,
        "athlete": null,
        "athleteSport": null,
        "dorm": null,
        "source": 0,
        "hasExpiredMealPlans": false,
        "photoUrl": null,
        "lucovaUserName": "abc123def4567890",
        "hasCreditCard": true,
        "applicableMealEquivalency": null,
        "fiitLocationId": null,
        "fiitPosStationId": null,
        "fiitCashierShiftId": null,
        "fiitLocationOverrideAllowed": false,
        "fiitLastTransaction": null,
        "overridePatronCompanyKey": 0,
        "mealPlans": [
            {
                "mealPlanId": 1,
                "companyId": 1,
                "name": "Unlimited Coffee 100",
                "mealPlanDescription": "",
                "mealPlanType": "DCB",
                "enabled": true,
                "mealPlanRunning": true,
                "mealPlanApplicable": true,
                "isMealPlanApplicable": true,
                "mealPlanAvailable": true,
                "isAvailable": true,
                "unavailablityReasons": {
                    "unavailableAtLocation": false,
                    "expired": false,
                    "noPriority": false,
                    "disabled": false,
                    "noBalance": false,
                    "active": true,
                    "ruleApplied": false
                },
                "patronMealRuleDeductions": [],
                "rules": [],
                "initialBalance": 100,
                "currentDcbBalance": 95.5,
                "currentMealPlanBalance": 0,
                "currentChargeBalance": 0,
                "remainingServicePeriodDcb": 95.5,
                "remainingServicePeriodMeals": 0,
                "remainingServicePeriodCharge": 0,
                "mealsPerPeriod": 0,
                "mealsPerServicePeriod": 0,
                "priority": 2,
                "taxFree": false,
                "guestPlan": false,
                "mealEquivalencyEnabled": true,
                "maxFlexAmountPerTransaction": 100,
                "maxTransactionServicePeriod": 0,
                "chargeBalance": 0,
                "flexAmount": 0,
                "mealPlanCyclePeriod": "WEEKLY",
                "mealPlanResetDay": "SATURDAY",
                "mealPlanResetMonth": null,
                "resetPeriodEnd": true,
                "forceReset": false,
                "isForceReset": false,
                "importExclusionEnabled": false,
                "archivedBalanceDeducted": false,
                "expired": null,
                "startDateTime": 1691539200000,
                "endDateTime": 1782863999000,
                "mealPlanResetTimeMillis": 1778043600000,
                "mealPlanResetDateMillis": 1736553600000,
                "servicePeriodEndMillis": 1778817540000,
                "nextResetDateMillis": 1778907600000,
                "lastResetDateMillis": 1778302800000,
                "patronMealPlanId": 10001,
                "patronId": 10001,
                "patronKey": null,
                "patronCompanyKey": null,
                "emailAddress": "",
                "firstName": "",
                "lastName": ""
            }
        ]
    }
}

Response: patron not found

{
    "success": false,
    "error": { "reason": "Invalid Patron: Not Found", "code": 101 }
}

Notes


Transactions

POST /payment/gateway/transaction/estimate

Calculates what a transaction would look like without committing it. Useful for showing the cashier a tender breakdown (DCB applied, remaining balance to charge to credit card, taxes) before confirming the sale.

Request body: same shape as direct-transaction.

Request

curl '{HOST}/payment/gateway/transaction/estimate' \
  -X POST \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {AUTH_KEY}' \
  --data-raw '{
    "requestAmount": 4.50,
    "request": {
        "patronId": 10001,
        "subtotal": 4.50,
        "totalTaxes": 0,
        "totalSales": 4.50,
        "receiptItems": [
            { "quantity": 1, "name": "Blueberry Yogurt Parfait", "itemPrice": 4.50, "itemTotalPrice": 4.50 }
        ]
    }
  }'

Response: success

{
    "success": true,
    "data": {
        "approvedAmount": 4.50,
        "approvedDCBAmount": 4.50,
        "approvedMealEqAmount": 0,
        "approvedMeal": 0,
        "outstandingAmount": 0,
        "partiallyApproved": false,
        "items": [ /* per-item calculation breakdown */ ]
    }
}

Response fields

Field Type Description
approvedAmount number Total amount the patron's plans can cover.
approvedDCBAmount number Amount covered by Declining Cash Balance (DCB) plans.
approvedMealEqAmount number Amount covered by meal equivalency.
approvedMeal integer Count of meal-swipe redemptions.
outstandingAmount number Amount not covered by any plan. Must be settled out-of-band (cash, credit card, etc.).
partiallyApproved boolean true if some but not all of the request is covered.
items array Per-item calculation results.

Notes


POST /payment/gateway/direct-transaction

Charges a transaction directly to the patron's account. This is the primary transaction endpoint for most integrations.

Required headers

Content-Type: application/json
Authorization: Bearer {AUTH_KEY}

Request body

Field Type Required Description
requestAmount number Yes Total amount to charge, in dollars. Must be > 0.
request.transactionUuid string Yes RFC 4122 v4 UUID. Used as an idempotency key; retry the same UUID on network failures.
request.lucovaUserName string Yes Patron's app username, taken from the patron-lookup response (lucovaUserName field).
request.patronId long No (see below) Patron's numeric ID. Omit or set to -1 to charge a credit card on file instead of a meal plan.
request.dcbAmount number No Amount to draw from DCB. If omitted, the server calculates the split for you.
request.creditCardAmount number No Amount to charge to a credit card on file.
request.debitCardAmount number No Amount to charge to a debit card.
request.subtotal number Yes Subtotal of the order.
request.totalTaxes number Yes Total taxes. Required even if zero, especially for taxable DCB or credit-card-on-file transactions.
request.totalSales number Yes Total sales (subtotal + taxes).
request.receiptItems array Yes Non-empty list of items. See below.

Receipt item fields

Field Type Required Description
name string Yes Non-empty.
quantity number Yes Must be > 0.
itemPrice number Yes Unit price. Must be > 0. Note: use itemPrice, not price.
itemTotalPrice number Yes (if line total > 0) Line-item total. Use itemTotalPrice, not total.

Charging a credit card on file

To charge a customer's credit card stored in the Nōwn app:

  1. Confirm the patron has one: hasCreditCard: true in the patron-lookup response.
  2. Submit direct-transaction without a patronId (or with patronId: -1).
  3. Set creditCardAmount to the amount to charge.
  4. Still include lucovaUserName and transactionUuid.

Request

curl '{HOST}/payment/gateway/direct-transaction' \
  -X POST \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {AUTH_KEY}' \
  --data-raw '{
    "requestAmount": 4.50,
    "request": {
        "patronId": 10001,
        "dcbAmount": 4.50,
        "creditCardAmount": 0,
        "debitCardAmount": 0,
        "subtotal": 4.50,
        "totalTaxes": 0,
        "totalSales": 4.50,
        "receiptItems": [
            {
                "quantity": 1,
                "name": "Blueberry Yogurt Parfait",
                "itemPrice": 4.50,
                "itemTotalPrice": 4.50
            }
        ],
        "lucovaUserName": "abc123def456",
        "transactionUuid": "11111111-2222-4333-8444-555555555555"
    }
  }'

Response: success

{
    "success": true,
    "data": {
        "success": true,
        "transaction": {
            "preauth": false,
            "amount_cents": 450,
            "tax_cents": 0,
            "discount_cents": 0,
            "transaction_type": "direct",
            "status": "completed",
            "transaction_opened": 1747227024,
            "transaction_closed": 1747227024,
            "transaction_opened_obj": "2026-05-14T13:30:24Z",
            "transaction_opened_hr": "2026-05-14 13:30:24 +0000",
            "user_name": "abc123def456",
            "short_id": "12345678",
            "merchant_id": "...",
            "node_id": "...",
            "node_name": "Example Cafeteria",
            "pos_order_id": "...",
            "summary": "$4.50"
        },
        "message": null,
        "error_code": 0
    }
}

Response fields (top-level under data.transaction)

Field Type Notes
status string "completed" on success, otherwise see message and error_code.
amount_cents integer Transaction total in cents.
tax_cents integer Tax in cents.
discount_cents integer Discount in cents.
transaction_type string "direct" for this endpoint.
transaction_opened / transaction_closed integer Epoch seconds.
transaction_opened_obj string ISO-8601 UTC. Prefer this for parsing.
transaction_opened_hr string Human-readable.
user_name string Patron's app username.
short_id string Short human-friendly transaction identifier.

Response: failure

If the request is rejected before reaching the payment processor (validation, no POS station, no active shift), the envelope returns success: false with an error object. If the payment processor rejects the transaction, you'll get success: true at the envelope level but data.success: false and data.message / data.error_code set.

Common validation errors

error.reason Cause
"A unique Transaction UUID is required" transactionUuid missing or empty.
"lucovaUserName is required" lucovaUserName missing or empty.
"requestAmount must be greater than zero" requestAmount <= 0.
"receiptItems must not be empty" receiptItems missing or empty.
"Each receipt item must have a name" One of the items has no name.
"Receipt item '<name>' must have a quantity greater than zero" Item quantity <= 0.
"Receipt item '<name>' is missing itemPrice" Item itemPrice <= 0.
"Receipt item '<name>' is missing itemTotalPrice" Item has total > 0 but itemTotalPrice <= 0. (Most likely cause: you sent price/total instead of itemPrice/itemTotalPrice.)

POST /payment/gateway/transaction/refund

Refunds a previously processed transaction.

Required headers: same as direct-transaction.

Request body

Field Type Required Description
transactionId long Yes The MPS transaction ID to refund. Obtain it via GET /payment/gateway/last-transaction?patronId={patronId}.
description string No Optional note attached to the refund.
transactionUuid string No Idempotency key for the refund itself.
toRefundServicePeriodBalance boolean No Whether to restore the original service-period balance.
refundType string No Reserved.
fiitLocationId long No Override the location.
paymentsToRefund array No List of specific payment tenders to refund (partial refunds). Each entry: { "tenderType": string, "amount": number }.
nownRefund boolean No Set true if the refund originates from the Nōwn POS side (different processing path).

Request

curl '{HOST}/payment/gateway/transaction/refund' \
  -X POST \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {AUTH_KEY}' \
  --data-raw '{
    "transactionId": 5001,
    "description": "Customer return: item out of stock",
    "transactionUuid": "99999999-2222-4333-8444-555555555555"
  }'

Response: success

{
    "success": true,
    "data": {
        "transactionId": 5001,
        "patronName": "Pat Patron",
        "userName": "abc123def456",
        "servicePeriodName": "Week 19",
        "posStationName": "Station 1",
        "dcbAmount": 4.50,
        "cashAmount": 0,
        "changeAmount": 0,
        "creditCardAmount": 0,
        "totalTaxes": 0,
        "totalSales": 4.50,
        "totalDiscount": 0,
        "time": "2026-05-14T14:02:11Z"
    }
}

Other endpoints

GET /payment/gateway/last-transaction

Returns the most recent transaction for a patron. Returns the LastTransactionDTO directly (no envelope).

Query parameters

Name Type Required Description
patronId string Yes Patron's numeric ID, accepted as a string for graceful error reporting.

Response

{
    "transactionId": 5001,
    "locationName": "Example Cafeteria",
    "paymentMethod": "DCB",
    "transactionDateTime": "2026-05-14T13:30:24Z",
    "elapsedTime": 12,
    "elapsedTimeUnit": "minutes",
    "tenders": { "DCB": 450 }
}

tenders is a map of tender-type → amount in cents.


GET /payment/gateway/location

Returns the location associated with the auth key.

Request

curl '{HOST}/payment/gateway/location' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {AUTH_KEY}'

Response: success

{
    "success": true,
    "data": {
        "locationId": 1,
        "companyId": 1,
        "nodeId": "node-example",
        "locationName": "Example Cafeteria",
        "mealPlanIds": [1, 2],
        "mealPlans": [ /* MealPlanDTO list */ ],
        "servicePeriodIds": [10, 11],
        "servicePeriods": [ /* ServicePeriodDTO list */ ],
        "tenderTypeIds": [1, 2, 3],
        "overrideAllowed": false
    }
}

Useful for confirming which location a given auth key is provisioned for, and which meal plans are configured at that location.


GET /payment/gateway/version

Returns the MPS calculation API version. Useful as a health check.

Response

{ "success": true, "version": "1.0.0" }

This endpoint returns the payload directly (no {success, data} envelope around it).


End-to-end walkthrough

The typical integration flow for a single transaction:

  1. Look up the patron with GET /payment/gateway/patron?patronKey=<URL_ENCODED_PATRON_KEY>.

  2. Check whether to charge a meal plan or a credit card. Look at the response:

    • If any meal plan has mealPlanAvailable: true, charge the patron's meal plan. Note the lucovaUserName from the response.
    • If no plan is available but hasCreditCard: true, charge their credit card on file.
    • If neither is available, prompt the cashier for an alternative payment method.
  3. (Optional) Preview the tender split with POST /payment/gateway/transaction/estimate to show the cashier what will be charged to DCB vs. credit card.

  4. Charge the patron with POST /payment/gateway/direct-transaction:

    • Generate a fresh transactionUuid (UUID v4).
    • Include the lucovaUserName from the patron-lookup response.
    • Include itemPrice and itemTotalPrice (not price/total) on each receipt item.
    • On network errors, retry with the same transactionUuid. The server is idempotent.
  5. Persist short_id from the response for reconciliation and customer-facing receipts.

  6. Refund later if needed: look up the transactionId via GET /payment/gateway/last-transaction?patronId={patronId}, then call POST /payment/gateway/transaction/refund with it.


Error code reference

Error codes fall into four groups:

  1. Common errors (codes 400413): general operational and validation errors that any endpoint may return.
  2. MPS-specific errors (codes 10001001): returned by /last-transaction.
  3. Gateway calculation errors (codes 100001100002): issues talking to the meal-plan calculation server.
  4. Validation errors (no numeric code): request-shape failures on direct-transaction. The code field is 0 or omitted; the reason field carries the human-readable message.

In addition, the direct-transaction endpoint may surface passthrough error codes from the downstream payment processor under data.error_code when data.success is false. These are not MPS codes. Contact engineering@nownpos.com for guidance on specific codes.

Common errors

Code Name Reason
101 (legacy, no enum name) Invalid Patron: Not Found. Returned by /patron (v1) when the QR/key does not resolve to a known patron.
400 NOT_FOUND Unable to find object
401 INVALID Invalid State. Generic invalid-state error; the accompanying message usually explains what was invalid (e.g. Insufficient tender amount, Invalid transaction state. Transaction is a patron transaction without a patron id.).
402 ACTIVE_SHIFT_FOUND User already has an active shift
403 INSUFFICIENT_FUNDS Insufficient Funds
404 GENERIC Generic Error
405 TRANSACTION_REFUND_INVALID Transaction Refund Failed. The refund could not be processed.
406 FORBIDDEN Forbidden
407 NO_ACTIVE_SHIFT User does not have an active shift. The location has no open cashier shift; required by estimate and other endpoints that compute service-period state.
408 UNAUTHENTICATED User is not authenticated
409 PATRON_RELOAD_DISABLED Patron is not allowed to reload dcb
410 NO_POS_STATION Location does not have a pos station. The location has no default POS station configured.
411 NO_PATRON_FOUND Patron not found
412 NO_SERVICE_PERIOD No active FIIT service period
413 PATRON_STATUS_INACTIVE Patron status is not active

MPS-specific errors

Returned by /last-transaction. These arrive as a non-2xx HTTP status with the body { "reason": "...", "code": ... } (no envelope).

Code Name Reason
1000 MPS_UNEXPECTED_ERROR Unexpected Error. Contact system administrator
1001 MPS_UNAUTHORIZED Unauthorized

Gateway calculation errors

Code Name Reason
100001 GATEWAY_CALCULATION_SERVER_DISCONNECTED Meal Plan Service server is disconnected. Transient; retry after a short backoff.
100002 GATEWAY_INSUFFICIENT_TENDER_AMOUNT Insufficient Tender Amount. The patron's available plans plus any supplied tender cannot cover the request.

Validation errors (no code)

Returned with code: 0 (or omitted) and a reason describing the problem. These are emitted by request-shape validation before any business logic runs.

Reason Cause
Unauthorized The request reached an endpoint but no authenticated user could be resolved. Recheck the Authorization header. See Authentication.
Unknown Location The auth key is not associated with a location. Contact support.
A unique Transaction UUID is required direct-transaction request missing or empty request.transactionUuid.
lucovaUserName is required direct-transaction request missing or empty request.lucovaUserName.
requestAmount must be greater than zero direct-transaction request with requestAmount <= 0.
receiptItems must not be empty direct-transaction request with no receiptItems.
Each receipt item must have a name A receipt item has no name.
Receipt item '<name>' must have a quantity greater than zero A receipt item has quantity <= 0.
Receipt item '<name>' is missing itemPrice A receipt item has itemPrice <= 0. Most common cause: client sent price instead of itemPrice.
Receipt item '<name>' is missing itemTotalPrice A receipt item has total > 0 but itemTotalPrice <= 0. Most common cause: client sent total instead of itemTotalPrice.
Insufficient tender amount The provided tender amounts do not cover the request.

Payment processor passthrough errors

When a direct-transaction request authenticates and validates successfully but the downstream payment processor rejects the charge, the response envelope returns success: true with data.success: false, data.message set to a human-readable message, and data.error_code set to a processor-specific numeric code. These codes are not enumerated in this guide. Contact engineering@nownpos.com for guidance on specific codes.


Support

When reporting an integration issue to engineering@nownpos.com, include:


Changelog

2026-05-14

v1.1 (2024-10-08)