Tanei Pay Developer Portal

Complete API reference, interactive explorer, and webhook integration guide for the Tanei Pay merchant payment platform.

REST API Firebase Auth HMAC Webhooks Excel / CSV Export Cloud Run · europe-west1
Base URL https://www.taneipay.com/api All requests use HTTPS · JSON request/response bodies

Overview

The Tanei Pay API is a REST API served over HTTPS. It uses Firebase Authentication for identity — callers must obtain an ID token via the POST /api/auth/login endpoint and include it as a Bearer token in subsequent requests.

Response Format

All responses are JSON. Successful responses include the requested data. Error responses include an error field.

// Success { "ok": true, "id": "A1B2C3D4" } // Error { "error": "Missing field: grand_total" }

HTTP Status Codes

CodeMeaning
200Success
201Created
400Bad request — missing or invalid fields
401Unauthorized — missing or invalid token
403Forbidden — insufficient role
404Resource not found
409Conflict — duplicate idempotency key
429Rate limited (120 req/min per IP)
500Internal server error

⚡ Quick Start

1

Obtain an access token

Call the login endpoint with your username and PIN.

curl -X POST https://www.taneipay.com/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","pin":"1234"}' # Response includes a Firebase ID token: # { "token": "eyJhbGci...", "user": { "role": "admin", ... } }
2

Use the token in requests

Pass the token as a Bearer token in the Authorization header.

curl https://www.taneipay.com/api/transactions \ -H "Authorization: Bearer eyJhbGci..."
3

Submit a transaction

curl -X POST https://www.taneipay.com/api/transactions \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ecr-txn-uuid-001" \ -d '{ "ecr_txn_id": "ecr-txn-uuid-001", "user_id": "u-cashier1", "user_name": "Cashier 1", "status": "approved", "grand_total": 12.50, "items": [{"name":"Coffee","qty":1,"priceExVat":10.33,"vatRate":21}] }'

🔐 Authentication

Tanei Pay uses Firebase Authentication. The POST /api/auth/login endpoint validates your username and PIN, then returns a short-lived Firebase ID token (valid ~1 hour). Pass this token as a Bearer token.

Roles

RoleAccess
adminFull access to all endpoints including user management, branding, webhooks, exports, audit log
cashierCan submit and view own transactions only

Token Refresh

Tokens expire after ~1 hour. Call POST /api/auth/login again to get a fresh token. A 401 response means the token has expired or was revoked.

🏥 Health

GET /api/health Public Service health check

Returns the current service version and region. No authentication required.

Response

{ "status": "ok", "service": "tanei-pay-cloud", "version": "1.3.0", "region": "europe-west1" }

👤 Authentication Endpoints

POST /api/auth/login Public Login with username + PIN

Request Body

FieldTypeRequiredDescription
usernamestringMerchant username
pinstring4-digit PIN
totp_codestring6-digit TOTP code (required if 2FA enabled)
// Request { "username": "admin", "pin": "1234" } // Response (success) { "token": "eyJhbGciOiJSUzI1NiIs...", "user": { "id": "u-admin", "username": "admin", "name": "Admin", "role": "admin" } } // Response (TOTP required) { "totp_required": true }
POST /api/auth/logout Auth Revoke current token

Invalidates the current Firebase ID token. Subsequent requests with this token will return 401.

// Response { "ok": true }

💳 Transactions

POST /api/transactions Auth Submit a transaction from ECR app

Creates a new transaction. If a transaction with the same ecr_txn_id already exists, it updates the status (idempotent). Fires a transaction.created webhook event for new transactions.

Headers

HeaderDescription
Idempotency-KeyUnique key per transaction (use ECR transaction UUID). Prevents duplicate submissions.

Request Body

FieldTypeRequiredDescription
ecr_txn_idstringUnique ECR transaction ID (UUID)
user_idstringCashier user ID
user_namestringCashier display name
statusstringapproved | failed | pending
grand_totalnumberTotal amount including VAT and tip (€)
itemsarrayArray of line items (see below)
tip_amountnumberTip amount (€)
payment_brandstringe.g. Visa, Mastercard
wpiResponseobjectRaw WPI response from Worldline terminal
timestampstringISO 8601 transaction timestamp

