Skip to main content

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 di auth_service (port 8081). Merchant/PSP/transaction endpoints tetap di merchant_core_api (port 8080). Lihat auth service architecture.

Base URL

ServiceLocalProduction
merchant_core_apihttp://localhost:8080https://kesles.com/merchant/api
auth_servicehttp://localhost:8081https://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 created
  • pending_review: the application has entered the internal review stage

Active merchant status

  • draft
  • inactive
  • active
  • suspended
  • deleted

Mobile integration notes:

  • only merchant_status = active may be considered an active merchant
  • pending and pending_review must 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 code values:
    • terms_of_service
    • privacy_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:
    • phone
    • platform_code
    • device_id
    • device_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:
    • phone
    • method
    • platform_code
    • device_id
    • device_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_id
    • phone
    • code
    • platform_code
    • device_id
    • device_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_alert push 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 signInWithCredential succeeds) 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 from user.getIdToken() on mobile, format eyJ...eyJ...xxx (3 dot-separated segments, ~1000+ chars)
    • request_id (optional) — join with prior /auth/request-otp request for auto-fallback flow
    • platform_code (optional) — defaults to mobile_app
    • device_id (optional) — when present, the device is trusted + persisted to session_store
    • device_name (optional) — display name for audit + push login_alert
    • flow_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:

StatusCodeMeaning
400bad_requestid_token empty or JSON invalid
400unsupported_phone_regionPhone in Firebase claim is not Indonesia (only Indonesia supported in Phase 0-2)
401invalid_firebase_tokenID token signature mismatch / expired / malformed
502firebase_service_unreachableservices/firebase_service is down or Google JWKS not reachable
503firebase_service_not_configuredFCM_SERVICE_BASE_URL / FCM_SERVICE_API_KEY empty, OR firebase_service has no FIREBASE_PROJECT_ID
500user_persist_failed / token_issue_failed / firebase_verify_failedInternal 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 (in db_kesles_merchant_notification) by firebase_service (single-writer pattern) AND to auth.auth_audit_logs (in db_kesles_merchant_auth, via auth_service) by core_api (event_type=firebase_phone_verify, auth_provider=firebase_phone_auth).
  • login_alert push 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_token
    • device_id
    • device_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_id
  • full_name
  • display_name
  • nik
  • gender
  • address_line
  • avatar_url
  • phone
  • phone_verified
  • email
  • email_verified
  • merchant_id
  • merchant_name
  • merchant_status
  • merchant_contact_phone
  • merchant_contact_email
  • merchant_address_line
  • merchant_province_name
  • merchant_city_name
  • merchant_district_name
  • merchant_subdistrict_name
  • merchant_postal_code
  • merchant_business_type_id
  • merchant_business_type_name
  • merchant_business_scale_name
  • merchant_average_monthly_revenue
  • merchant_employee_count
  • merchant_reviewed_at
  • merchant_device_fee_service_accepted
  • merchant_terms_of_service_accepted
  • merchant_privacy_policy_accepted

PUT /auth/profile

  • Auth: bearer token
  • Purpose: update the user profile
  • Request body:
    • full_name
    • nik
    • gender
    • address_line
    • avatar_url
    • email

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 = 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

PUT /auth/account-settings

  • Auth: bearer token
  • Purpose: update account settings
  • Request body:
    • notification_push_enabled
    • notification_transactions_enabled
    • notification_promotions_enabled
    • notification_daily_summary_enabled
    • app_language
    • privacy_analytics_enabled
    • privacy_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:
    • email
    • code

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

Request body:

  • merchant_name
  • business_entity_type
  • merchant_type
  • business_type_id
  • business_type_code
  • business_type_name
  • average_monthly_revenue
  • employee_count
  • business_scale_code
  • business_scale_name
  • referral_code
  • contact_phone
  • contact_email
  • address_line
  • country_id
  • country_name
  • province_id
  • province_name
  • city_id
  • city_name
  • district_id
  • district_name
  • subdistrict_id
  • subdistrict_name
  • postal_code_id
  • postal_code
  • review_notes
  • device_fee_service_accepted
  • terms_of_service_accepted
  • privacy_policy_accepted

Minimum required fields:

  • merchant_name
  • business_entity_type
  • contact_phone
  • business_type_id
  • average_monthly_revenue
  • employee_count

