Skip to main content

Merchant Onboarding Service

1. Overview

The Merchant Onboarding Service is a dedicated microservice inside the CDE project (peakpos-cde). Its sole purpose is to bridge the gap between the gateway's TransIT credentials and physical payment devices that need those credentials. It handles TransIT MerchantActivation API calls and pushes resulting credentials to NexGO's XTMS device management system.

Why this service exists: TransIT credentials (deviceID, transactionKey) must never leave the CDE boundary. External systems (e.g., PeakPOS Management API) can only trigger provisioning workflows — they never see, receive, or transmit raw credentials.

Credential model: Each location is assigned a credential profile (the TransIT "var sheet"). Activation provisions one GATEWAY profile per location — there is no per-channel (web/mobile/POS) credential split. The origination channel of a payment is recorded on transactions.source (GATEWAY / ONLINE / SEMI_INTEGRATED), not by selecting a different credential profile.


2. Responsibilities

ResponsibilityDescription
Merchant activationCall TransIT MerchantActivation API to generate transactionKey + activate deviceID
Device credential pushPush TransIT credentials to NexGO XTMS by device serial number
Credential storageEncrypt and store TransIT credentials in CDE Spanner via Cloud KMS
Credential rotationRotate transactionKeys on demand or on schedule
Provisioning audit trailImmutable log of every provisioning action (who, what, when, outcome)
Job orchestrationManage async provisioning jobs with retry, dead-letter, and manual replay
Status reportingReturn provisioning job status to callers (without exposing credentials)

3. Architecture Context

┌──────────────────────────────────────────────┐
│ GCP Project: peakpos (OUT of CDE) │
│ │
│ Management API │
│ │ │
│ │ POST /api/v1/provisioning/jobs │
│ │ OAuth token (scope: provision:request) │
│ │ Body: { locationId, deviceSerial } │
│ │ Response: { jobId, status } │
└────┼─────────────────────────────────────────┘

│ Cloud Run IAM-gated HTTPS

┌────┼──────────────────────────────────────────────────────────────┐
│ ▼ │
│ Merchant Onboarding Service (CDE project) │
│ │ │
│ ├──→ TransIT MerchantActivation API (get credentials) │
│ ├──→ CDE Spanner (store encrypted credentials) │
│ ├──→ Cloud KMS (encrypt/decrypt credential material) │
│ └──→ NexGO XTMS API (push credentials to device by serial) │
│ │
│ GCP Project: peakpos-cde (PCI SCOPED) │
└───────────────────────────────────────────────────────────────────┘

4. API Specification

Authentication

All endpoints require a Gateway OAuth token issued by the Gateway Auth Service:

Authorization: Bearer <gateway-oauth-token>

Tokens are scoped — callers can only access endpoints matching their granted scopes.

Operationally, provisioning authorization is anchored to the location being activated and the organization that owns it.

4.1 External API (called by clients like PeakPOS Management API)

Create Provisioning Job

POST /api/v1/provisioning/jobs

Required Scope: provision:request

Request:
{
"locationId": "loc_abc123",
"deviceSerial": "N5-2024-001234",
"action": "ACTIVATE", // ACTIVATE | DEACTIVATE | ROTATE_KEY
"callbackUrl": "https://api.peakgateway.co/management/webhooks/provisioning", // optional
"requestedBy": "user_xyz789", // identity of the human who initiated this
"metadata": {
"storeId": "store_456",
"terminalId": "term_789",
"notes": "New c-store install at 123 Main St"
}
}

Response (202 Accepted):
{
"jobId": "prov_job_abc123",
"status": "QUEUED",
"locationId": "loc_abc123",
"deviceSerial": "N5-2024-001234",
"action": "ACTIVATE",
"createdAt": "2026-03-08T15:00:00Z",
"estimatedCompletionSeconds": 30
}

Note: 202 (not 200) — provisioning is async. The response contains a job ID for polling or the caller can register a callback URL.

Get Job Status

GET /api/v1/provisioning/jobs/{jobId}

Required Scope: provision:request