Item Object

{ "name": "Coffee", "qty": 1, "priceExVat": 3.31, "vatRate": 21 }
// Request example { "ecr_txn_id": "9a7f3b21-...", "user_id": "u-cashier1", "user_name": "Jan Smit", "status": "approved", "grand_total": 12.50, "tip_amount": 1.00, "items": [ { "name": "Espresso", "qty": 2, "priceExVat": 2.48, "vatRate": 21 }, { "name": "Croissant", "qty": 1, "priceExVat": 2.07, "vatRate": 9 } ], "payment_brand": "Visa", "wpiResponse": { "MaskedPAN": "****4242", "AuthorizationCode": "123456" } } // Response { "ok": true, "id": "A1B2C3D4" }
GET /api/transactions Auth List transactions with filters

Returns a paginated list of transactions. Cashiers only see their own transactions.

Query Parameters

ParameterTypeDescription
statusstringFilter by status: approved, failed, pending
fromstringISO 8601 start date (e.g. 2026-04-01)
tostringISO 8601 end date
limitnumberMax results (default: 200, max: 1000)
offsetnumberPagination offset (default: 0)
user_idstringFilter by cashier (admin only)
// Response { "transactions": [ { "id": "A1B2C3D4", "status": "approved", "grand_total": 12.50, ... } ], "total": 142, "limit": 200, "offset": 0 }

📥 Transaction Exports

GET /api/transactions/export/standard Auth Download standard transaction report

Downloads a formatted report with all standard columns. Supports Excel (.xlsx) and CSV formats. Up to 5,000 transactions per export.

ParameterTypeDescription
formatstringxlsx (default) or csv
fromstringStart date (ISO 8601)
tostringEnd date (ISO 8601)
statusstringFilter by status

Standard Columns

Transaction ID · Date/Time · Cashier · Status · Items · Subtotal (€) · VAT 0% · VAT 9% · VAT 21% · Tip (€) · Grand Total (€) · Payment Brand · Card (masked) · Auth Code · Synced

// Download as Excel (browser) const r = await fetch('/api/transactions/export/standard?format=xlsx&from=2026-04-01', { headers: { 'Authorization': 'Bearer ' + token } }); const blob = await r.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'transactions.xlsx'; a.click();
GET /api/transactions/export/bespoke Auth Download custom field report

Download a report with only the fields you specify, in the order you specify them. Pass preview=true to get a JSON preview of the first 5 rows without downloading.

ParameterTypeDescription
fieldsstringComma-separated field keys (see below)
formatstringxlsx or csv
previewbooleanReturn JSON preview (first 5 rows) instead of file
from / to / statusstringSame as standard export

Available Field Keys

id, timestamp, user_name, status, items_summary, subtotal, vat_0, vat_9, vat_21, tip_amount, grand_total, payment_brand, masked_pan, auth_code, synced
// Preview first 5 rows GET /api/transactions/export/bespoke?fields=timestamp,user_name,grand_total,payment_brand&preview=true // Response { "headers": ["Date/Time", "Cashier", "Grand Total (EUR)", "Payment Brand"], "rows": [ ["2026-04-14T10:23:00Z", "Jan Smit", 12.50, "Visa"], ... ] }

📊 Dashboard Stats

GET /api/stats Auth Revenue stats and chart data
ParameterValuesDescription
periodday|week|month|yearReporting period (default: day)
// Response { "period": "day", "current": { "count": 42, "revenue": 1284.50, "tips": 87.00, "vat_0": 0, "vat_9": 45.20, "vat_21": 212.30, "avg_ticket": 30.58 }, "previous": { "count": 38, "revenue": 1102.00, ... }, "chart": [ { "label": "09:00", "total": 88.50, "count": 3 }, ... ], "brands": [ { "payment_brand": "Visa", "count": 28, "total": 892.00 }, ... ] }

🎨 Branding

