Online Transaction Service
1. Overview
The Online Transaction Microservice handles e-commerce payment flows — checkout session management, hosted payment page backend, tokenized card-on-file payments, webhook delivery, and fraud screening. It delegates actual payment processing to the Processing Microservice.
Rename note: the current gateway tenant model is Organization -> Location. Legacy
merchantIdfields remain in parts of the online transaction wire contract; on this page they should be read as concrete location ids, not as a separate merchant-first tenant model.
1.1 Rename guardrails
Safe statements:
- hosted checkout and hosted payment flows still accept legacy
merchantIdfields in parts of the wire contract - those values identify the selected concrete location
- new docs and app state should still prefer organization/location terminology
Unsafe statements:
- that hosted-payment APIs are exempt from the organization/location rename
- that
merchantIdin these examples means a different tenant layer from Location - that new SDK or product documentation should keep introducing merchant-first names because the wire alias still exists
2. Responsibilities
| Responsibility | Description |
|---|---|
| Checkout session management | Create, query, expire checkout sessions |
| Payment form backend | Validate and process payment form submissions |
| Token management | Store and retrieve tokenized card references for card-on-file |
| Subscription management | Create/update/cancel recurring billing subscriptions on behalf of merchants |
| Webhook delivery | Deliver payment event webhooks to merchant callback URLs |
| Fraud screening | Basic velocity checks, IP reputation (extensible) |
| Hosted payment flows | Manage hosted payment button and iframe integrations for quick merchant onboarding |
| Merchant API key management | Issue, rotate, validate API keys for SDK/API access |
3. API Specification
Authentication
Two auth modes:
| Context | Auth Method |
|---|---|
| Server-to-server (SDK) | Authorization: Bearer <api-key> |
| Checkout page (browser) | Session-based (checkout session ID in URL) |
Endpoints
Checkout Sessions
POST /api/v1/checkout/sessions
GET /api/v1/checkout/sessions/{sessionId}
POST /api/v1/checkout/sessions/{sessionId}/pay
POST /api/v1/checkout/sessions/{sessionId}/cancel
POST /api/v1/checkout/sessions
Authorization: Bearer <api-key>
Request:
{
"amount": 2500,
"currency": "USD",
"description": "Order #12345",
"customerEmail": "customer@example.com",
"successUrl": "https://merchant.com/success",
"cancelUrl": "https://merchant.com/cancel",
"webhookUrl": "https://merchant.com/webhooks",
"metadata": { "orderId": "12345" },
"expiresIn": 3600,
"allowedPaymentMethods": ["card"],
"captureMethod": "automatic"
}
Response (201):
{
"sessionId": "cs_live_abc123",
"checkoutUrl": "https://pay.pinpointgateway.com/c/cs_live_abc123",
"status": "PENDING",
"expiresAt": "2026-03-03T16:00:00Z"
}
Process Payment (called by checkout page)
POST /api/v1/checkout/sessions/{sessionId}/pay
Request:
{
"cardNumber": "4111111111111111",
"expirationDate": "1226",
"cvv": "123",
"cardHolderName": "John Doe",
"billingZip": "28202",
"saveCard": false
}
Response (200):
{
"status": "SUCCEEDED",
"transactionId": "txn_abc123",
"redirectUrl": "https://merchant.com/success?session_id=cs_live_abc123"
}
Response (200 - Declined):
{
"status": "DECLINED",
"message": "Card was declined. Please try a different payment method.",
"retryAllowed": true
}
Card-on-File (Tokenized Payments)
POST /api/v1/tokens
GET /api/v1/tokens?customerId=...
DELETE /api/v1/tokens/{tokenId}
POST /api/v1/payments/token
POST /api/v1/payments/token
Authorization: Bearer <api-key>
Request:
{
"tokenId": "tok_xyz789",
"amount": 2500,
"currency": "USD",
"description": "Recurring charge",
"metadata": { "subscriptionId": "sub_001" }
}
Response (200):
{
"transactionId": "txn_def456",
"status": "APPROVED",
"processedAmount": 2500
}
Hosted Payment Button
POST /api/v1/hosted/button
Authorization: Bearer <api-key>
Request:
{
"merchantId": "loc_abc123",
"amount": 2500,
"currency": "USD",
"description": "Payment",
"successUrl": "https://merchant.com/success",
"cancelUrl": "https://merchant.com/cancel"
}
Response (201):
{
"buttonId": "btn_abc123",
"embedCode": "<script src=\"https://pay.pinpointgateway.com/button/btn_abc123.js\"></script>",
"checkoutUrl": "https://pay.pinpointgateway.com/b/btn_abc123"
}
Sale-only flow (no refund/void through button). Certification: Hosted Payment 2.0 — described as a "really simple certification process." Embeddable JavaScript snippet for merchant websites. Could be the first certified integration due to simplest certification path.
Hosted Payment iFrame
POST /api/v1/hosted/iframe-session
Authorization: Bearer <api-key>
Request:
{
"merchantId": "loc_abc123",
"amount": 2500,
"currency": "USD",
"description": "Order #12345",
"theme": {
"primaryColor": "#1a73e8"
}
}
Response (201):
{
"sessionId": "ifs_abc123",
"iframeUrl": "https://pay.pinpointgateway.com/iframe/ifs_abc123",
"expiresAt": "2026-03-03T16:00:00Z"
}
Embeddable iFrame for seamless checkout within merchant's site. Secure cross-origin communication via postMessage API. Prioritized for speed — implement before certified backend portal. Card data stays within hosted iFrame (TSYS domain), reducing PCI scope.
API Key Management
POST /api/v1/api-keys # Generate new API key
GET /api/v1/api-keys # List API keys (masked)
DELETE /api/v1/api-keys/{keyId} # Revoke API key
POST /api/v1/api-keys/{keyId}/rotate # Rotate key
Subscriptions (Recurring Billing)
Merchant-facing subscription API. Proxies to Processing Microservice's subscription engine. All endpoints require Authorization: Bearer <api-key>.
POST /api/v1/subscriptions # Create subscription
GET /api/v1/subscriptions # List subscriptions
GET /api/v1/subscriptions/{subscriptionId} # Get subscription details
PATCH /api/v1/subscriptions/{subscriptionId} # Update subscription
POST /api/v1/subscriptions/{subscriptionId}/pause # Pause billing
POST /api/v1/subscriptions/{subscriptionId}/resume # Resume billing
POST /api/v1/subscriptions/{subscriptionId}/cancel # Cancel subscription
GET /api/v1/subscriptions/{subscriptionId}/billing-history # Billing history
POST /api/v1/subscriptions
Authorization: Bearer <api-key>
Request:
{
"customerId": "cust_001",
"customerEmail": "customer@example.com",
"tokenId": "tok_xyz789",
"planName": "Monthly Premium",
"amount": 4999,
"currency": "USD",
"interval": "MONTHLY",
"intervalCount": 1,
"startDate": "2026-04-01",
"endDate": null,
"maxCycles": null,
"webhookUrl": "https://merchant.com/webhooks/subscription",
"metadata": { "planId": "plan_premium" }
}
Response (201):
{
"subscriptionId": "sub_abc123",
"customerId": "cust_001",
"status": "ACTIVE",
"nextBillingDate": "2026-04-01",
"createdAt": "2026-03-07T10:00:00Z"
}
Subscription + Checkout flow: Merchants can combine checkout sessions with subscription creation — the first checkout collects card data and creates a Guardian token, then a subscription is created using that token.
POST /api/v1/checkout/sessions
{
"amount": 4999,
"description": "Monthly Premium — First Payment",
"saveCard": true,
"createSubscription": {
"planName": "Monthly Premium",
"interval": "MONTHLY",
"startDate": "2026-05-01"
}
}
On successful payment: token is stored, subscription is created with startDate for next billing cycle. First payment is processed immediately as a regular sale.
Webhooks
GET /api/v1/webhooks/events # List webhook event types
GET /api/v1/webhooks/deliveries # List delivery attempts
POST /api/v1/webhooks/deliveries/{id}/retry # Retry a failed delivery
3.1 Docs-side closeout checklist
Treat this page as closure-grade from the repo side only if it is clear that:
- legacy
merchantIdfields are wire compatibility - the preferred tenant model is still organization/location
- support can describe hosted checkout and hosted payment flows without implying that the broader rename was partial or reversed
4. Webhook Delivery System
Event Types
| Event | Trigger |
|---|---|
payment.completed | Sale/auth approved |
payment.declined | Sale/auth declined |
payment.voided | Transaction voided |
payment.refunded | Refund processed |
payment.settled | Transaction settled in batch |
checkout.expired | Checkout session expired |
subscription.created | New subscription activated |
subscription.billed | Recurring charge succeeded |
subscription.payment_failed | Recurring charge declined/errored |
subscription.past_due | Multiple billing failures — action needed |
subscription.cancelled | Subscription cancelled |
subscription.paused | Subscription paused |
subscription.resumed | Subscription resumed |
gift_card.issued | Gift card issued under an organization |
gift_card.redeemed | Gift card redemption succeeded |
gift_card.reloaded | Gift card reload succeeded |
gift_card.refunded | Refund posted back to a gift card |
gift_card.expired | Gift card expired and remaining balance was written down |
gift_card.revoked | Gift card revoked by an admin flow |
gift_card.balance_low | Gift card balance crossed the configured low-balance threshold |
gift_card.adjusted | Platform-admin balance adjustment applied |
Delivery Mechanism
- Receive event from Pub/Sub (
transaction-eventstopic) - Look up merchant webhook configuration
- Build webhook payload with event data
- Sign payload with HMAC-SHA256 using merchant's webhook secret
- POST to merchant's webhook URL
- Retry on failure: 3 attempts with exponential backoff (1min, 5min, 30min)
- Log all delivery attempts
Signature Verification (for merchants)
X-Pinpoint-Signature: sha256=<HMAC-SHA256(webhook_secret, raw_body)>
X-Pinpoint-Timestamp: 1709478000
// Merchant verification:
expected = HMAC-SHA256(webhook_secret, timestamp + "." + raw_body)
assert(expected == signature_from_header)
assert(abs(now - timestamp) < 300) // 5 minute tolerance
5. Checkout Session State Machine
┌─────────┐ ┌───────────┐ ┌───────────┐
│ PENDING │────>│ COMPLETED │ │ CANCELLED │
└────┬────┘ └───────────┘ └───────────┘
│ ^
│ ┌───────────┐ │
├─────────>│ EXPIRED │ │
│ └───────────┘ │
│ │
└──────────────────────────────────┘
6. Fraud Screening (Basic)
| Check | Description |
|---|---|
| Velocity limit | Max N transactions per card per hour |
| Amount limit | Max single transaction amount per merchant |
| IP reputation | Block known malicious IPs (GCP reCAPTCHA Enterprise, future) |
| Card BIN check | Validate BIN country matches billing country |
| Duplicate detection | Same card + amount within 5 minutes = warning |
7. Technical Stack
| Component | Technology |
|---|---|
| Language | Kotlin (JVM 25) |
| Framework | Spring Boot 4.0.2 |
| Database | Cloud Spanner (PostgreSQL dialect) — online_sessions schema |
| Messaging | Cloud Pub/Sub — webhook delivery |
| Build | Bazel |
| Testing | JUnit 5, Testcontainers, WireMock |
8. Database Schema
CREATE TABLE checkout_sessions (
id VARCHAR(64) PRIMARY KEY,
merchant_id VARCHAR(64) NOT NULL,
amount BIGINT NOT NULL,
currency VARCHAR(3) DEFAULT 'USD',
description VARCHAR(500),
customer_email VARCHAR(255),
success_url VARCHAR(512),
cancel_url VARCHAR(512),
webhook_url VARCHAR(512),
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
transaction_id VARCHAR(64),
metadata JSONB,
expires_at TIMESTAMPTZ NOT NULL,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_sessions_merchant ON checkout_sessions(merchant_id, created_at DESC);
CREATE INDEX idx_sessions_status ON checkout_sessions(status, expires_at);
CREATE TABLE stored_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
merchant_id VARCHAR(64) NOT NULL,
customer_id VARCHAR(128),
token_id VARCHAR(64) NOT NULL, -- TransIT token
card_brand VARCHAR(20),
card_last4 VARCHAR(4),
expiration_date VARCHAR(4),
card_holder_name VARCHAR(255),
status VARCHAR(20) DEFAULT 'ACTIVE',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_tokens_merchant_customer ON stored_tokens(merchant_id, customer_id);
CREATE TABLE api_keys (
id VARCHAR(64) PRIMARY KEY,
merchant_id VARCHAR(64) NOT NULL,
key_hash VARCHAR(128) NOT NULL, -- bcrypt hash of API key
key_prefix VARCHAR(12) NOT NULL, -- first 12 chars for identification
label VARCHAR(100),
status VARCHAR(20) DEFAULT 'ACTIVE',
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE subscription_webhook_config (
merchant_id VARCHAR(64) PRIMARY KEY,
webhook_url VARCHAR(512),
webhook_secret VARCHAR(128),
notify_on VARCHAR(255)[] DEFAULT ARRAY[
'subscription.billed',
'subscription.payment_failed',
'subscription.cancelled'
],
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE webhook_deliveries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
merchant_id VARCHAR(64) NOT NULL,
event_type VARCHAR(50) NOT NULL,
payload JSONB NOT NULL,
webhook_url VARCHAR(512) NOT NULL,
status VARCHAR(20) NOT NULL, -- PENDING, DELIVERED, FAILED
http_status INTEGER,
attempts INTEGER DEFAULT 0,
next_retry_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_webhook_status ON webhook_deliveries(status, next_retry_at);
9. Health Check
GET /actuator/health