TypeScript / Node.js SDK
The official TypeScript/Node.js SDK — @itpay/mcp — provides a fully typed client for the ItPay Protocol. It wraps the REST API with idiomatic TypeScript, supports both direct API usage and MCP transport integration, and includes built-in webhook signature verification.
Installation
npm install @itpay/mcp
Requirements: Node.js 18+ (native fetch and crypto module).
Quick Setup
Configure the client with your API key and optional defaults:
import { ItPayClient } from '@itpay/mcp';
const client = new ItPayClient({
apiKey: 'sk_liv_your_api_key_here',
channel: 'alipay',
});
The API key can also be loaded from the ITPAY_API_KEY environment variable, in which case the constructor argument is optional:
const client = new ItPayClient({ channel: 'alipay' });
// Reads ITPAY_API_KEY from process.env
Client Configuration
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | process.env.ITPAY_API_KEY | Your secret API key |
channel | string | — | Default payment channel (e.g. alipay) |
baseUrl | string | https://api.itpay.ai/v1 | API base URL |
timeout | number | 30000 | Request timeout in milliseconds |
webhookSecret | string | process.env.ITPAY_WEBHOOK_SECRET | Secret for verifying webhooks |
Creating a PaymentIntent
The payment flow starts by creating a PaymentIntent, then generating a QR code so the user can scan and pay.
import { ItPayClient } from '@itpay/mcp';
import { randomUUID } from 'node:crypto';
const client = new ItPayClient({
apiKey: 'sk_liv_your_api_key_here',
channel: 'alipay',
});
async function createPayment(): Promise<void> {
// 1. Create the PaymentIntent
const intent = await client.paymentIntents.create({
id: `pi_${randomUUID()}`,
service_id: '01J7XYKZ1A2B3C4D5E6F7G8H9I',
type: 'one_time',
amount: {
currency: 'CNY',
value: 699, // 6.99 CNY in minor units
},
description: 'AI document summary (42 pages)',
payer: {
agent_id: 'agent_cli_a1b2c3d4',
human_id: 'user_abc_789',
},
metadata: {
session_id: 'sess_xyz_456',
},
});
console.log(`Intent created: ${intent.id} (status: ${intent.status})`);
// → Intent created: pi_01J7XZ1A2B3C4D5E6F7G8H9IK (status: pending)
// 2. Generate the QR code
const qrCharge = await client.qr.generate(intent.id);
console.log(`QR scan URL: ${qrCharge.scan_url}`);
console.log(`QR expires at: ${intent.expires_at}`);
// 3. Display the QR to the user
// In a web UI: <img src={qrCharge.scan_url} alt="Pay with Alipay" />
// In a terminal: open the URL via your system browser
// In a chat: send the image inline
// 4. Poll for completion (useful for development)
const result = await client.paymentIntents.waitForCompletion(intent.id, {
timeout: 300_000, // 5 minutes
pollInterval: 2_000, // poll every 2 seconds
});
if (result.status === 'succeeded') {
console.log('✓ Payment received!');
} else {
console.log(`✗ Payment failed: ${result.status}`);
}
}
createPayment().catch(console.error);
API Reference
client.paymentIntents.create(params)
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique UUIDv7 identifier (idempotency key) |
service_id | string | Yes | ServiceManifest ID from registration |
type | string | Yes | one_time, cumulative, or subscription |
amount | Money | Yes | { currency: string, value: number } |
description | string | No | Human-readable payment label |
payer | Party | Yes | { agent_id: string, human_id?: string } |
metadata | object | No | Arbitrary key-value data (max 4 KB) |
client.qr.generate(paymentIntentId)
Generates a QR charge for the given PaymentIntent. The PaymentIntent must be in pending status. Returns a QRCharge object with a scan_url for rendering.
client.paymentIntents.waitForCompletion(id, options)
Polls the PaymentIntent until it reaches a terminal state (succeeded, expired, cancelled) or the timeout is hit. Accepts { timeout?: number, pollInterval?: number }.
Handling Webhooks
When a payment completes, the ItPay platform sends a signed webhook POST to your ServiceManifest's configured endpoint URL. Your handler must verify the HMAC-SHA256 signature and check for idempotency.
Here's a complete Express.js example:
import express, { Request, Response } from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const app = express();
const PORT = 8080;
// ⚠️ Must match the webhook secret from your ItPay dashboard
const WEBHOOK_SECRET = process.env.ITPAY_WEBHOOK_SECRET || 'whsec_xxxxxxxxxxxx';
const WEBHOOK_MAX_AGE_MS = 300_000; // 5 minutes
// Track processed events (use Redis in production)
const processedEvents = new Set<string>();
// --- Signature verification ---
function verifyWebhookSignature(
rawBody: string,
signatureHeader: string,
webhookSecret: string,
maxAgeMs: number = 300_000,
): boolean {
const headerParts = Object.fromEntries(
signatureHeader.split(',').map((p) => p.split('=', 2)),
);
const timestamp = headerParts['t'];
const expectedSig = headerParts['v1'];
if (!timestamp || !expectedSig) return false;
// Reject expired webhooks
if (Date.now() - Number(timestamp) * 1000 > maxAgeMs) {
return false;
}
const signedPayload = `${timestamp}.${rawBody}`;
const computedSig = createHmac('sha256', webhookSecret)
.update(signedPayload)
.digest('hex');
// Constant-time comparison — prevents timing side-channel attacks
return timingSafeEqual(
Buffer.from(computedSig),
Buffer.from(expectedSig),
);
}
// --- Webhook endpoint ---
// ⚠️ IMPORTANT: Use express.raw() BEFORE the JSON parser for webhooks.
// The raw body is needed for HMAC verification.
app.use('/itpay/v1/webhook', express.raw({ type: 'application/json' }));
app.post('/itpay/v1/webhook', (req: Request, res: Response) => {
// 1. Read signature headers
const signatureHeader = req.headers['x-itpay-signature'] as string;
if (!signatureHeader) {
res.status(401).json({ error: 'Missing X-ItPay-Signature header' });
return;
}
// 2. Verify the HMAC signature
const rawBody = (req.body as Buffer).toString('utf-8');
if (!verifyWebhookSignature(rawBody, signatureHeader, WEBHOOK_SECRET, WEBHOOK_MAX_AGE_MS)) {
res.status(401).json({ error: 'Invalid signature' });
return;
}
// 3. Parse the event
const event = JSON.parse(rawBody);
const eventId: string = event.id;
const eventType: string = event.type;
// 4. Idempotency check — skip events already processed
if (processedEvents.has(eventId)) {
res.status(200).json({ status: 'already_processed' });
return;
}
processedEvents.add(eventId);
// 5. Handle the event
switch (eventType) {
case 'payment_intent.succeeded': {
const intent = event.data;
console.log(`✓ Payment succeeded — Intent: ${intent.id}`);
console.log(` Amount: ${intent.amount.value} ${intent.amount.currency}`);
console.log(` Channel: ${intent.channel}`);
console.log(` Metadata: ${JSON.stringify(intent.metadata)}`);
// TODO: Deliver the service to the user
// - Look up session_id from intent.metadata
// - Unlock content or grant access
break;
}
case 'payment_intent.failed': {
const intent = event.data;
console.log(`✗ Payment failed — ${intent.id}: ${intent.failure_reason || 'unknown'}`);
break;
}
case 'payment_intent.expired': {
const intent = event.data;
console.log(`⏰ Payment expired — ${intent.id}`);
break;
}
default:
console.log(`ℹ Unhandled event type: ${eventType}`);
}
// 6. Acknowledge receipt
res.status(200).json({ status: 'received' });
});
app.listen(PORT, () => {
console.log(`Webhook handler listening on port ${PORT}`);
});
Testing the Webhook Locally
# Start your webhook handler
npx tsx webhook-handler.ts
# Expose it to the internet with ngrok
ngrok http 8080
# → Forwarding https://abc123.ngrok.io → http://localhost:8080
# Update your ServiceManifest's endpoint to the ngrok URL
# https://abc123.ngrok.io/itpay/v1/webhook
MCP Server Integration
The @itpay/mcp package can also be used as a Model Context Protocol (MCP) server, exposing ItPay payment capabilities as MCP tools. This allows any MCP-compatible AI agent to discover and use payment tools automatically.
As an MCP Server (stdio transport)
import { ItPayMCPServer } from '@itpay/mcp/server';
const server = new ItPayMCPServer({
apiKey: 'sk_liv_your_api_key_here',
channel: 'alipay',
});
await server.start();
// Listens on stdin/stdout as MCP stdio transport
// Tools are auto-discovered by MCP clients:
// - create_payment_intent
// - generate_qr
// - check_payment_status
Configure in an MCP Client
Add @itpay/mcp to your MCP client configuration:
- Claude Desktop
- Hermes Agent
{
"mcpServers": {
"itpay": {
"command": "npx",
"args": ["-y", "@itpay/mcp"],
"env": {
"ITPAY_API_KEY": "sk_liv_your_api_key_here",
"ITPAY_CHANNEL": "alipay"
}
}
}
}
# config.yaml
mcp_servers:
itpay:
transport: stdio
command: npx -y @itpay/mcp
env:
ITPAY_API_KEY: sk_liv_your_api_key_here
ITPAY_CHANNEL: alipay
Once configured, your AI agent can invoke payment tools using natural language:
The user wants to pay ¥6.99 for the document summary.
→ Agent calls create_payment_intent and generate_qr
→ QR code is displayed to the user
→ Agent waits for webhook confirmation
→ Service is delivered
Available MCP Tools
| Tool Name | Description |
|---|---|
create_payment_intent | Create a new PaymentIntent for receiving funds |
generate_qr | Generate a QR code for an existing intent |
check_payment_status | Poll the status of a PaymentIntent |
list_manifests | List registered ServiceManifests |
Error Handling
The SDK throws typed errors for predictable error handling:
import { ItPayError, ItPayAuthenticationError, ItPayValidationError } from '@itpay/mcp';
try {
const intent = await client.paymentIntents.create({ /* ... */ });
} catch (err) {
if (err instanceof ItPayAuthenticationError) {
console.error('Invalid API key — check your credentials');
} else if (err instanceof ItPayValidationError) {
console.error(`Validation failed: ${err.message}`);
} else if (err instanceof ItPayError) {
console.error(`ItPay API error: ${err.statusCode} — ${err.message}`);
} else {
console.error(`Network error: ${err}`);
}
}
Error Classes
| Class | HTTP Status | Description |
|---|---|---|
ItPayAuthenticationError | 401 | Invalid or missing API key |
ItPayValidationError | 422 | Request validation failed |
ItPayRateLimitError | 429 | Rate limit exceeded |
ItPayAPIError | 5xx | Server-side error (retryable) |
ItPayError | — | Base class for all ItPay errors |
Best Practices
- Use UUIDv7 for PaymentIntent
idvalues — they are time-sortable and unique. - Store the webhook secret as an environment variable, never in source code.
- Always verify webhook signatures using
timingSafeEqualfor constant-time comparison. - Use idempotency — the
idfield on PaymentIntent creation is the idempotency key. Reusing the sameidis safe. - Set reasonable timeouts — QR payments typically complete within 2–5 minutes.
- Prefer webhooks over polling in production. The built-in
waitForCompletionhelper is convenient for development but webhooks are more reliable at scale.
Next Steps
- MCP Integration — Deeper guide on MCP-based payment flows
- Core Objects: PaymentIntent — Complete state machine reference
- Security Model — Webhook verification and request signing details
- Channel Matrix — Supported payment channels and regions