GET /api/branding Public Fetch current white-label branding
// Response { "brand_name": "Tanei Pay", "logo_url": "https://...", "primary_color": "#0F2D6E", "accent_color": "#4ECDC4", "company_name": "Tanei Payments B.V.", "receipt_footer": "Thank you for your business!" }
PUT /api/branding Admin Update branding settings

Partial update — only send the fields you want to change. Colors must be hex (#RRGGBB). Logo can be a URL or base64 data URI (max ~500 KB).

{ "brand_name": "My Brand", "primary_color": "#1A2B3C", "receipt_footer": "Thank you!" }

👥 Users

All user management endpoints require admin role.

GET /api/users Admin List all users
// Response { "users": [ { "id": "u-admin", "username": "admin", "name": "Admin", "role": "admin", "active": true, "totp_enabled": false } ] }
POST /api/users Admin Create a new user
// Request { "username": "cashier2", "name": "Cashier 2", "pin": "5678", "role": "cashier" } // Response { "id": "u-abc1234", "username": "cashier2", "name": "Cashier 2", "role": "cashier" }

🔗 Webhook Configuration API

GET /api/webhook Admin Get current webhook config
// Response { "configured": true, "active": true, "url": "https://your-server.com/tanei-webhook", "events": ["transaction.created", "transaction.synced"], "has_secret": true, "last_fired_at": "2026-04-14T10:23:00Z", "last_status": 200, "last_event": "transaction.created", "available_events": ["transaction.created","transaction.synced","branding.updated","user.created"] }
PUT /api/webhook Admin Save webhook configuration
FieldTypeRequiredDescription
urlstringHTTPS endpoint URL
eventsarrayEvent types to subscribe to (default: all)
secretstringHMAC signing secret (stored encrypted)
clear_secretbooleanSet to true to remove existing secret
// Request { "url": "https://your-server.com/tanei-webhook", "events": ["transaction.created"], "secret": "my-signing-secret" } // Response { "ok": true }
POST /api/webhook/test Admin Send a test ping to your webhook
// Response (success) { "ok": true, "status": 200, "body": "OK" } // Response (failure) { "ok": false, "status": 500, "body": "Internal Server Error" }
DELETE /api/webhook Admin Remove webhook configuration
// Response { "ok": true }

📋 Audit Log

GET /api/audit-log Admin Retrieve audit log entries
f
ParameterDescription
limitMax entriesth> )ng

Aerard"UNerd

