Core API Endpoint Reference
This document summarizes active HTTP endpoints for mobile app development, dashboard, internal testing, and operational integration.
Service split (post 2026-05-30): Auth/KYC/IAM endpoints (
/auth/*) sekarang diauth_service(port 8081). Merchant/PSP/transaction endpoints tetap dimerchant_core_api(port 8080). Lihat auth service architecture.
Base URL
| Service | Local | Production |
|---|---|---|
merchant_core_api | http://localhost:8080 | https://kesles.com/merchant/api |
auth_service | http://localhost:8081 | https://kesles.com/merchant/auth-api |
Authentication Conventions
Public endpoint
No token required.
Bearer endpoint
Use the header:
Authorization: Bearer <access_token>
Endpoints that use a bearer token:
/auth/push-tokens/auth/profile/auth/account-settings/auth/profile/email/request-otp/auth/profile/email/verify-otp/merchant/registration/merchant/payment-terminal/merchant/transactions/merchant/feedback
Internal endpoint
Use the header:
X-Internal-API-Key: <INTERNAL_NOTIFICATION_API_KEY>
Internal endpoints:
/internal/notifications/whatsapp/otp/internal/notifications/push/test/internal/merchant/registration/review/internal/merchant/registration/approve/internal/merchant/status
Standard Error Format
Most endpoints return JSON errors with the following shape:
{
"error": "human readable message",
"code": "machine_readable_code",
"retry_after_secs": 60
}
retry_after_secs only appears when the endpoint actually applies a cooldown or backoff.
Merchant Status
Registration request status
pending: the application has been submitted, an active merchant has not yet been createdpending_review: the application has entered the internal review stage
Active merchant status
draftinactiveactivesuspendeddeleted
Mobile integration notes:
- only
merchant_status = activemay be considered an active merchant pendingandpending_reviewmust still be treated as activation-in-progress, not as an active merchant
Health and Observability
GET /health
- Auth: not required
- Purpose: main service health check
Example response:
{
"service": "merchant_core_api",
"status": "ok",
"db_reference_status": "connected"
}
GET /health/otp-runtime
- Auth: not required
- Purpose: check the OTP runtime store currently in use
Example response:
{
"service": "merchant_core_api",
"status": "ok",
"otp_runtime_store": "redis"
}
GET /metrics
- Auth: not required
- Purpose: expose Prometheus / plain text metrics
Public Endpoints
GET /public/legal-documents
- Auth: not required
- Required query:
code
- Source data:
legal.documents
- Currently available
codevalues:terms_of_serviceprivacy_policy
Example:
GET /public/legal-documents?code=terms_of_service
Example abbreviated response:
{
"code": "terms_of_service",
"title": "Terms of Service",
"version": "v1.0",
"status": "published",
"page_title": "Terms of Service",
"hero_title": "Kesles Merchant Terms of Service",
"hero_subtitle": "...",
"info_title": "Important Information",
"info_description": "...",
"support_title": "Need Help?",
"support_body": "...",
"sections": [
{
"title": "Service Scope",
"body": "..."
}
],
"published_at": "2026-04-08T00:00:00Z",
"updated_at": "2026-04-08T00:00:00Z"
}
Auth Endpoints
POST /auth/resolve-phone
- Auth: not required
- Purpose:
- check the phone number
- determine whether the user needs OTP or can be authenticated directly
- Request body:
phoneplatform_codedevice_iddevice_name
Example request:
{
"phone": "08114169868",
"platform_code": "mobile_user",
"device_id": "android-123",
"device_name": "Samsung A54"
}
Possible responses:
{
"status": "verification_required",
"phone": "08114169868",
"reason": "new_device",
"verification_channel": "whatsapp"
}
or
{
"status": "authenticated",
"phone": "08114169868",
"token_type": "Bearer",
"access_token": "...",
"refresh_token": "...",
"expires_in_secs": 3600
}
POST /auth/request-otp
- Auth: not required
- Purpose: request an OTP for login/device verification
- Request body:
phonemethodplatform_codedevice_iddevice_name
Example request:
{
"phone": "08114169868",
"method": "whatsapp",
"platform_code": "mobile_user",
"device_id": "android-123",
"device_name": "Samsung A54"
}
Example response:
{
"status": "otp_sent",
"request_id": "uuid",
"phone": "08114169868",
"method": "whatsapp",
"expires_in_secs": 300,
"retry_after_secs": 60
}
POST /auth/verify-otp
- Auth: not required
- Purpose: verify the OTP and issue a JWT session
- Request body:
request_idphonecodeplatform_codedevice_iddevice_name
Example response:
{
"status": "verified",
"phone": "08114169868",
"verified": true,
"token_type": "Bearer",
"access_token": "...",
"refresh_token": "...",
"expires_in_secs": 3600
}
Notes:
- after a successful OTP, the backend may also send a
login_alertpush to other active devices if a push token is available
POST /auth/firebase-phone-verify
- Auth: not required (the Firebase ID token IS the credential)
- Purpose: verify a Firebase Phone Auth ID token (issued by mobile after
signInWithCredentialsucceeds) and issue a JWT session, without going through/auth/request-otp→/auth/verify-otp - Added: Phase 1 Backend Deliverable A+B (2026-05-20) per plan
sms-otp-service-plan.md(notification site → firebase) §6.2 - Used by:
- Mode A registration when mobile UX lets user pick SMS as channel (Google sends SMS via Firebase Phone Auth)
- Mode B login auto-fallback when WhatsApp/email primary fails (deferred — Deliverable C)
- Mode B login manual switch ("Coba metode lain") (deferred — Deliverable D)
- Request body:
id_token(required) — Firebase ID token JWT fromuser.getIdToken()on mobile, formateyJ...eyJ...xxx(3 dot-separated segments, ~1000+ chars)request_id(optional) — join with prior/auth/request-otprequest for auto-fallback flowplatform_code(optional) — defaults tomobile_appdevice_id(optional) — when present, the device is trusted + persisted tosession_storedevice_name(optional) — display name for audit + push login_alertflow_mode(optional) —registration|login_primary|login_fallback(audit only)fallback_reason(optional) —whatsapp_unavailable|email_unavailable|user_switched(audit only)hint_phone(optional) — best-effort phone for audit when verify fails
Example request:
{
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"platform_code": "mobile_app",
"device_id": "android-uuid-abc",
"device_name": "Samsung A54",
"flow_mode": "registration"
}
Example response (200 OK, shape match /auth/verify-otp):
{
"status": "verified",
"phone": "+628114169868",
"verified": true,
"token_type": "Bearer",
"access_token": "...",
"refresh_token": "...",
"expires_in_secs": 3600
}
Error responses:
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | id_token empty or JSON invalid |
| 400 | unsupported_phone_region | Phone in Firebase claim is not Indonesia (only Indonesia supported in Phase 0-2) |
| 401 | invalid_firebase_token | ID token signature mismatch / expired / malformed |
| 502 | firebase_service_unreachable | services/firebase_service is down or Google JWKS not reachable |
| 503 | firebase_service_not_configured | FCM_SERVICE_BASE_URL / FCM_SERVICE_API_KEY empty, OR firebase_service has no FIREBASE_PROJECT_ID |
| 500 | user_persist_failed / token_issue_failed / firebase_verify_failed | Internal error |
Notes:
- The backend proxies verification to
services/firebase_service(which holds the Firebase Admin trust chain). Core API never holds Firebase credentials directly. - Audit log entry is written to
firebase.phone_verifications(indb_kesles_merchant_notification) byfirebase_service(single-writer pattern) AND toauth.auth_audit_logs(indb_kesles_merchant_auth, viaauth_service) by core_api (event_type=firebase_phone_verify,auth_provider=firebase_phone_auth). login_alertpush to other devices is NOT yet emitted on this path (Phase 1 scope decision — can be added in Phase 1+).
GET /auth/device-login/cancel
- Auth: not required
- Purpose: cancel a new-device login from the security link
- Required query:
token
Example response:
{
"status": "cancelled",
"message": "login on the new device was successfully cancelled"
}
POST /auth/push-tokens
- Auth: bearer token
- Purpose: register or update the device's FCM token
- Request body:
push_tokendevice_iddevice_name
Example response:
{
"status": "registered"
}
GET /auth/profile
- Auth: bearer token
- Purpose: fetch the user profile along with the active/in-progress merchant snapshot
Important response fields:
user_idfull_namedisplay_namenikgenderaddress_lineavatar_urlphonephone_verifiedemailemail_verifiedmerchant_idmerchant_namemerchant_statusmerchant_contact_phonemerchant_contact_emailmerchant_address_linemerchant_province_namemerchant_city_namemerchant_district_namemerchant_subdistrict_namemerchant_postal_codemerchant_business_type_idmerchant_business_type_namemerchant_business_scale_namemerchant_average_monthly_revenuemerchant_employee_countmerchant_reviewed_atmerchant_device_fee_service_acceptedmerchant_terms_of_service_acceptedmerchant_privacy_policy_accepted
PUT /auth/profile
- Auth: bearer token
- Purpose: update the user profile
- Request body:
full_namenikgenderaddress_lineavatar_urlemail
Example request:
{
"full_name": "Mustaqim",
"nik": "7371xxxxxxxxxxxx",
"gender": "male",
"address_line": "Jl. Deng Ramang",
"avatar_url": "https://...",
"email": "merchant@example.com"
}
The response uses the same shape as GET /auth/profile.
GET /auth/account-settings
- Auth: bearer token
- Purpose: fetch user account settings, preferences, and privacy
- Source data:
identity.user_account_settings
Example response:
{
"user_id": "uuid",
"notification_push_enabled": true,
"notification_transactions_enabled": true,
"notification_promotions_enabled": true,
"notification_daily_summary_enabled": true,
"app_language": "id",
"privacy_analytics_enabled": true,
"privacy_data_sharing_enabled": false,
"updated_at": "2026-04-08T01:10:00Z"
}
Defaults when no row exists yet:
notification_push_enabled = truenotification_transactions_enabled = truenotification_promotions_enabled = truenotification_daily_summary_enabled = trueapp_language = idprivacy_analytics_enabled = trueprivacy_data_sharing_enabled = false
PUT /auth/account-settings
- Auth: bearer token
- Purpose: update account settings
- Request body:
notification_push_enablednotification_transactions_enablednotification_promotions_enablednotification_daily_summary_enabledapp_languageprivacy_analytics_enabledprivacy_data_sharing_enabled
Example request:
{
"notification_push_enabled": true,
"notification_transactions_enabled": false,
"notification_promotions_enabled": false,
"notification_daily_summary_enabled": true,
"app_language": "id",
"privacy_analytics_enabled": true,
"privacy_data_sharing_enabled": false
}
The response uses the same shape as GET /auth/account-settings.
POST /auth/profile/email/request-otp
- Auth: bearer token
- Purpose: send an OTP for email verification
- Request body:
email
Example response:
{
"status": "otp_sent",
"email": "merchant@example.com",
"expires_in_secs": 300
}
POST /auth/profile/email/verify-otp
- Auth: bearer token
- Purpose: verify the email OTP and mark the email as verified
- Request body:
emailcode
Example response:
{
"status": "verified",
"profile": {
"user_id": "uuid",
"email": "merchant@example.com",
"email_verified": true
}
}
Merchant Endpoints
POST /merchant/registration
- Auth: bearer token
- Purpose: create a merchant registration request
- Output:
- creates a row in
merchant.merchant_registration_requests - initial status
pending - does not immediately create an active merchant
- creates a row in
Request body:
merchant_namebusiness_entity_typemerchant_typebusiness_type_idbusiness_type_codebusiness_type_nameaverage_monthly_revenueemployee_countbusiness_scale_codebusiness_scale_namereferral_codecontact_phonecontact_emailaddress_linecountry_idcountry_nameprovince_idprovince_namecity_idcity_namedistrict_iddistrict_namesubdistrict_idsubdistrict_namepostal_code_idpostal_codereview_notesdevice_fee_service_acceptedterms_of_service_acceptedprivacy_policy_accepted
Minimum required fields:
merchant_namebusiness_entity_typecontact_phonebusiness_type_idaverage_monthly_revenueemployee_count
Business notes:
business_entity_typemust be one ofPerseorangan(Individual) orBadan Usaha(Business Entity)business_type_idis validated against thedb_referencemastermerchant_typeis currently filled from the validJenis Usaha(business type) name from the master referencebusiness_scale_codeandbusiness_scale_nameare derived from the combination ofaverage_monthly_revenue + employee_countreferral_codeis optional- if
referral_codeis empty, the registration is still processed as a direct Kesles Merchant registration - if
referral_codeis provided but does not match an active partner, the registration is still processed as a direct Kesles Merchant registration - the current mobile flow sends device fee consent from the
Device Selectionstep - the current mobile flow sends
Terms of ServiceandPrivacy Policyconsent from theSummarypage - those consents are also carried over to the active merchant upon approval
Example abbreviated response:
{
"status": "pending",
"merchant": {
"id": "uuid",
"merchant_code": "MRC-0001",
"merchant_name": "Makan",
"business_type_name": "Restoran / Rumah Makan",
"average_monthly_revenue": 250000000,
"employee_count": 8,
"business_scale_name": "Kecil",
"merchant_status": "pending",
"contact_phone": "628114169868"
},
"profile": {
"merchant_status": "pending"
}
}
GET /merchant/transactions
- Auth: bearer token
- Purpose: fetch the merchant transaction summary and transaction history by period
- Optional query:
period
- Supported
periodvalues:todayweekmonthyear
- Default:
today
- Source data:
merchant.transactionsmerchant.transaction_daily_fees
Business behavior:
- only
activemerchants return transaction data inactivemerchants or users without an active merchant return a zero summary + empty list- the summary only counts transactions with
transaction_status = success net_amountin the summary is computed from total gross minus total MDR and daily service fees within the period range- the period is calculated using the
Asia/Makassartimezone
Example request:
GET /merchant/transactions?period=week
Example abbreviated response:
{
"merchant_id": "uuid",
"merchant_name": "Masa",
"merchant_status": "active",
"period": "week",
"period_start_at": "2026-04-07T00:00:00+08:00",
"period_end_at": "2026-04-09T00:00:00+08:00",
"summary": {
"gross_amount": 328000,
"mdr_fee_amount": 2296,
"service_fee_amount": 15000,
"net_amount": 310704,
"transaction_count": 17
},
"items": [
{
"id": "uuid",
"transaction_code": "TRX-DEMO-001",
"transaction_status": "success",
"payment_method": "qris",
"payment_channel": "qris_plus",
"gross_amount": 25000,
"mdr_rate": 0.007,
"mdr_fee_amount": 175,
"net_amount_after_mdr": 24825,
"daily_service_fee_amount": 5000,
"transaction_at": "2026-04-08T08:10:00+08:00"
}
]
}
GET /merchant/payment-terminal
- Auth: bearer token
- Purpose:
- fetch the primary payment terminal device of the active merchant
- fetch active/non-deleted PSP pairings for the terminal
- fetch the latest terminal activity log
- Source data:
merchant.payment_terminalsmerchant.payment_terminal_psp_pairingsmerchant.payment_terminal_activity_logs
- Notes:
- if the user's
merchant_statusis notactive, the response is still200withterminal = null - the mobile
Devicespage now reads from this endpoint
- if the user's
Example:
GET /merchant/payment-terminal
Authorization: Bearer <access_token>
Example abbreviated response:
{
"merchant_id": "b524d25f-6156-41ff-9762-dcea9921027e",
"merchant_name": "Masa",
"merchant_status": "active",
"terminal": {
"id": "41f2eab1-a258-4c89-8095-8162a53ef5f7",
"terminal_code": "TRM-QP-DEMO-001",
"brand_name": "QRIS Plus",
"product_model_name": "Q161 Pro",
"device_type_name": "V.2.1",
"firmware_version": "V.2.1",
"device_id": "QRIS-PLUS-DEMO-001",
"nmid": "ID1026000000001",
"placement_name": "Masa",
"status": "online",
"last_seen_at": "2026-04-08T00:00:00Z"
},
"psp_pairings": [
{
"psp_code": "BMRI",
"psp_name": "Bank Mandiri",
"kode_kartu": "91",
"nama_kartu": "QRIS Mandiri",
"psp_mid": "MID-BMRI-DEMO-001",
"psp_tid": "TID-BMRI-DEMO-001",
"pairing_status": "active"
}
],
"activity_logs": [
{
"event_type": "qris_displayed",
"event_title": "QRIS displayed",
"severity": "info",
"occurred_at": "2026-04-08T00:00:00Z"
}
]
}
POST /merchant/feedback
- Auth: bearer token
- Purpose: store merchant feedback from the mobile app
Request body:
category_codecategory_labelratingmessagecontact_phoneallow_follow_up
Required fields:
category_codecategory_label
Business validation:
- rating must be within a valid range
messagemust be sufficiently long
Example abbreviated response:
{
"status": "submitted",
"feedback": {
"id": "uuid",
"merchant_id": "uuid",
"category_code": "smart_bisnis",
"category_label": "Smart Bisnis",
"rating": 5,
"message": "The feature is helpful.",
"allow_follow_up": true,
"source": "mobile_app",
"created_at": "2026-04-08T01:20:00Z"
}
}
Reference Endpoints
All reference endpoints use the GET method and the standard response shape:
{
"items": []
}
GET /references/provinces
- Auth: not required
- Query: none
- Response item:
idcodename
GET /references/cities
- Auth: not required
- Required query:
province_id
- Response item:
idcodenametype
Notes:
- the city/regency name is already normalized to avoid duplicating the
Kota/Kabupatenprefix
GET /references/districts
- Auth: not required
- Required query:
city_id
- Response item:
idcodename
GET /references/subdistricts
- Auth: not required
- Required query:
district_id
- Response item:
idcodenametypelatlng
Notes:
latandlngare used by mobile to recenter the map when a kelurahan/desa is selected
GET /references/postal-codes
- Auth: not required
- Required query:
subdistrict_id
- Response item:
idcodenamelabel
Example label:
90231 - Karuwisi Utara
GET /references/business-types
- Auth: not required
- Query: none
- Source:
db_reference.public.ref_business_type
- Response item:
idcodenamecategory_idcategory_codecategory_namebenchmark_transactionsbenchmark_ticket_sizebase_operational_cost_rate
Notes:
- only active business types where
supports_qris_plus = trueare exposed - this endpoint is used by the
Business Datastep in merchant registration
GET /references/business-scales
- Auth: not required
- Query: none
- Source:
db_reference.public.ref_business_scale
- Response item:
idcodenamemodal_usaha_minmodal_usaha_maxomzet_per_bulan_minomzet_per_bulan_maxjumlah_karyawan_minjumlah_karyawan_maxsort_order
Notes:
- the business scale is currently used as the initial scoring master in the
Business Datastep - mobile reads this scale to display the
Business Scoringresult
Internal Endpoints
All internal endpoints use:
X-Internal-API-Key: <INTERNAL_NOTIFICATION_API_KEY>
POST /internal/notifications/whatsapp/otp
- Auth: internal API key
- Purpose: trigger sending a WhatsApp OTP directly
- Request body:
phonecode
Example response:
{
"status": "queued"
}
POST /internal/notifications/push/test
- Auth: internal API key
- Purpose: send a test push notification to all active tokens for the given phone
- Request body:
phonetypetitlebodyscreenreference_idmerchant_id
Required fields:
phonetitlebody
Example response:
{
"status": "queued",
"phone": "08114169868",
"sent_count": 1
}
POST /internal/merchant/registration/review
- Auth: internal API key
- Purpose: change the registration request from
pendingtopending_review - Request body:
request_idorphonereview_notesreviewed_by_user_id
Example request:
{
"phone": "08114169868",
"review_notes": "Entered internal review"
}
Example abbreviated response:
{
"status": "pending_review",
"request": {
"id": "uuid",
"merchant_status": "pending_review"
},
"profile": {
"merchant_status": "pending_review"
}
}
POST /internal/merchant/registration/approve
- Auth: internal API key
- Purpose:
- approve the merchant application
- move data from
merchant_registration_requeststomerchant.merchants - create a new row in
merchant.merchant_users - delete the registration request that has been processed
- propagate the registration consent fields to the active merchant
- set
reviewed_aton the active merchant
- Request body:
request_idorphonereview_notesreviewed_by_user_id
Example abbreviated response:
{
"status": "active",
"merchant": {
"id": "uuid",
"merchant_status": "active"
},
"profile": {
"merchant_status": "active"
}
}
POST /internal/merchant/status
- Auth: internal API key
- Purpose: update the status of an already-created active merchant
- Request body:
merchant_idorphonetarget_statusreview_notesreviewed_by_user_id
Currently supported transitions:
active -> suspendedactive -> deletedsuspended -> activesuspended -> deleted
Example request:
{
"phone": "08114169868",
"target_status": "suspended",
"review_notes": "Suspended for testing"
}
Example abbreviated response:
{
"status": "suspended",
"merchant": {
"id": "uuid",
"merchant_status": "suspended"
},
"profile": {
"merchant_status": "suspended"
}
}
WhatsApp Webhook Endpoint
The WhatsApp webhook is no longer served by merchant_core_api. It now lives in whatsapp_service (port 8091) at:
GET /webhooks/whatsapp/status— Meta webhook verification (hub.mode/hub.verify_token/hub.challenge); when valid the response body is the rawhub.challengePOST /webhooks/whatsapp/status— receive Meta delivery-status callbacks; signature verified viaX-Hub-Signature-256(HMAC usingWHATSAPP_API_SECRET)
See the whatsapp_service README for the current contract.
Route Source Files
The active routes are currently registered in the following files (under merchant_core_api/internal/httpapi/):
routes_auth.goroutes_merchant.goroutes_reference.goroutes_internal.goroutes_public.goroutes_dashboard.goroutes_partner.go
Development Notes
- this documentation reflects the routes active as of
2026-06-24 - when a new route is added, update this document along with the addition of the handler or route registry
- for the mobile app,
merchant_statusmust continue to be treated as the source of truth for merchant activation status in the UI