Response (200):
{
"jobId": "prov_job_abc123",
"status": "COMPLETED", // QUEUED | IN_PROGRESS | COMPLETED | FAILED | RETRYING
"locationId": "loc_abc123",
"deviceSerial": "N5-2024-001234",
"action": "ACTIVATE",
"steps": [
{
"step": "TRANSIT_ACTIVATION",
"status": "COMPLETED",
"completedAt": "2026-03-08T15:00:05Z"
},
{
"step": "CREDENTIAL_STORAGE",
"status": "COMPLETED",
"completedAt": "2026-03-08T15:00:06Z"
},
{
"step": "XTMS_PUSH",
"status": "COMPLETED",
"completedAt": "2026-03-08T15:00:12Z"
}
],
"createdAt": "2026-03-08T15:00:00Z",
"completedAt": "2026-03-08T15:00:12Z",
"error": null
}

Note: Steps are visible for transparency. Credential values are NEVER included in any response.

List Jobs

GET /api/v1/provisioning/jobs?locationId={locationId}&status={status}&page={page}&size={size}

Required Scope: provision:request

Response (200):
{
"jobs": [ ... ],
"page": 1,
"size": 20,
"totalElements": 3
}

Cancel Job

POST /api/v1/provisioning/jobs/{jobId}/cancel

Required Scope: provision:request

Response (200):
{
"jobId": "prov_job_abc123",
"status": "CANCELED",
"canceledAt": "2026-03-08T15:00:03Z",
"canceledBy": "user_xyz789"
}

Only cancelable if status is QUEUED. In-progress jobs cannot be canceled.

Retry Failed Job

POST /api/v1/provisioning/jobs/{jobId}/retry

Required Scope: provision:request

Response (202 Accepted):
{
"jobId": "prov_job_abc123",
"status": "QUEUED",
"retryCount": 2,
"retriedAt": "2026-03-08T16:00:00Z"
}

4.2 Internal API (called by other CDE services only)

Internal route families are exposed under /internal/locations. Callers pass the location id they are authorized to operate on.

Get Credential Metadata

The credential metadata endpoint is VPC-internal only, never exposed through the API Gateway, service-account authenticated, and fully audit-logged. It reads the single active credential_profiles row for the location and returns only API-safe metadata. The live TSYS transaction key is never returned by this endpoint; callers rotate keys server-side with the rotation endpoint below.

GET /internal/locations/{locationId}/credentials

Required: Service-to-service IAM (not OAuth; internal VPC only)

Response (200):
{
"locationId": "loc_abc123",
"deviceId": "88700000123456",
"activatedAt": "2026-03-08T15:00:05Z",
"lastRotatedAt": null
}

Rotate Credentials

POST /internal/locations/{locationId}/rotate

Required: Service-to-service IAM
Caller: Processing Service or scheduled job

Response (200):
{
"locationId": "loc_abc123",
"rotatedAt": "2026-03-08T15:00:00Z",
"previousKeyDeactivated": true,
"xtmsPushStatus": "COMPLETED"
}

TransIT Activation Metadata and Rotation

Activation endpoints return metadata only and never return the plaintext activation password. Use the rotation endpoint to rotate the TSYS transaction key server-side.

GET /internal/locations/{locationId}/transit-activation
POST /internal/locations/{locationId}/transit-activation/rotate
POST /internal/locations/{locationId}/transit-activation
POST /internal/locations/{locationId}/transit-activation/reuse
GET /internal/locations/{locationId}/transit-state

5. Provisioning Job State Machine

┌─────────┐
┌──────────>│ QUEUED │
│ └────┬────┘
│ │
(retry) │ worker picks up
│ ▼
│ ┌──────────────┐
├───────────│ IN_PROGRESS │
│ └──┬───────┬───┘
│ │ │
│ success failure
│ │ │
│ ▼ ▼
│ ┌───────────┐ ┌────────────┐
│ │ COMPLETED │ │ RETRYING │──── max retries exceeded ──→ FAILED
│ └───────────┘ └─────┬──────┘
│ │
└─────────────────────────┘

CANCELED ← (only from QUEUED, via cancel endpoint)

Job Steps (executed sequentially)

