Skip to content
Members and points behind the endpoints

Members and points behind the endpoints

“We posted the sale, the member says the points never arrived, and the balance we read back says something else again.” This page is how your code tells which of the four usual answers it is: the document was not the type the company awards on, its lines carried no points, the job that writes them has not run, or your code read the wrong balance endpoint. The six endpoints themselves are documented page by page from the Membership endpoints hub.

What happens after you finalise a sale with a member on it

None of this shows in a response. It decides whether a member ever sees the points.

  1. The points are on the document line before Finalise, or they are nowhere. At the till the price book puts point_amount, point_currency and the validity dates on each line as it is rung. On a document your own code creates, the line carries points only if you put them there, with one exception: when a cash bill or sales invoice whose header carries member_guid is updated or has its posting status changed through the API, the server fills in points for the lines that have none, from a price set that names a Member Class, and only while the PRICEBOOK_POINTS_AUTO_APPLY setting is on for the POS General or Sales Invoice applet. Other document types are skipped. Points Setup has the setting. A second way to get a figure is PUT core2/tnt/dm/crm/membership/update-points-ccy-amount: send one document-line container and it writes the points from the item’s pricing-scheme point link onto that line and saves the line. That is the pricing scheme’s figure, not the price book’s.
  2. Finalise queues one job and answers. A document you create is stored and numbered and starts no points job, even when you send it as FINAL (since mid-October 2026 one sent as FINAL is handed to the e-invoice entry, and to nothing else). Finalise is the separate call that queues the primary processor; its 200 means the document is final and the job is queued. Draft, then Finalise has the two calls.
  3. Which follow-on jobs run is a subscription, decided per tenant and per company. The primary processor reads the processors subscribed to it for this tenant, drops any the company’s include or exclude list on its posting configuration rules out, and queues one job each. The points writer is one of those subscribers, registered as GENERIC_DOCUMENT_POINT_UPDATE_PROCESSOR. A tenant where it is not subscribed finalises sales that earn nothing, with no error anywhere.
  4. The points writer decides once, and says nothing when it declines. It returns at once when the document’s membership posting is already POSTED or VOID. It then needs three things together: a member_guid on the header, and a document type and posting status equal to the company’s MEMBER_POINT configuration, which defaults to INTERNAL_SALES_ORDER at FINAL and is set through the company API rather than on a screen (Organisation). Then, for each line that carries both point_amount and point_currency and has no points line yet, it writes one line: REWARD when the amount is positive, REDEEM when it is negative, validity copied from the document line’s point dates, source set to the writer’s own name. It marks the document POSTED only when it wrote a line or every eligible line already had one. A document with no eligible line is left unmarked, so a later run can award once the lines are fixed. An exception is logged and nothing is raised: Finalise answered 200 long before.
  5. Each line queues the balance job, to run at once. The job re-reads the line, skips it when it is deleted or its value is zero, recomputes the member’s stored balance in that currency as the sum of their active lines, and builds the lot (the control-account row) the expiry run will later consume. The stored balance is one row per member and point currency in bl_crm_membership_points_current_balance; current-balance/query reads it, lines/query is the ledger behind it, and the lots carry the dates.
  6. Nothing tells you it happened. The MEMBERSHIP_POINTS_TXN_CREATED webhook fires from the header route, POST …/membership-points-txn, and not from the lines the writer creates or from POST …/txn-line. An integration that must know a sale earned points queries lines/query with doc_hdr_guid after Finalise; the member-record events MEMBERSHIP_CARD_CREATED and MEMBERSHIP_CARD_UPDATED do fire, on the webhook feed.

The member is not the account you bill

GadgetSphere Online’s web store enrols a shopper at checkout: she buys wireless earbuds, joins the programme, and your integration sends POST core2/tnt/dm/membership-cards with her name, mobile number, e-mail and identity-card number and no entity_hdr_guid. The backend looks for a customer by her sign-in, then by the phone, then by the e-mail, and creates an individual customer only when none matches. A customer it creates carries her name, identity number, phone and e-mail. A customer it finds keeps whatever identity it already had, and nothing copies hers across.

Three weeks later she orders a laptop for the small company she runs and wants the e-invoice in the company’s name. Your order carries two different parties on purpose: member_guid is her card, so the points go to her, and the document’s entity is the company’s trade account, which is billed and e-invoiced. The e-invoice takes the buyer’s identity from that entity, never from the card. So the identity number you sent on the member three weeks ago is of no use to this invoice unless it is also on the customer record, and PUT Update Member will not put it there: the update writes the membership tables only.

