NeoCurrency — NEO
Voucher vendor adapter — lazy OAuth2 bearer (24h JWT, reactive 401-retry), synchronous inline delivery, link-only vouchers, and heavy spec drift.
Quick facts
Code NEO (NeoCurrency / Your Digital Reward) · Line voucher · Auth OAuth2 password-grant bearer (lazy, 24h JWT) · Model synchronous (codes inline) + recovery-by-reference · Webhook none (polling-only) · Delivery activation link (no PIN) · Adapter neo_vendor.go, neo_errors.go
Synchronous, polling-only. POST /createordernow returns codes inline; lost responses recovered via GET /get-orders-rfid?reference_id=. Registered factory.go:64. See Order Lifecycle.
Auth
Lazy OAuth2 password-grant → bearer. POST /get-token with {client_id, client_secret, email, password}; token cached in memory (double-checked sync.RWMutex).
24h JWT, reactive refresh. Docs claim non-expiring but production returns a JWT with exp = iat+24h. Rather than proactive refresh, every authed call goes through authedDo which on a 401 clears the token, re-mints, and retries exactly once. Do NOT add proactive refresh. Minting invalidates the prior token for that user — run multiple processes against distinct API users.
vendor_attributes: host, client_id, client_secret, email (all in Extra), password, optional prepaid_client_id (Bancorp/Pathward for Visa/MC brands).
Catalog
GetCatalogBatched only — GET /brands?page=N&per_page=100 (1-indexed, no pagination metadata; stop on a short page). Brand DTO uses integer 0/1 for active/is_percent; value>0 = fixed denomination (min==max), value=0 = open range. Exactly one Product per brand.
Order create — synchronous
POST /createordernow. Codes returned inline; no async/polling on the happy path. Idempotency via unique_id — an integer (≤10 digits) derived by stripping the _R<n> retry suffix then FNV-1a hashing the reference (all retries share one key → duplicate retries rejected as 422, not double-charged). prepaid_client_id attaches to the brand item, not top-level. ProductAvailability is a no-op (true) — real failures surface at order time as 422.
Order status / recovery
No get-by-vendor-order-id endpoint. GetOrderStatus/GetVouchers key on the Octopus reference_id via GET /get-orders-rfid. A non-empty result implies COMPLETED (the API only stores fulfilled orders). 404/empty → ErrOrderNotFound.
Webhook
None — polling-only. Inherits the base normaliser (unused for NEO in prod).
Delivery — link only
Always a URL, never a PIN. neoClaimURL builds <base>/[sandbox/]activate-code/<code> from the host. Code set, Pin="", reference = uuid (or the code itself on the RFID recovery path). CancelVoucher unsupported (dashboard-only void).
Balance
GET /funds → flat array → one BalanceResponse per currency (unallocated pool).
Spec drift & error taxonomy
Production ≠ OpenAPI spec — the headline
The spec at docs/neocurrency/openapi.json is unreliable. Confirmed divergences (see Memory Appendix):
/get-tokensuccessis an array[{access_token}], not an object.- token is a 24h JWT (spec says non-expiring).
step_sizeis often null;expiry_in_monthsis sometimes a number, sometimes an empty string.
Rule: fields not actively consumed → json.RawMessage to tolerate drift (trusting the spec caused cannot unmarshal panics during catalog sync). Prod wins; document the divergence in the DTO comment.
Error kinds (neo_errors.go): 401→auth (handled by the one-shot retry); 403→insufficient-funds (terminal); 406/422→validation (terminal, re-classified to insufficient-funds if the message matches); 429→rate-limit (retry, honors Retry-After); ≥500→server (retry 10s). Two envelope shapes handled permissively ({error} and {errors:[{field:msg}]}).
Tests, mock, docs
Contract (neo_contract_test.go) + chaos (neo_chaos_contract_test.go, incl. the 401 auto-refresh) + orchestration (test/clientapi/neo_orchestration_test.go, incl. Sync_HappyPathDeliversInline_NoPolling and the opt-out-of-link-path policy). Mocky route frontend/mocky-balboa/src/routes/neocurrency.ts deliberately reproduces the spec drift (array-wrapped success, step_size:null). Docs + Postman under docs/neocurrency/; mapping/neo_currency_catalog.tsv.
iRewardify — IRW
Voucher vendor adapter — lazy bearer login, custom unsigned webhook, two-phase async delivery with dual code/link formats.
Wupex — WPX (voucher)
Voucher vendor adapter — static API-key header, Cloudflare-fronted catalog with 429 backoff, USD-settled with currency inferred from product names.