PaymentIntent Integration Guide
This guide explains how to integrate with mpac-pgw using the PaymentIntent API, following Stripe-style patterns.
Overview
A PaymentIntent represents a payment transaction from creation to completion. It tracks the lifecycle of a payment and provides a payment_token for secure frontend operations.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Merchant Backend│────▶│ mpac-pgw │────▶│ Providers │
│ (svc-smarttab) │HMAC │ │ │ (PayPay, etc.) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ ▲
│ payment_token │ payment_token
▼ │
┌─────────────────┐ ┌───────┴─────────┐
│ Terminal App │────▶│ @cloudsky/mpac-pgw-sdk-js │
│ (MPACQR, etc.)│ │ │
└─────────────────┘ └─────────────────┘Authentication
Server-to-Server (HMAC-SHA256)
For backend-to-backend communication, use HMAC-SHA256 authentication:
Authorization: HMAC-SHA256 <merchant_id>:<timestamp>:<signature>Signature Calculation:
const crypto = require('crypto');
function generateHMACSignature(
secretKey: string,
method: string,
path: string,
timestamp: number,
body: string
): string {
const bodyHash = crypto.createHash('sha256').update(body || '').digest('hex');
const message = `${method}\n${path}\n${timestamp}\n${bodyHash}`;
return crypto.createHmac('sha256', secretKey).update(message).digest('hex');
}
// Example usage
const timestamp = Math.floor(Date.now() / 1000);
const signature = generateHMACSignature(
'your_secret_key',
'POST',
'/v1/payment_intents',
timestamp,
JSON.stringify(requestBody)
);
const authHeader = `HMAC-SHA256 ${merchantId}:${timestamp}:${signature}`;Frontend SDK (payment_token)
The frontend SDK uses the payment_token returned from PaymentIntent creation:
- GET requests: Query parameter
?payment_token=... - POST requests: In request body
{ "payment_token": "..." }
Creating a PaymentIntent (Server-to-Server)
Endpoint
POST /v1/payment_intents
Authorization: HMAC-SHA256 <merchant_id>:<timestamp>:<signature>
Idempotency-Key: <unique-uuid>
Content-Type: application/jsonRequest Body
{
"merchant_ref_id": "order_2025_001",
"merchant_store_id": "store_tokyo_001",
"merchant_terminal_id": "pos_001",
"amount": 2500,
"currency": "JPY",
"payment_method_types": ["qr_mpm", "credit_card", "cash"],
"capture_method": "automatic"
}| Field | Type | Required | Description |
|---|---|---|---|
merchant_ref_id | string | ✅ | Your unique order/transaction reference |
merchant_store_id | string | Optional store identifier for multi-store merchants | |
merchant_terminal_id | string | Optional terminal/POS identifier | |
amount | integer | ✅ | Amount in minor units (e.g., yen for JPY) |
currency | string | ISO 4217 currency code (default: JPY) | |
payment_method_types | string[] | ✅ | Allowed payment methods |
capture_method | string | automatic (default) or manual |
Payment Method Types:
qr_mpm- QR code displayed by merchant (PayPay, LINE Pay, etc.)qr_cpm- QR code scanned from customercredit_card- Credit card via MPACCreditCard app (external)cash- Cash payment (manual confirmation)
Response
{
"id": "pi_01HXY...",
"object": "payment_intent",
"status": "requires_payment_method",
"amount": 2500,
"currency": "JPY",
"payment_method_types": ["qr_mpm", "credit_card", "cash"],
"payment_token": "pi_01HXY..._secret_abc123...",
"capture_method": "automatic",
"merchant_store_id": "store_tokyo_001",
"merchant_terminal_id": "pos_001",
"expires_at": "2025-12-08T10:30:00Z",
"created_at": "2025-12-08T10:00:00Z"
}Important: Pass the
payment_tokento your frontend/terminal app. This token allows the app to confirm/cancel the payment without exposing your API credentials.
Code Examples
Node.js / TypeScript
import crypto from 'crypto';
interface CreatePaymentIntentRequest {
merchant_ref_id: string;
amount: number;
currency?: string;
payment_method_types: string[];
capture_method?: 'automatic' | 'manual';
}
async function createPaymentIntent(
request: CreatePaymentIntentRequest
): Promise<PaymentIntent> {
const baseUrl = 'https://api.pgw.mpac-cloud.com';
const path = '/v1/payment_intents';
const method = 'POST';
const body = JSON.stringify(request);
const timestamp = Math.floor(Date.now() / 1000);
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const message = `${method}\n${path}\n${timestamp}\n${bodyHash}`;
const signature = crypto
.createHmac('sha256', process.env.MPS_SECRET_KEY!)
.update(message)
.digest('hex');
const response = await fetch(`${baseUrl}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `HMAC-SHA256 ${process.env.MPS_MERCHANT_ID}:${timestamp}:${signature}`,
'Idempotency-Key': crypto.randomUUID(),
},
body,
});
if (!response.ok) {
const error = await response.json();
throw new Error(`mpac-pgw Error: ${error.code} - ${error.message}`);
}
return response.json();
}
// Usage
const paymentIntent = await createPaymentIntent({
merchant_ref_id: 'order_123',
amount: 1500,
currency: 'JPY',
payment_method_types: ['qr_mpm', 'credit_card', 'cash'],
});
// Pass payment_token to terminal app
sendToTerminal(paymentIntent.payment_token);Python
import hashlib
import hmac
import time
import uuid
import requests
import json
import os
def create_payment_intent(
merchant_ref_id: str,
amount: int,
payment_method_types: list[str],
currency: str = "JPY",
capture_method: str = "automatic"
) -> dict:
base_url = "https://api.pgw.mpac-cloud.com"
path = "/v1/payment_intents"
method = "POST"
body = json.dumps({
"merchant_ref_id": merchant_ref_id,
"amount": amount,
"currency": currency,
"payment_method_types": payment_method_types,
"capture_method": capture_method,
})
timestamp = int(time.time())
body_hash = hashlib.sha256(body.encode()).hexdigest()
message = f"{method}\n{path}\n{timestamp}\n{body_hash}"
signature = hmac.new(
os.environ["MPS_SECRET_KEY"].encode(),
message.encode(),
hashlib.sha256
).hexdigest()
response = requests.post(
f"{base_url}{path}",
headers={
"Content-Type": "application/json",
"Authorization": f"HMAC-SHA256 {os.environ['MPS_MERCHANT_ID']}:{timestamp}:{signature}",
"Idempotency-Key": str(uuid.uuid4()),
},
data=body,
)
response.raise_for_status()
return response.json()
# Usage
payment_intent = create_payment_intent(
merchant_ref_id="order_123",
amount=1500,
payment_method_types=["qr_mpm", "credit_card", "cash"],
)
# Pass payment_token to terminal app
print(f"Payment Token: {payment_intent['payment_token']}")Go
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"time"
"github.com/google/uuid"
)
type CreatePaymentIntentRequest struct {
MerchantRefID string `json:"merchant_ref_id"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
PaymentMethodTypes []string `json:"payment_method_types"`
CaptureMethod string `json:"capture_method,omitempty"`
}
type PaymentIntent struct {
ID string `json:"id"`
Object string `json:"object"`
Status string `json:"status"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
PaymentMethodTypes []string `json:"payment_method_types"`
PaymentToken string `json:"payment_token"`
CaptureMethod string `json:"capture_method"`
ExpiresAt string `json:"expires_at"`
CreatedAt string `json:"created_at"`
}
func createPaymentIntent(req CreatePaymentIntentRequest) (*PaymentIntent, error) {
baseURL := "https://api.pgw.mpac-cloud.com"
path := "/v1/payment_intents"
method := "POST"
body, _ := json.Marshal(req)
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
// Calculate signature
bodyHash := sha256.Sum256(body)
message := fmt.Sprintf("%s\n%s\n%s\n%s", method, path, timestamp, hex.EncodeToString(bodyHash[:]))
mac := hmac.New(sha256.New, []byte(os.Getenv("MPS_SECRET_KEY")))
mac.Write([]byte(message))
signature := hex.EncodeToString(mac.Sum(nil))
// Create request
httpReq, _ := http.NewRequest(method, baseURL+path, bytes.NewReader(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", fmt.Sprintf("HMAC-SHA256 %s:%s:%s",
os.Getenv("MPS_MERCHANT_ID"), timestamp, signature))
httpReq.Header.Set("Idempotency-Key", uuid.New().String())
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var pi PaymentIntent
json.NewDecoder(resp.Body).Decode(&pi)
return &pi, nil
}
func main() {
pi, _ := createPaymentIntent(CreatePaymentIntentRequest{
MerchantRefID: "order_123",
Amount: 1500,
Currency: "JPY",
PaymentMethodTypes: []string{"qr_mpm", "credit_card", "cash"},
})
fmt.Printf("Payment Token: %s\n", pi.PaymentToken)
}Payment Flow by Method
QR MPM (PayPay, LINE Pay, etc.)
1. Backend creates PaymentIntent → returns payment_token
2. Terminal app confirms with SDK → generates QR code
3. Customer scans QR and pays in their app
4. Provider webhook → PGW updates status
5. SDK polls and detects success → emits payment_successConfirm Request (from SDK):
POST /v1/payment_intents/{id}/confirm
{
"payment_token": "pi_..._secret_...",
"payment_method_type": "qr_mpm",
"provider": "PAYPAY"
}Response:
{
"object": "payment_intent",
"status": "requires_action",
"amount": 2500,
"currency": "JPY",
"payment_id": "pi_01HXY...",
"payment_intent_id": "pi_01HXY...",
"qr_content": "https://qr.paypay.ne.jp/...",
"expires_at": "2025-12-08T10:05:00Z",
"provider": "PAYPAY",
"qr_code_url": "https://..."
}Credit Card (External via MPACCreditCard)
1. Backend creates PaymentIntent → returns payment_token
2. Terminal app confirms via Native Bridge → launches MPACCreditCard app
3. MPACCreditCard processes payment externally
4. Native Bridge receives result → confirms with PGW
5. PGW records the payment (processing_mode: external)Confirm Request (from Native Bridge):
POST /v1/payment_intents/{id}/confirm
{
"payment_token": "pi_..._secret_...",
"payment_method_type": "credit_card",
"provider": "VISA",
"external_ref_id": "APPROVAL_CODE_123"
}Response:
{
"object": "payment_intent",
"status": "succeeded",
"amount": 2500,
"currency": "JPY",
"payment_id": "pi_01HXY...",
"payment_intent_id": "pi_01HXY...",
"provider": "VISA"
}Cash (Manual Confirmation)
1. Backend creates PaymentIntent → returns payment_token
2. Customer pays cash to staff
3. Staff confirms cash received via app
4. App confirms with PGW (processing_mode: manual)Confirm Request:
POST /v1/payment_intents/{id}/confirm
{
"payment_token": "pi_..._secret_...",
"payment_method_type": "cash"
}Response:
{
"object": "payment_intent",
"status": "succeeded",
"amount": 2500,
"currency": "JPY",
"payment_id": "pi_01HXY...",
"payment_intent_id": "pi_01HXY...",
"provider": "CASH"
}PaymentIntent Status Flow
requires_payment_method ──▶ requires_action ──▶ processing ──▶ succeeded
│ │ │
└────────────────────────┴────────────────┴──▶ failed
│ │
└────────────────────────┴──────────────────▶ cancelled| Status | Description |
|---|---|
requires_payment_method | Initial state, waiting for payment method selection |
requires_action | QR code generated, waiting for customer action |
processing | Payment is being processed by provider |
succeeded | Payment completed successfully |
failed | Payment failed |
cancelled | Payment was cancelled |
Processing Modes
| Mode | Payment Methods | Description |
|---|---|---|
pgw_processed | QR (MPM/CPM) | mpac-pgw processes the payment directly |
external | Credit Card | Recorded by PGW, processed by external app |
manual | Cash | Recorded by PGW, confirmed manually by staff |
Idempotency
Always include an Idempotency-Key header for POST requests:
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000- Same key within 24 hours → returns cached response
merchant_ref_idis also unique per merchant → prevents duplicate orders
Error Handling
Error Response Format
{
"error": {
"code": "invalid_request",
"message": "Amount must be greater than 0",
"param": "amount"
}
}Common Error Codes
| Code | HTTP Status | Description |
|---|---|---|
invalid_request | 400 | Invalid request parameters |
authentication_error | 401 | Invalid HMAC signature or credentials |
payment_not_found | 404 | PaymentIntent not found |
invalid_payment_state | 409 | Invalid status transition |
provider_error | 502 | Error from payment provider |
internal_error | 500 | Internal server error |
Webhooks
Configure your webhook URL in the merchant settings. mpac-pgw sends POST requests for payment events:
{
"id": "evt_01HXY...",
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_01HXY...",
"status": "succeeded",
"amount": 2500,
"currency": "JPY"
}
},
"created": 1702000000
}Webhook Event Types
payment_intent.succeededpayment_intent.failedpayment_intent.cancelled
Best Practices
- Always use Idempotency-Key for create operations
- Store payment_token securely - it grants access to modify the payment
- Handle webhook events for reliable payment status updates
- Set appropriate timeouts - PaymentIntents expire after 30 minutes by default
- Use merchant_ref_id to link payments to your order system