Skip to main content

Python SDK

The official Python SDK — itpay-sdk — provides a fully typed client for the ItPay Protocol. It wraps the REST API with idiomatic Python, including Pydantic-validated models, automatic request signing, and built-in webhook verification.


Installation

pip install itpay-sdk

Requirements: Python 3.9+.


Quick Setup

Configure the client with your API key and optional defaults:

from itpay_sdk import ItPayClient

client = ItPayClient(
api_key='sk_liv...here',
channel='alipay',
)

The API key can also be loaded from the ITPAY_API_KEY environment variable:

import os

client = ItPayClient(
api_key=os.environ['ITPAY_API_KEY'],
channel='alipay',
)

Client Configuration

ParameterTypeDefaultDescription
api_keystr(required)Your secret API key
channelstrDefault payment channel (e.g. alipay)
base_urlstrhttps://api.itpay.ai/v1API base URL
timeoutint30Request timeout in seconds
webhook_secretstrSecret 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 uuid
from itpay_sdk import ItPayClient

client = ItPayClient(
api_key='sk_liv...here',
channel='alipay',
)

def create_payment():
# 1. Create the PaymentIntent
intent = client.payment_intents.create(
id=f'pi_{uuid.uuid4().hex}',
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',
},
)

print(f"Intent created: {intent['id']} (status: {intent['status']})")
# → Intent created: pi_01J7XZ1A2B3C4D5E6F7G8H9IK (status: pending)

# 2. Generate the QR code
qr_charge = client.qr.generate(intent['id'])

print(f"QR scan URL: {qr_charge['scan_url']}")
print(f"QR expires at: {intent['expires_at']}")

# 3. Display the QR to the user
# In a web UI: <img src={qr_charge['scan_url']} alt="Pay with Alipay" />
# In a terminal: open the URL via webbrowser.open()
# In a chat: send the image inline

# 4. Poll for completion (useful for development)
result = client.payment_intents.wait_for_completion(
intent['id'],
timeout=300, # 5 minutes
poll_interval=2, # poll every 2 seconds
)

if result['status'] == 'succeeded':
print('✓ Payment received!')
else:
print(f'✗ Payment failed: {result["status"]}')

create_payment()

API Reference

client.payment_intents.create(params)

ParameterTypeRequiredDescription
idstrYesUnique UUIDv7 identifier (idempotency key)
service_idstrYesServiceManifest ID from registration
typestrYesone_time, cumulative, or subscription
amountdictYes{ "currency": str, "value": int }
descriptionstrNoHuman-readable payment label
payerdictYes{ "agent_id": str, "human_id"?: str }
metadatadictNoArbitrary key-value data (max 4 KB)

client.qr.generate(payment_intent_id)

Generates a QR charge for the given PaymentIntent. The PaymentIntent must be in pending status. Returns a dict with a scan_url for rendering.

client.payment_intents.wait_for_completion(id, timeout, poll_interval)

Polls the PaymentIntent until it reaches a terminal state (succeeded, expired, cancelled) or the timeout is hit.


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 Flask example:

import os
import hmac
import hashlib
import json
import time

from flask import Flask, request, jsonify

app = Flask(__name__)

# Must match the webhook secret from your ItPay dashboard
WEBHOOK_SECRET = os.environ.get('ITPAY_WEBHOOK_SECRET', 'whsec_xxxxxxxxxxxx')
WEBHOOK_MAX_AGE = 300 # 5 minutes

# Track processed events (use Redis in production)
PROCESSED_EVENTS: set[str] = set()


def verify_webhook_signature(
raw_body: bytes,
signature_header: str,
webhook_secret: str,
max_age: int = 300,
) -> bool:
"""Verify the HMAC-SHA256 signature on a webhook payload."""
# Parse the signature header: t=<timestamp>,v1=<hex_sig>
parts = dict(
item.split('=', 1)
for item in signature_header.split(',')
)

timestamp = parts.get('t')
expected_sig = parts.get('v1')

if not timestamp or not expected_sig:
return False

# Reject webhooks older than max_age
if int(time.time()) - int(timestamp) > max_age:
return False

# Reconstruct the signed payload: "<timestamp>.<raw_body>"
signed_payload = f"{timestamp}.{raw_body.decode('utf-8')}"