StepActionRollback on failure
TRANSIT_ACTIVATIONCall TransIT MerchantActivation APINone (idempotent — can retry safely)
CREDENTIAL_STORAGEEncrypt credentials via KMS, store in CDE SpannerDelete stored record
XTMS_PUSHPush credentials to NexGO XTMS by device serialMark as pending manual intervention

Retry Policy

Failure typeRetry strategy
TransIT API timeout/5xxExponential backoff: 5s, 15s, 45s (max 3 retries)
TransIT API 4xxNo retry (invalid request — fail immediately)
XTMS API timeout/5xxExponential backoff: 10s, 30s, 90s (max 3 retries)
XTMS API 4xxNo retry — fail and alert
KMS/Spanner errorsExponential backoff: 2s, 4s, 8s (max 3 retries)
Max retries exceededMove to dead-letter queue, alert on-call, status = FAILED

6. IAM & Service Account Matrix

Service Accounts

Service AccountGCP ProjectPurpose
merchant-onboarding-sa@peakpos-cdepeakpos-cdeRuns the Merchant Onboarding Service
processing-sa@peakpos-cdepeakpos-cdeProcessing Service — calls internal credential endpoint
gateway-auth-sa@peakpos-cdepeakpos-cdeGateway Auth Service — validates OAuth tokens
mgmt-api-sa@peakpospeakposManagement API — calls external provisioning API

IAM Role Bindings

PrincipalRoleResourcePurpose
merchant-onboarding-saroles/secretmanager.secretAccessorTransIT developer credentials secretRead TransIT developerID/developerKey
merchant-onboarding-saroles/cloudkms.cryptoKeyEncrypterDecrypterCredential encryption keyEncrypt/decrypt merchant transactionKeys
merchant-onboarding-saroles/spanner.databaseUserCDE Spanner instanceRead/write provisioning tables
merchant-onboarding-saroles/pubsub.publisherprovisioning-events topicPublish provisioning audit events
processing-saroles/run.invokerMerchant Onboarding Service (internal)Call internal credential endpoint
mgmt-api-sa@peakposroles/run.invokerMerchant Onboarding Service (external)Call external provisioning API
mgmt-api-sa@peakpos(none on secrets/KMS/Spanner)Cannot access credentials, keys, or CDE data

OAuth Scope Matrix

ScopeWho gets itWhat it allows
provision:requestPeakPOS Management API, Gateway Portal usersCreate/view/cancel/retry provisioning jobs
provision:adminGateway Portal admins onlyView audit logs, manual replay, configuration
merchant:activatePeakPOS Management API, Gateway Portal usersTrigger merchant activation in TransIT
credentials:readProcessing Service only (via IAM, not OAuth)Read raw TransIT credentials
credentials:rotateProcessing Service, scheduled jobs (via IAM)Rotate TransIT credentials

7. Data Model (CDE Spanner)

provisioning_jobs

CREATE TABLE provisioning_jobs (
job_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
location_id VARCHAR(50) NOT NULL,
device_serial VARCHAR(100) NOT NULL,
action VARCHAR(20) NOT NULL, -- ACTIVATE, DEACTIVATE, ROTATE_KEY
status VARCHAR(20) NOT NULL, -- QUEUED, IN_PROGRESS, COMPLETED, FAILED, RETRYING, CANCELED
requested_by VARCHAR(100) NOT NULL, -- OAuth subject (user ID)
client_id VARCHAR(100) NOT NULL, -- OAuth client (e.g., "peakpos-mgmt-api")
callback_url VARCHAR(500),
metadata JSONB DEFAULT '{}',
current_step VARCHAR(30), -- Current step being executed
retry_count INTEGER DEFAULT 0,
error_message TEXT,
error_code VARCHAR(20),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
canceled_at TIMESTAMPTZ
);

CREATE INDEX idx_prov_jobs_location ON provisioning_jobs (location_id);
CREATE INDEX idx_prov_jobs_status ON provisioning_jobs (status);
CREATE INDEX idx_prov_jobs_client ON provisioning_jobs (client_id);

credential_profiles (encrypted)

Credential profiles are the authoritative encrypted credential surface. A profile is owned by an organization and assigned to one or more locations. Each profile represents the TSYS TransIT credential set needed for processing; there is no per-channel credential split. Origination channel is recorded on transactions.source (GATEWAY / ONLINE / SEMI_INTEGRATED), not by selecting a different credential profile.