Two things follow for an integration. If your own system is the master of identity and you want the customer kept in step, enrol through POST and PUT …/membership-cards/with-customer/etl-ep, which write the member’s name, identity, phone and e-mail onto the linked customer on every create and update, instead of the plain pair. And before you create, POST …/membership-cards/duplicates/backoffice with the same container answers the count and the customer records that already share the phone or e-mail; a shared office number or a family e-mail is exactly how a new member gets linked to somebody else’s account, so read back entity_hdr_guid after every create and show it to a person when the check found more than one. Scenario 024 in our planning notes records the support case this was written from; the same separation is on the membership module page.

What these endpoints will not do

Stated plainly, so you can plan around it.

Finalise does not work out points. A line that reaches Finalise without point_amount and point_currency earns nothing, and the response does not say so. Put the points on the line, or rely on the server’s fill-in pass only for a cash bill or sales invoice with the setting on.
A hand-written points line is not checked against the member. The validator requires points_value and nothing else: card_no and membership_hdr_guid are stored as you sent them, a blank point_currency becomes DEF, and absent validity dates become 1 January 1900 to 31 December 9000, so the lot never expires. The response is 200 in every one of those cases.
No ceiling is applied to limit. Paged reads default to 100 rows and pass whatever you send straight to the query. Count the total only when you need it (calcTotalRecords=true runs a second query), because a tenant whose connection slots run out answers 429 for everybody.
Points do not post anywhere else. A reward or redemption line touches no journal and no stock; a redemption reaches the ledger only as the settlement or discount line of the bill it was spent on. The points writer awards on one document type per company, so a sales return of another type writes no line and the member keeps the points, and voiding the source document removes the lots but leaves the lines and the stored balance as they were.

When it goes wrong

What you seeUsual causeWhat to do
403 with code CLIENT_AUTH_USER_NOT_AUTHORIZED on a points writeThe key’s user is not an owner or admin and holds none of TNT_LOG_MEMBERSHIP_POINT_TXN_OWNER, TNT_LOG_MEMBERSHIP_POINT_TXN_ADMIN, TNT_API_MEMBERSHIP_POINT_TXN_CREATEGrant the CREATE permission to the key’s user. The reads want TNT_API_MEMBERSHIP_POINT_TXN_READ, a member create TNT_API_MEMBERSHIP_CARD_CREATE, and the duplicate check TNT_API_ENTITY_READ
400 with code EXCEPTION_INCONSISTENT_DATA and a list in dataThe validator refused the body: a points line without points_value; a member whose card_no or member_id is already taken, or whose entity_hdr_guid does not existRead the code on each entry in data; it names the field. This envelope code is not yet on the error index
400 with code CLIENT_MEMBERSHIP_POINT_CARD_NO_OR_POINT_CURRENCY_NOT_FOUND from current-balance/{cardNo}/{pointCurrency}No stored balance row for that card in that currency: the member has never earned in it, or the balance job has not run yetTreat it as zero, or read current-balance/query?card_no=… without a currency, which lists every currency the member holds and answers an empty list otherwise. A line sent without a currency is in DEF
404 with code OK_NODATA from balance/{card_no} or any login-entity-ep readThe card is not found, or on a login-entity-ep route it is not one of the signed-in user’s own cardsCheck the number against membership-cards/query; for a member portal, send the member’s own token
417 with code SERVER_GENERAL_UNKNOWN_ERROR from current-balance/{cardNo}/{pointCurrency} for one card, while other cards answerTwo balance rows exist for that card and currency. Lines were written with card_no and no membership_hdr_guid, and the balance job keyed a second row on the empty memberSend membership_hdr_guid on every line you write. For the rows already there, ask BigLedger to remove the orphan row, then rebuild the balance from the lines (put the points back the right way)
200 on POST …/txn-line, and the balance has not movedpoints_value was 0, which the job skips; the job is queued to run at once but after your response; or the line went into currency DEF because you sent noneRead again after a moment, and read current-balance/query?card_no=… without a currency to see every currency the member now holds
The sale is FINAL and lines/query?doc_hdr_guid=… returns nothingThe document is not the type and status the MEMBER_POINT configuration names; the header has no member_guid; the lines carry no points; the writer is not subscribed for the tenant; or it declined and loggedWalk the chain on membership points and vouchers. A document left unmarked is awarded by a later run once its lines are fixed; a document marked POSTED with no lines is the one broken state, and its repair is the data-fix run on that page
The balance reads zero while lines/query shows the linesAn EXPIRY or insufficient-balance adjustment zeroed the stored figure and the recompute hop was lostThe same page, step 5: re-drive the balance from the lines
429 with code CLIENT_OVERLOAD_DATABASEThe tenant’s database connection slots are exhausted: large pages with calcTotalRecords=true, or many parallel readsSmaller pages, no total count unless you use it, and retry with a back-off
balance/{card_no} disagrees with current-balanceIt is one lot row, not a sum, and the read can rewrite itRead current-balance; read the lots for validity