Business notes:

  • business_entity_type must be one of Perseorangan (Individual) or Badan Usaha (Business Entity)
  • business_type_id is validated against the db_reference master
  • merchant_type is currently filled from the valid Jenis Usaha (business type) name from the master reference
  • business_scale_code and business_scale_name are derived from the combination of average_monthly_revenue + employee_count
  • referral_code is optional
  • if referral_code is empty, the registration is still processed as a direct Kesles Merchant registration
  • if referral_code is 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 Selection step
  • the current mobile flow sends Terms of Service and Privacy Policy consent from the Summary page
  • 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 period values:
    • today
    • week
    • month
    • year
  • Default:
    • today
  • Source data:
    • merchant.transactions
    • merchant.transaction_daily_fees

Business behavior:

  • only active merchants return transaction data
  • inactive merchants or users without an active merchant return a zero summary + empty list
  • the summary only counts transactions with transaction_status = success
  • net_amount in 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/Makassar timezone

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_terminals
    • merchant.payment_terminal_psp_pairings
    • merchant.payment_terminal_activity_logs
  • Notes:
    • if the user's merchant_status is not active, the response is still 200 with terminal = null
    • the mobile Devices page now reads from this endpoint

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_code
  • category_label
  • rating
  • message
  • contact_phone
  • allow_follow_up

Required fields:

  • category_code
  • category_label

Business validation:

  • rating must be within a valid range
  • message must 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:
    • id
    • code
    • name

GET /references/cities

  • Auth: not required
  • Required query:
    • province_id
  • Response item:
    • id
    • code
    • name
    • type

Notes:

  • the city/regency name is already normalized to avoid duplicating the Kota/Kabupaten prefix

GET /references/districts

  • Auth: not required
  • Required query:
    • city_id
  • Response item:
    • id
    • code
    • name

GET /references/subdistricts

  • Auth: not required
  • Required query:
    • district_id
  • Response item:
    • id
    • code
    • name
    • type
    • lat
    • lng

Notes:

  • lat and lng are 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:
    • id
    • code
    • name
    • label

Example label:

  • 90231 - Karuwisi Utara

GET /references/business-types

  • Auth: not required
  • Query: none
  • Source:
    • db_reference.public.ref_business_type
  • Response item:
    • id
    • code
    • name
    • category_id
    • category_code
    • category_name
    • benchmark_transactions
    • benchmark_ticket_size
    • base_operational_cost_rate

Notes:

  • only active business types where supports_qris_plus = true are exposed
  • this endpoint is used by the Business Data step in merchant registration

GET /references/business-scales

  • Auth: not required
  • Query: none
  • Source:
    • db_reference.public.ref_business_scale
  • Response item:
    • id
    • code
    • name
    • modal_usaha_min
    • modal_usaha_max
    • omzet_per_bulan_min
    • omzet_per_bulan_max
    • jumlah_karyawan_min
    • jumlah_karyawan_max
    • sort_order

Notes:

  • the business scale is currently used as the initial scoring master in the Business Data step
  • mobile reads this scale to display the Business Scoring result

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:
    • phone
    • code

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:
    • phone
    • type
    • title
    • body
    • screen
    • reference_id
    • merchant_id

Required fields:

  • phone
  • title
  • body

Example response:

{
"status": "queued",
"phone": "08114169868",
"sent_count": 1
}

POST /internal/merchant/registration/review

  • Auth: internal API key
  • Purpose: change the registration request from pending to pending_review
  • Request body:
    • request_id or phone
    • review_notes
    • reviewed_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_requests to merchant.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_at on the active merchant
  • Request body:
    • request_id or phone
    • review_notes
    • reviewed_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_id or phone
    • target_status
    • review_notes
    • reviewed_by_user_id

Currently supported transitions:

  • active -> suspended
  • active -> deleted
  • suspended -> active
  • suspended -> 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 raw hub.challenge
  • POST /webhooks/whatsapp/status — receive Meta delivery-status callbacks; signature verified via X-Hub-Signature-256 (HMAC using WHATSAPP_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.go
  • routes_merchant.go
  • routes_reference.go
  • routes_internal.go
  • routes_public.go
  • routes_dashboard.go
  • routes_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_status must continue to be treated as the source of truth for merchant activation status in the UI