CREATE TABLE credential_profiles (
credential_profile_id VARCHAR(64) PRIMARY KEY,
organization_id VARCHAR(64) NOT NULL,
processor_type VARCHAR(32) NOT NULL,
processor_platform VARCHAR(32) NOT NULL,
encrypted_payload BYTEA NOT NULL,
status VARCHAR(20) NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);

provisioning_audit_log (append-only)

CREATE TABLE provisioning_audit_log (
log_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
job_id UUID NOT NULL,
event_type VARCHAR(50) NOT NULL, -- See audit events below
actor VARCHAR(100) NOT NULL, -- User ID or service account
actor_type VARCHAR(20) NOT NULL, -- USER, SERVICE, SYSTEM
client_id VARCHAR(100), -- OAuth client ID
location_id VARCHAR(50) NOT NULL,
device_serial VARCHAR(100),
details JSONB DEFAULT '{}', -- Step-specific details (NEVER contains credentials)
ip_address VARCHAR(45),
timestamp TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_prov_audit_location ON provisioning_audit_log (location_id);
CREATE INDEX idx_prov_audit_job ON provisioning_audit_log (job_id);
CREATE INDEX idx_prov_audit_time ON provisioning_audit_log (timestamp);

8. Audit Events

Every action produces an immutable audit log entry. Credential values are NEVER logged.

Event TypeTriggerDetails captured
PROVISION_REQUESTEDJob created via external APIlocationId, deviceSerial, action, requestedBy, clientId, IP
PROVISION_STARTEDWorker picks up jobjobId, step
TRANSIT_ACTIVATION_SUCCESSTransIT MerchantActivation returns 200locationId, transitMerchantId (not keys), responseCode
TRANSIT_ACTIVATION_FAILEDTransIT returns errorlocationId, errorCode, errorMessage, retryCount
CREDENTIAL_STOREDEncrypted credential written to SpannerlocationId, keyVersion, kmsKeyName
CREDENTIAL_ROTATEDKey rotation completedlocationId, previousKeyVersion, newKeyVersion
CREDENTIAL_ACCESSEDProcessing Service reads credentialslocationId, accessedBy (service account), purpose
XTMS_PUSH_SUCCESSXTMS accepts credential pushlocationId, deviceSerial, xtmsResponseCode
XTMS_PUSH_FAILEDXTMS rejects or times outlocationId, deviceSerial, errorCode, retryCount
PROVISION_COMPLETEDAll steps finished successfullyjobId, totalDurationMs
PROVISION_FAILEDMax retries exceededjobId, failedStep, errorCode, errorMessage
PROVISION_CANCELEDJob canceled by userjobId, canceledBy, IP
PROVISION_RETRIEDManual retry triggeredjobId, retriedBy, retryCount
CREDENTIAL_DEACTIVATEDLocation deactivatedlocationId, deactivatedBy, reason

9. Security Controls

Credential Handling

  • Encryption at rest: All TransIT credentials encrypted via Cloud KMS (AES-256-GCM) before Spanner write
  • Encryption in transit: TLS 1.2+ on all connections (internal and external)
  • Ephemeral in memory: Credentials decrypted only for the duration of the XTMS push call, then zeroed
  • No logging of secrets: Credential values excluded from all application logs, audit logs, and error messages
  • No credential return: External API never returns credential values — only job status

Network Isolation

  • Service runs on Cloud Run in the CDE VPC
  • External API reachable only through Gateway API Gateway (Cloud Endpoints) with OAuth validation
  • Internal API reachable only via VPC-internal Cloud Run URL (no public endpoint)
  • Egress restricted to: TransIT API, XTMS API, CDE Spanner, Cloud KMS, Pub/Sub

Rate Limiting

EndpointLimitPer
POST /api/v1/provisioning/jobs10/minclient_id
GET /api/v1/provisioning/jobs/{jobId}60/minclient_id
GET /internal/locations/{locationId}/credentials100/minservice account

10. Pub/Sub Events

The service publishes events to the provisioning-events topic for async consumers.

{
"eventType": "PROVISION_COMPLETED",
"jobId": "prov_job_abc123",
"locationId": "loc_abc123",
"deviceSerial": "N5-2024-001234",
"action": "ACTIVATE",
"timestamp": "2026-03-08T15:00:12Z",
"steps": [
{ "step": "TRANSIT_ACTIVATION", "status": "COMPLETED", "durationMs": 5000 },
{ "step": "CREDENTIAL_STORAGE", "status": "COMPLETED", "durationMs": 1000 },
{ "step": "XTMS_PUSH", "status": "COMPLETED", "durationMs": 6000 }
]
}

Subscribers

SubscriberEvent typesPurpose
Gateway PortalAllReal-time status updates in UI
Callback deliveryPROVISION_COMPLETED, PROVISION_FAILEDPOST to caller's callbackUrl
AlertingPROVISION_FAILED, XTMS_PUSH_FAILEDPagerDuty alert for on-call

11. XTMS API Integration

The NexGO XTMS Cloud Open API (V1.9) provides full programmatic access for device provisioning.

Authentication

All XTMS API calls use SHA-256 signature-based authentication:

  1. Collect all request parameters (excluding signature)
  2. Sort by parameter name in ASCII/lexicographical order
  3. Concatenate as URL key-value pairs: key1=value1&key2=value2...
  4. Append the API key: stringA + key
  5. SHA-256 hash → uppercase hex string

Required per-request fields:

FieldDescription
appIdDistributor identifier (provided by NexGO)
keyAPI secret key (provided by NexGO)
signatureMethodAlways SHA-256
signatureNonce13-digit timestamp (unique per request)
timestampUTC time: yyyy-MM-dd'T'HH:mm:ssZ
versionAlways v1

XTMS app credentials are managed secrets, not an implementation blocker: xtms-app-id and xtms-app-key are injected into merchant-onboarding as XTMS_APP_ID and XTMS_KEY. Rotate them through the secrets rotation runbook.

Provisioning Flow (XTMS_PUSH step detail)

The XTMS_PUSH job step executes the following XTMS API calls in sequence:

Step 1: Create XTMS Merchant

POST /openapi/merchant/add

Request:
{
"merchantName": "Store ABC",
"merchantNo": "MID_12345", // TransIT merchantId
"terminalNos": ["TID_001", "TID_002"] // TransIT terminal IDs
}

Response:
{ "status": 200, "detail": 10086 } // XTMS merchant ID

Error codes: 1005001 (merchant already exists), 1005003 (TID duplicated)

Step 2: Bind Device to Terminal

POST /openapi/terminal/bindMachine

Request:
{
"merchantNo": "MID_12345",
"sn": "N5-2024-001234", // Physical device serial
"terminalNo": "TID_001"
}

Response:
{ "status": 200, "detail": null }

Rules: One device → one terminal. Device must belong to our distributor account. Error codes: 1008003 (terminal already bound), 1008004 (device already bound), 1007 (SN not owned)

Step 3: Create Parameter File with TransIT Credentials

POST /openapi/param/addParamFile

Request:
{
"fileName": "transit_creds_MID_12345_TID_001",
"remark": "TransIT credentials for merchant MID_12345",
"spcParam": "{\"TRANSIT_DEVICE_ID\":\"88700000123456\",\"TRANSIT_TXN_KEY\":\"encrypted_key_value\"}"
}

Response:
{
"status": 200,
"detail": {
"id": 695,
"fileName": "transit_creds_MID_12345_TID_001",
"pkey": "ebf6deaaa3933c1ffffdd7eba20883da" // Used for subsequent updates
}
}

SPC = Specific Parameter Configuration. Custom key-value pairs delivered to the SmartConnect app. The isEncryption: 1 flag on parameter items hides values in the XTMS UI.

Step 4: Push Parameters to Device

POST /openapi/paramPush/pushBySN

Request:
{
"snList": ["N5-2024-001234"],
"appPackageName": "com.nexgo.smartconnect", // SmartConnect package name
"paramId": 695 // Parameter file ID from step 3
}

Response:
{ "status": 200, "detail": null }

Rules: Max 5000 SNs per push. Creates async task — device must be online to receive.

Step 5: Monitor Push Status

POST /openapi/paramPush/getPushDetail

Request:
{
"pushId": 2,
"sn": "N5-2024-001234",
"page": 1,
"pageSize": 10
}

Response:
{
"status": 200,
"detail": {
"processingMachineCount": 0,
"successMachineCount": 1,
"failMachineCount": 0,
"pageData": {
"rows": [
{
"sn": "N5-2024-001234",
"state": 3, // 3 = Success
"updateTime": "2026-03-08T15:00:12Z"
}
]
}
}
}

Push states: 0 Not requested, 1 Requesting, 2 Downloading, 3 Success, 4 Failed, 5 Canceled, 9 Ignored

Additional XTMS Endpoints Used

EndpointMethodPurpose
/openapi/merchant/updateInfoPOSTUpdate merchant name/contact info
/openapi/merchant/detailPOSTGet merchant details by ID
/openapi/terminal/unboundMachinePOSTUnbind device from terminal (deprovisioning)
/openapi/terminal/getListPOSTList terminals and binding status
/openapi/param/specific/updatePOSTUpdate SPC parameters (credential rotation)
/openapi/paramPush/cancelBySnPOSTCancel pending push for a device
/openapi/paramPush/closeByIdPOSTClose a completed push task
/openapi/paramPush/getPushListBySNPOSTGet all push tasks for a specific device

XTMS Status Codes

CodeDescription
200Success
1001General failure
1005Distributor does not exist or lacks API permissions
1006Signature verification failed
1007SN does not match distributor ownership
1008No permission for this interface
1005001Merchant already exists
1005002Merchant does not belong to distributor
1005003TID is duplicated
1005004Terminal already bound to a device
1008003Terminal already bound to another merchant terminal
1008004Merchant terminal already bound to another device
1008006No authority to bind device to this terminal
9999Unknown error
-1System exception

Easy Deploy (Alternative for Bulk Provisioning)

XTMS Easy Deploy (V1.8+) bundles APP + PARAM + OTA + RKI tasks into a single template. Useful for fleet provisioning:

POST /openapi/easydpl/template/create // Create template (max 10 tasks)
POST /openapi/easydpl/push/create // Push template to devices by SN
POST /openapi/easydpl/push/list // List push tasks
POST /openapi/easydpl/push/detail // Track per-device status

Consider using Easy Deploy for initial fleet rollout (SmartConnect app + TransIT credentials + firmware in one push).

Gateway vs NexGO Cloud / MDM Boundary

Gateway uses XTMS for merchant/device provisioning, terminal binding and unbinding, SmartConnect SPC parameter creation/update, TransIT credential pushes, credential rotation, and push-status inspection. The Android SDK owns the SmartConnect runtime protocol and exposes Gateway runtime commands for runtime wipe, credential rotation, reconciliation trigger, SDK state reset, payment-runtime reprovision, SmartConnect prepare, and SmartConnect status.

XTMS app deployment, firmware/OTA, RKI/certificate pushes, Easy Deploy template administration, and other fleet MDM functions remain NexGO Cloud / operations scope unless a specific workflow is promoted into the Gateway runtime-command surface. POS applications should not call XTMS directly.


KeyLabelSecret
PROCESSORTSYSNo
MERCHANT_IDTransIT Merchant ID (MID)No
USER_IDTransIT User IDNo
PASSWORDPasswordYes
DEVICE_IDTransIT Device IDNo
FCS_IDFCS ID (Optional)No
HOST_URLHost URLNo

Manual copy workflow:

  1. Provisioning completes TRANSIT_ACTIVATION and CREDENTIAL_STORAGE.
  2. Authorized support/admin opens the manual Nexgo credential package in the gateway-controlled workflow.
  3. UI displays masked values with explicit copy controls for the selected Nexgo device.
  4. Operator copies the values onto the device using the approved Nexgo device credential entry path.
  5. Operator confirms completion in the gateway workflow.
  6. Audit log records who viewed the raw package and when manual copy was confirmed.

This preserves the audit trail and keeps raw credentials inside the CDE boundary. This mode is a degraded fallback for devices that cannot receive credentials through the API push path.