Check yourself in thirty seconds

This check was written from the code and has not been run: no demo tenant is available to this desk, and on a customer tenant only the two reads are allowed. One card, one currency, and the arithmetic the balance job itself performs.

  1. Find the card. GET …/membership-cards/query?card_no=GS-0001234 answers one record; keep its guid, which is the membership_hdr_guid every points line should carry.
  2. Read the balance. GET …/membership-points-txn/etl-ep/current-balance/GS-0001234/GSPTS answers point_balance, or 400 with the not-found code when the member has never earned in GSPTS; take that as zero.
  3. Write one point, on a test tenant only. POST …/membership-points-txn/txn-line/etl-ep with points_value 1, txn_type ADJ, card_no, membership_hdr_guid, point_currency GSPTS and a remarks saying what it is. The answer is 200 with the line’s guid. Read step 2 again: the balance is one higher. Then post the same line with points_value -1 and read it back to where it was.
  4. Prove the stored figure is the sum of the ledger. GET …/membership-points-txn/etl-ep/lines/query?card_no=GS-0001234&point_currency=GSPTS&limit=500 and add points_value over the lines whose status is ACTIVE. That sum equals point_balance from step 2. When it does not, the stored figure is the wrong one, and the repair is on the troubleshooting page, not a line you write by hand.

Members and points in the rest of the product

  • What must exist first. A member record, which is where card_no and membership_hdr_guid come from; a point currency, registered under crm/membership-points-currency; the company’s MEMBER_POINT configuration naming the document type and status you actually post; and, unless your code puts the points on every line itself, a price book whose price sets carry a Point treatment and the fill-in setting above. Missing any of these looks the same from your side: a 200 on Finalise and no line. Rewards Setup has the order.
  • What becomes possible. Once lines exist, the till offers the stored balance at the next sale, a member portal can show a member their own balance and history through the login-entity-ep routes with their own token, and finance can build the month’s points liability from the three figures that should agree (What You Owe Your Members).
  • The siblings, and when to use which. POST …/membership-cards finds or creates the customer and then leaves it alone; …/with-customer/etl-ep writes the member’s identity onto the customer every time, so choose by which system owns identity. POST …/txn-line writes one line and raises no webhook; POST …/membership-points-txn writes a header with its lines and raises MEMBERSHIP_POINTS_TXN_CREATED; the CSV upload under crm/membership-points-transaction/import-file-hdrs is for a migration a person checks afterwards. current-balance is the figure, balance/{card_no} is one lot, and the lot rows are what still counts and until when. The Membership Admin applet does all of this through screens, for a person, and its Add Point Adjustment is the same txn-line route.
  • What breaks if it is wrong. A hand-written line moves the balance the till offers before your next request returns, so a wrong points_value can be spent at a counter within the minute; its lot carries the dates you sent, or none, so a mistaken award without a date_valid_to outlives every genuine one; and because points post to no journal, finance sees it only in the month-end liability figure. A line without membership_hdr_guid breaks the per-card balance read for that card. On the sales side, a return re-issued to a different account moves the points with the account, and the first party loses them without being told.
  • What it is used alongside. The till: points earned on bills and spent at the counter are by far the commonest reward, and where a company has changed its MEMBER_POINT configuration at all it has almost always pointed it at the cash bill. For an integration, the pairing is this page’s lines/query after Finalise with the member-record webhooks, and the e-invoice buyer block on the document when the member is not the account.

Related documentation

Last updated on