computed_sig = hmac.new(
key=webhook_secret.encode('utf-8'),
msg=signed_payload.encode('utf-8'),
digestmod=hashlib.sha256,
).hexdigest()

# Constant-time comparison — prevents timing side-channel attacks
return hmac.compare_digest(computed_sig, expected_sig)


@app.route('/itpay/v1/webhook', methods=['POST'])
def webhook():
# 1. Read signature header
signature_header = request.headers.get('X-ItPay-Signature')
if not signature_header:
return jsonify({'error': 'Missing X-ItPay-Signature header'}), 401

# 2. Verify the HMAC signature
if not verify_webhook_signature(request.data, signature_header, WEBHOOK_SECRET, WEBHOOK_MAX_AGE):
return jsonify({'error': 'Invalid signature'}), 401

# 3. Parse the event
event = request.get_json(force=True)
event_id = event['id']
event_type = event['type']

# 4. Idempotency check — skip events already processed
if event_id in PROCESSED_EVENTS:
return jsonify({'status': 'already_processed'}), 200
PROCESSED_EVENTS.add(event_id)

# 5. Handle the event
if event_type == 'payment_intent.succeeded':
intent = event['data']
print(f"✓ Payment succeeded — Intent: {intent['id']}")
print(f" Amount: {intent['amount']['value']} {intent['amount']['currency']}")
print(f" Channel: {intent['channel']}")
print(f" Metadata: {intent.get('metadata', {})}")

# TODO: Deliver the service to the user
# - Look up session_id from intent['metadata']
# - Unlock content or grant access

elif event_type == 'payment_intent.failed':
intent = event['data']
print(f"✗ Payment failed — {intent['id']}: {intent.get('failure_reason', 'unknown')}")

elif event_type == 'payment_intent.expired':
intent = event['data']
print(f"⏰ Payment expired — {intent['id']}")

else:
print(f"ℹ Unhandled event type: {event_type}")

# 6. Acknowledge receipt
return jsonify({'status': 'received'}), 200


if __name__ == '__main__':
app.run(port=8080)

Testing the Webhook Locally

# Start your webhook handler
python webhook_handler.py

# 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

Using the SDK's Built-in Verifier

The SDK includes a convenience function for webhook verification:

from itpay_sdk.webhooks import verify_signature

is_valid = verify_signature(
raw_body=request.data,
signature_header=request.headers['X-ItPay-Signature'],
webhook_secret=WEBHOOK_SECRET,
max_age=300,
)

Using the SDK with Django

For Django applications, use the @csrf_exempt decorator on the webhook view:

import json
import hmac
import hashlib
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse, HttpRequest

@csrf_exempt
def itpay_webhook(request: HttpRequest):
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)

# Verify signature
signature_header = request.headers.get('X-ItPay-Signature')
if not signature_header:
return JsonResponse({'error': 'Missing signature'}, status=401)

raw_body = request.body
# ... verification and handling same as Flask example above

return JsonResponse({'status': 'received'})

Error Handling

The SDK raises exceptions for predictable error handling:

from itpay_sdk.exceptions import (
ItPayError,
AuthenticationError,
ValidationError,
RateLimitError,
)

try:
intent = client.payment_intents.create(...)
except AuthenticationError:
print('Invalid API key — check your credentials')
except ValidationError as e:
print(f'Validation failed: {e}')
except RateLimitError:
print('Rate limit exceeded — slow down requests')
except ItPayError as e:
print(f'ItPay API error: {e.status_code}{e}')

Exception Classes

ExceptionHTTP StatusDescription
AuthenticationError401Invalid or missing API key
ValidationError422Request validation failed
RateLimitError429Rate limit exceeded
ItPayError5xxBase class for server-side errors

Best Practices

  • Use UUIDv4 or UUIDv7 for PaymentIntent id values — they ensure idempotency.
  • Store secrets as environment variables — never hardcode api_key or webhook_secret.
  • Always verify webhook signatures using hmac.compare_digest for constant-time comparison.
  • Use idempotency — the id field on PaymentIntent creation is the idempotency key. Reusing the same id is safe.
  • Prefer webhooks over polling in production. The convenience poller is for development only.
  • Use request.data (raw body) for webhook verification, not request.json — the raw bytes are needed for HMAC computation.

Next Steps