ir GETinndp#RRGGBBETinndp#RRGGBBETinndp#RRGGBBETinndp#RRGGBBETinndp#Ro
(22ss="inline-code">tran), tr>< "> 2ctions.nt-desc">Removeu nt-desc">Remov6thod DELETE;nse-e cc
Remov6thod DELETiv clbVnclar 7g:wdl u"parat-desc">Remov6thod DELETE;nse-e cc .N a 429Rate limited (. Supports ii dPrts iir2"11px;font-w;/spat-desc">Removeu Ul1clbVnclar e0t-path">": 200, "offset": 0 } Des/th>Descriptin- GETDes="endpoint-bodysectionbof f inndpt ar);maroint-bodysectionbof f tps://yo I=ms://yo ts_secret": true,I=ms://yo E=0t": 42,o -bd claseEh>Des/td> font-sont-size:13p.c Re(t"inlish>Descrip; E=bransactie;size:13p. "pclad a test ping to your wlass="auth-secrex} laseson,/a.700;cransparent;m="tnc">;size'an>inneola cla ord reated"bhgf", "usarent;m="6I=ms:rh-bn Remove webhook configuration
;siz izes": 87.y"> 3>;siz izes: Removeu Admin Remove webh;Teys (see below) formatste-badge">steoXpe-badge">ste-badge">steoXpe-badge">ste-badge">steoXpe-badge">ste-bade,1">s n>s n> iir2"11pYbad1">;sS> > Vetrc }sr2"11pY;y" te-bB,1">xe">st"; atd>edgezutS001" \ -9firY>Pa/)gezutS001" \ -9f\ezutS0)r>< USERS ═══ --> Save webhook configurationGet current webhook confiveu Ret ;si Ret
x Vekn_summary, s="endpoint-body">
inndpt ar);maroint-bodysectionbof f >edgez3rent;m="6I=ms:rh-bnd,"9dge">m="6I=mt-y)< 3vthsdesc/="code-b8 kenmary, s="endpoint7(/span>in "r keyx font-leEndpoint(lick=dendebh/div> x font-l l(cPa/emo="endptheaBsont-say)">Tra_r>x font-l lceated"bhgf", ;9"42f) { "ok": true, "status"ation x3M ir
Remov6tho < Ul> . er0).tlIocPt":i_w) Remov6tho < Ul> i1Get current webhook co
U current web5de-blockldeo n> U current web5de-blockldeo Endpoint(this)"> GET Remov6thod DELETE;nse-e s "rows"l(:n6aeg Cash1", "user_namspoinardrtion x3M web5de-blockt.klass="method G x3M o3etpan>tp/login Public ubl to get a JSON f (22ss="inline-code">tran), tr>< "> 2ctions.Retrieve audit log entries n tableso="endlm)=5y> ytd>Tfhu( cla| 04-01', { heepoint-desc">Remove webh /div>sacti /div>sacti /div>sacti /div>sacti /div>sacti /div>sacti /div>sacti /div>sacti /div>sacti /div>sacti /div>sacti /div>sacti /div>sacti /div>sactioggti /V0rypted)Redpoint /diE ldeo {HTTPRb"l(:n6aeg Cas.ctr1an> g)"> GET<s n> iitva sos_summaryard">oken ._aV,,otal" } a1Vo.Ad1eEne /div/spanh1">s n> iispanh1"> ET /api/transactions/export/bespoke?fields=timestamp,user_name,grand_total,payment_brand&preview=true // ofsnelGl">Cn> g)"n
iitva-n>tps://yo
< USERS ═══d>fromes="endpoint">
.6thod DELETE;nse-e s "rows"l(:n6aeg Cash1", "user_namspoinardrtion x3M inardrtihgin-b-a inn-Eu.e s "ro;Ho your webhnline-code uS.el E Removeu , str9ttE Removeu ,
Re# Retrieve audit log entries
Des/span> ypet-des. "09:00", "nU< oncl _et-desc">Ret Admin #a x3t;m="6I=ms:rh-bn et obtain an ID token viaU)ET">GET s="param-table"Ra p;l l(cPa/emo="endptheaBsont-say)">Tra_r>x font-l lPt":i_w){able"Ra p;l l(cPa/emo="sont-sr>x font-l oL1h, "usaPp.tlIocPv-sr>x font-l oL1h, "usaPp.tlIocPv-sr>x font-l oL1h, "usaPp.tlIo"> 1tr1an> ypet-de-de-de-de-d42sX&l oL1h, "usaPp.tlIocPv-sr>x font-l oL1h, "usaPp.tlIo"> 1tr1an> ypet-de-de-de-de-d42sX&l oL1c11B_">Trbvh, "usaPp<:yendp1"y"> .tr>
PUTCas1vmH .6thod DELETE;nse-e s "rows(n x3M t"o&u, nadge"gin-, t":i_Rs"l(:n6aegnPUT">PUT < s "|aA=(:(.tsa nadge Cas1vmH .6thod DELETE;nse-e s "rows(nons.n6aeg Cash1", "useass="endpoint-desc">Retrieve audit log entries n ta>innd s=c">b n ta>innline-code"tuve 2POST hies n ta>innd s=c">boveu yc"- <(( C

n> y'6tho> f;Ho .6aee 4e s "rows(nons.n6aeg ec">boveu") /div>sacti /din> y#rmo<(( C

n> y'6(nopa"ca0de-de-d42sX&l oePlfnsacesn> y'6(n<0Str><(( C

n> y'6tho> f;Ho .6aee 4e s "rows(nons.n6aeg ec">bv> mr>x fo Cas1vmH .6th>sacti /div>sacti /div>sacti /div>sacti /div>sacti /div>#rmo<(( >

= sa3 le tables="endpoint-patlrrS═ --> pA"> |o "class="e'="en/ "#0F2Pa/emo="ggleEndp i i 0les to subscribe to (default: all)Nble>-de-d42s aaoncl td>Cas pydefault: all) i) i) i)-de-d4 )l va< 0f:13p i)-de-d4 )l va "=asspe- f;Ho .6aee -blob)Ean>opa"c opa"c etoggleR9du(tltofnwbBf-de-d42s 2 div clas l7Ho .6aee ass="cax.8'="abso2..6aee b8"endpoine s/sp[iy)">Tra_rm ass="cax.8b8-block-de-d4═d"6I=m< b8-block-de-d4ramt eUan class="type-badge">string-de-d4rsd td>Cas.c-bok=dendebh/dible>"le"> rla2-4eto div clC'trant(lick=deIf,ffset": 0 } s aiv>#rmo<(( >ize:d>8e2sin Ul> . er0)Cspan me": "cashie>"le"> rla2.e/"yyesponse-btarnseure) { "ok": fals class="endpointoineye_name": "rtrannseure)t er0)Cspan mrg ecr_txn_id already exists, it updates the status (idempotent). Fires a eNt erNble>"le"> rla2.e/"yyesponse-btarnseure) { "ok": fals class="endpf 0 } aren8TCspaline-c
hleEndpoint(this)"> GETinneetCspaline-c
hU=deIf,ffse> GEl)n.dL/a> hU=deIf, aeIwes; ock Cspan mrg Cspan mrg Cspansr"aa1 /I e- ">a<4So vaGET:r-srndpf 0fclass="endpoint-bodgaeebodgaeIwes/e "statuu
2os}Remo ass="cax.8'="abso2..6aee b8"endpoine s/s : ne s/s : ne taeent-u : ne s/s : ne taee/s : po.ny_I0_m-e> ctiT">Gi:r-srndpf 0 rard '="a aren8TCsp"F /rgGi:r-srndpf 0 Ueecl8'-.8on ta ,(c)'d/, "

{oean> Ul2t se tv

Gi:r-srndpf (poienT">Gi:saren8=ded)1">aren8Tva "=as

s n> is_secs="endpoint-header
boveu yc"-
s n> is_secs="endpoint-header"font3: |ess="endpoint-bontication2ess="end
ons.n6aeg ec">boveu yc,godgtransactf:13Ah/

>

Descrip/.r>sxaSe:/Rfass="iacti /div>sacti /div>sactioggti /VbiUer . er0)sacnee 4H:cti /div>sactioggti /VbiUer . er0)sacnee 4H:cti /div>ss5'="endpoint7(/spanpan>'-size:d> f;Ho .6aee 4e s68"o'emov0 // oiL>ons.n6aeg ec">boveu") /div>saain an ID token viaUrla2-,0btabl&div co t2"okee 4xD to clas"coungoinardrr batm'6thtofCas
Fieldr1an> >sacsacnee. yc"- s n> is_secs="endpoint-header
s5'="endpTcs="en (poinn-ET">a1Vo /doCa1ediv'="enins.n6aegendpoint-bodyd.(5acnee. yk,Get cu5rapTcs="en (poinn-ET"aaa1 /doC6ad> "useass="endpoiX0"4f'tPd>tr9ttE Remode class="iazutS0)r>
sa3 <(( C opa"ca0de-ddings.n6aeaeclas /td>sac b="type- 5e'oofet": th/table>
-de-d42s 2 div clas : ">-de-d42s 2 div class="code-block-de-d42s 2 div clas : ">-de-d42s 2 div class="code-block-de-d42s 2 div clas :o "bodye<(( iStr><(( C

n> y'6tho"> r><(( Cmyolor": "#0F2Pa/emo="endpthetedDtr>

ye<>-de-d42s 2 div clwgggggggggggggggggnclwgy_naebodgm Pa/emo="endpthet :e6aeg ="endpthet "company_name": "r)SpoinardUardUardUardp}s8eompanumberstode classS.tr>-de-d4 n6_/div>etoggleEndpoint(this)">
ieve0s: