Skip to main content

Official Waffo Payment Platform Python SDK

Project description

Waffo Python SDK

English | 中文

PyPI version Python Version License: MIT

Official Python SDK for Waffo Payment Platform, providing typed access to payments, refunds, subscriptions, subscription changes, merchant configuration, payment method configuration, and webhook signature verification.

Latest Release Highlights

  • Subscription update API: Added subscription.update() with amount, trialPeriodAmount, and scheduledAmounts support.
  • Scheduled amount models: Added generated scheduled amount fields across subscription create, change, update, and inquiry models.
  • Contract hardening: Added order capture timestamp auto-injection and expanded schema field coverage.

Introduction

Core Features

  • Global Payments: Create, query, cancel, and refund payment orders.
  • Subscription Management: Create, query, cancel, manage, and change subscriptions.
  • Webhook Handling: Verify Waffo signatures and route notification callbacks.
  • Typed Models: Pydantic v2 request/response models generated from openapi.json.
  • Secure Defaults: RSA request signing, response signature verification, explicit environment selection, and configurable timeouts.

Table of Contents

Requirements

  • Python 3.9+
  • pip

Version Compatibility

Python Version Support Status
3.13 Fully Supported
3.12 Fully Supported
3.11 Fully Supported (Recommended)
3.10 Fully Supported
3.9 Fully Supported
< 3.9 Not Supported

Installation

python -m pip install "waffo==0.1.0b0"
# or install the latest beta/pre-release
python -m pip install --pre waffo

Quick Start

1. Initialize the SDK

from waffo import Environment, Waffo, WaffoConfig

config = WaffoConfig(
    api_key="your-api-key",
    private_key="your-base64-encoded-private-key",
    waffo_public_key="waffo-public-key",
    merchant_id="your-merchant-id",
    environment=Environment.SANDBOX,
)

waffo = Waffo(config)

2. Create a Payment Order

from uuid import uuid4

payment_request_id = uuid4().hex

response = waffo.order().create(
    {
        "paymentRequestId": payment_request_id,
        "merchantOrderId": f"ORDER_{payment_request_id}",
        "orderCurrency": "HKD",
        "orderAmount": "100.00",
        "orderDescription": "Test Product",
        "notifyUrl": "https://your-site.com/webhook",
        "userInfo": {
            "userId": "user_123",
            "userEmail": "user@example.com",
            "userTerminal": "WEB",
        },
        "paymentInfo": {
            "productName": "ONE_TIME_PAYMENT",
        },
        "goodsInfo": {
            "goodsUrl": "https://your-site.com/product/001",
            "goodsName": "Test Product",
        },
    }
)

if response.is_success():
    data = response.get_data()
    print("Redirect action:", data.order_action if data else None)
    print("Acquiring Order ID:", data.acquiring_order_id if data else None)
else:
    print("Error:", response.get_message())

3. Query Order Status

response = waffo.order().inquiry({"paymentRequestId": payment_request_id})

if response.is_success():
    data = response.get_data()
    print("Order Status:", data.order_status if data else None)
    print("Order Amount:", data.order_amount if data else None)

Configuration

Full Configuration Options

from waffo import Environment, Waffo, WaffoConfig

waffo = Waffo(
    WaffoConfig(
        api_key="your-api-key",
        private_key="your-base64-private-key",
        waffo_public_key="waffo-public-key",
        environment=Environment.SANDBOX,
        merchant_id="your-merchant-id",
        connect_timeout=10000,
        read_timeout=30000,
    )
)

Environment Variables

export WAFFO_API_KEY=your-api-key
export WAFFO_PRIVATE_KEY=your-private-key
export WAFFO_PUBLIC_KEY=waffo-public-key
export WAFFO_ENVIRONMENT=SANDBOX
export WAFFO_MERCHANT_ID=your-merchant-id
from waffo import Waffo

waffo = Waffo.from_env()

Environment URLs

Environment Base URL Description
SANDBOX https://api-sandbox.waffo.com Test environment
PRODUCTION https://api.waffo.com Production environment

Important: Environment must be explicitly specified. SDKs do not default to any environment to prevent accidental requests to wrong environments.

Request-Level Configuration

from waffo import RequestOptions

response = waffo.order().create(
    params,
    RequestOptions(connect_timeout=10000, read_timeout=30000),
)

API Usage

Order Management

Method Description
order().create() Create a payment order
order().inquiry() Query order status
order().cancel() Cancel an order
order().refund() Create an order refund
order().capture() Capture an authorized payment

Subscription Management

Method Description
subscription().create() Create a subscription
subscription().inquiry() Query a subscription
subscription().cancel() Cancel a subscription
subscription().manage() Get a subscription management URL
subscription().change() Upgrade or downgrade a subscription
subscription().change_inquiry() Query subscription change status

Subscription Change (Upgrade/Downgrade)

Change an existing subscription to a new plan (upgrade or downgrade).

Change Subscription

from uuid import uuid4

from waffo import WaffoUnknownStatusError

subscription_request = uuid4().hex
origin_subscription_request = "original-subscription-request-id"

try:
    response = waffo.subscription().change(
        {
            "subscriptionRequest": subscription_request,
            "originSubscriptionRequest": origin_subscription_request,
            "remainingAmount": "50.00",
            "currency": "HKD",
            "notifyUrl": "https://your-site.com/webhook/subscription",
            "productInfoList": [
                {
                    "description": "Annual Premium Subscription (12 months)",
                    "periodType": "MONTHLY",
                    "periodInterval": "12",
                    "amount": "999.00",
                }
            ],
            "userInfo": {
                "userId": "user_123",
                "userEmail": "user@example.com",
            },
            "goodsInfo": {
                "goodsId": "GOODS_PREMIUM",
                "goodsName": "Premium Plan",
            },
            "paymentInfo": {
                "productName": "SUBSCRIPTION",
            },
        }
    )

    if response.is_success():
        data = response.get_data()
        print("Change Status:", data.subscription_change_status if data else None)
        print("New Subscription ID:", data.subscription_id if data else None)
except WaffoUnknownStatusError as exc:
    print("Unknown status, query later or wait for webhook:", exc)

Subscription Change Status Values

Status Description
IN_PROGRESS Change is being processed
AUTHORIZATION_REQUIRED User needs to authorize the change (redirect to webUrl)
SUCCESS Change completed successfully
CLOSED Change was closed (timeout or failed)

Query Subscription Change Status

response = waffo.subscription().change_inquiry(
    {
        "subscriptionRequest": "new-subscription-request-id",
        "originSubscriptionRequest": "original-subscription-request-id",
    }
)

if response.is_success():
    data = response.get_data()
    print("Change Status:", data.subscription_change_status if data else None)
    print("New Subscription ID:", data.subscription_id if data else None)
    print("Remaining Amount:", data.remaining_amount if data else None)
    print("Currency:", data.currency if data else None)

Refund Query

response = waffo.refund().inquiry({"refundRequestId": "refund_request_id"})

Merchant Configuration

response = waffo.merchant_config().inquiry({})

Webhook Handling

from fastapi import FastAPI, Header, Request, Response
from waffo import Waffo

app = FastAPI()
waffo = Waffo.from_env()

waffo.webhook().on_payment(
    lambda notification: print(notification.get("result", {}).get("acquiringOrderId"))
)

@app.post("/webhook")
async def webhook(request: Request, x_signature: str | None = Header(default=None)) -> Response:
    body = (await request.body()).decode("utf-8")
    result = waffo.webhook().handle_webhook(body, x_signature)
    return Response(
        content=result.response_body,
        media_type="application/json",
        headers={"X-SIGNATURE": result.response_signature},
        status_code=200 if result.success else 400,
    )

Webhook Notification Types

Event Type Handler Method Description
PAYMENT_NOTIFICATION onPayment() Payment result notification (triggered on every payment attempt, including retries)
REFUND_NOTIFICATION onRefund() Refund result notification
SUBSCRIPTION_STATUS_NOTIFICATION onSubscriptionStatus() Subscription status change notification (triggered when subscription main record status changes)
SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION onSubscriptionPeriodChanged() Subscription period change notification (final result of each period)
SUBSCRIPTION_CHANGE_NOTIFICATION onSubscriptionChange() Subscription change (upgrade/downgrade) result notification

Subscription Notification Types Explained

Notification Type Trigger Condition Scope Includes Retry Events Typical Use Case
SUBSCRIPTION_STATUS_NOTIFICATION Subscription main record status changes Subscription level No Track subscription lifecycle: first payment success activation (ACTIVE), cancellation (MERCHANT_CANCELLED, CHANNEL_CANCELLED), first payment failure close (CLOSE), etc.
SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION Subscription period reaches final state Period level No (only final result) Only need final result of each period, no intermediate retry events
SUBSCRIPTION_CHANGE_NOTIFICATION Subscription change (upgrade/downgrade) completes Change request level No (only final result) Track subscription change results: SUCCESS or CLOSED
PAYMENT_NOTIFICATION Every payment order Payment order level Yes (includes all retries) Need complete details of every payment attempt, including failure reasons, timestamps, retry details

Selection Guide:

  • If you only care about subscription activation/cancellation, use SUBSCRIPTION_STATUS_NOTIFICATION
  • If you only care about final renewal result of each period, use SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION
  • If you only care about subscription change (upgrade/downgrade) final result, use SUBSCRIPTION_CHANGE_NOTIFICATION
  • If you need to track every payment attempt (including retries), use PAYMENT_NOTIFICATION

Subscription Payment Note: Each period's payment (including first payment and renewals) triggers PAYMENT_NOTIFICATION events. You can get subscription-related info (subscriptionId, period, etc.) from subscriptionInfo.

Subscription Change (Upgrade/Downgrade) Webhook Note: When a subscription change is processed, the following notifications are triggered:

  • SUBSCRIPTION_CHANGE_NOTIFICATION: When subscription change completes (SUCCESS or CLOSED)
  • SUBSCRIPTION_STATUS_NOTIFICATION: When original subscription status changes to MERCHANT_CANCELLED
  • SUBSCRIPTION_STATUS_NOTIFICATION: When new subscription status changes to ACTIVE
  • PAYMENT_NOTIFICATION: If upgrade requires additional payment (price difference)

Webhook Notification Payload Examples

The following examples show the actual payload structure for each notification type.

PAYMENT_NOTIFICATION

{
  "eventType": "PAYMENT_NOTIFICATION",
  "result": {
    "acquiringOrderId": "A2026xxxxxxxxxxxxxxxxxxxx",
    "orderStatus": "PAY_SUCCESS",
    "orderAmount": "109.00",
    "orderCurrency": "USD",
    "finalDealAmount": "109.00",
    "userCurrency": "USD",
    "orderDescription": "Sample Product",
    "orderRequestedAt": "2026-02-28T10:05:58.000Z",
    "orderCompletedAt": "2026-02-28T10:06:10.000Z",
    "orderUpdatedAt": "2026-02-28T10:06:10.000Z",
    "refundExpiryAt": "2026-08-26T23:59:59.999Z",
    "userInfo": {
      "userId": "user@example.com",
      "userEmail": "user@example.com"
    },
    "merchantInfo": {
      "merchantId": "YOUR_MERCHANT_ID"
    },
    "goodsInfo": {
      "goodsId": "goods_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "goodsName": "Sample Product"
    },
    "addressInfo": {},
    "paymentInfo": {
      "productName": "SUBSCRIPTION",
      "payMethodType": "CREDITCARD",
      "payMethodName": "CC_VISA",
      "payMethodProperties": "{\"cardToken\":\"CARD_TOKEN_XXXXXXXXXXXXXXXXXXXX\",\"cardTransactionType\":\"CIT\"}",
      "payMethodResponse": "{\"maskCardData\":\"XXXX42****XX4242\"}"
    },
    "subscriptionInfo": {
      "subscriptionId": "SC2026xxxxxxxxxxxxxxxxxxxxxxxxxx",
      "subscriptionRequest": "sub_req_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "merchantRequest": "sub_req_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "period": "1"
    },
    "cancelRedirectUrl": "https://YOUR_DOMAIN/checkout/YOUR_CHECKOUT_ID"
  }
}

SUBSCRIPTION_STATUS_NOTIFICATION / SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION

These two notification types share the same payload structure:

{
  "eventType": "SUBSCRIPTION_STATUS_NOTIFICATION",
  "result": {
    "subscriptionId": "SC2026xxxxxxxxxxxxxxxxxxxxxxxxxx",
    "subscriptionRequest": "sub_req_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "merchantSubscriptionId": "sub_req_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "subscriptionStatus": "ACTIVE",
    "currency": "USD",
    "userCurrency": "USD",
    "amount": "109.00",
    "requestedAt": "2026-02-28T10:05:58.000Z",
    "updatedAt": "2026-02-28T10:06:10.000Z",
    "userInfo": {
      "userId": "user@example.com",
      "userEmail": "user@example.com"
    },
    "merchantInfo": {
      "merchantId": "YOUR_MERCHANT_ID"
    },
    "goodsInfo": {
      "goodsId": "goods_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "goodsName": "Sample Product"
    },
    "productInfo": {
      "periodType": "MONTHLY",
      "periodInterval": "1",
      "currentPeriod": "1",
      "startDateTime": "2026-02-28T10:06:10.000Z",
      "nextPaymentDateTime": "2026-03-28T10:06:10.000Z",
      "description": "Sample Product"
    },
    "paymentInfo": {
      "productName": "SUBSCRIPTION",
      "payMethodType": "CREDITCARD",
      "payMethodName": "CC_VISA",
      "payMethodProperties": "{}"
    },
    "paymentDetails": [
      {
        "period": "1",
        "acquiringOrderId": "A2026xxxxxxxxxxxxxxxxxxxx",
        "orderAmount": "109.00",
        "orderCurrency": "USD",
        "orderStatus": "PAY_SUCCESS",
        "orderUpdatedAt": "2026-02-28T10:06:10.000Z"
      }
    ]
  }
}

Payment Method Types

payMethodType Reference

Type Description Example payMethodName
CREDITCARD Credit Card CC_VISA, CC_MASTERCARD, CC_AMEX, CC_JCB, etc.
DEBITCARD Debit Card DC_VISA, DC_MASTERCARD, DC_ELO, etc.
EWALLET E-Wallet GCASH, DANA, PROMPTPAY, GRABPAY, etc.
VA Virtual Account BCA, BNI, BRI, MANDIRI, etc.
APPLEPAY Apple Pay APPLEPAY
GOOGLEPAY Google Pay GOOGLEPAY

Usage Examples

// Specify type only, let user choose on checkout page
paymentInfo: {
  payMethodType: 'CREDITCARD',
}

// Specify exact payment method
paymentInfo: {
  payMethodType: 'CREDITCARD',
  payMethodName: 'CC_VISA',
}

// Combine multiple types
paymentInfo: {
  payMethodType: 'CREDITCARD,DEBITCARD',
}

// E-wallet with specific channel
paymentInfo: {
  payMethodType: 'EWALLET',
  payMethodName: 'GCASH',
}

Note: For available ProductName, PayMethodType, PayMethodName values, merchants can log in to Waffo Portal to view contracted payment methods (Home → Service → Pay-in).

Advanced Configuration

Timeout Configuration Recommendations

Operation Type Connect Timeout Read Timeout Notes
Create Order 5s 30s Recommended
Create Subscription 5s 30s Recommended
Refund Operation 5s 30s Recommended
Query Operations 5s 15s Can be shorter

Connection Pool Recommendations

Scenario Max Connections Max Per Route Notes
Low Traffic (< 10 QPS) 20 10 Default config sufficient
Medium Traffic (10-100 QPS) 50 20 Consider using OkHttp
High Traffic (> 100 QPS) 100-200 50 Consider Apache HttpClient

Instance Reuse

Create one Waffo instance during application startup and reuse it across requests.

Handling New API Fields (ExtraParams)

When Waffo API adds new fields that are not yet defined in the SDK, you can use the ExtraParams feature to access these fields without waiting for an SDK update.

Reading Unknown Fields from Responses

Sending Extra Fields in Requests

Important Notes

Upgrade SDK Promptly

ExtraParams is designed as a temporary solution for accessing new API fields before SDK updates.

Best Practices:

  1. Check SDK release notes regularly for new field support
  2. Once SDK officially supports the field, migrate from getExtraParam("field") to the official getter (e.g., getField())
  3. The SDK logs a warning when you use getExtraParam() on officially supported fields

Why migrate?

  • Official getters provide type safety
  • Better IDE auto-completion and documentation
  • Reduced risk of typos in field names

Error Handling

Error Code Classification

Error codes are classified by first letter:

Prefix Category Description
S SDK Internal Error SDK client internal error such as network timeout, signing failure, etc.
A Merchant Related Parameter, signature, permission, contract issues on merchant side
B User Related User status, balance, authorization issues
C System Related Waffo system or payment channel issues
D Risk Related Risk control rejection
E Unknown Status Server returned unknown status

Complete Error Code Table

SDK Internal Errors (Sxxxx)

Code Description Exception Type Handling Suggestion
S0001 Network Error WaffoUnknownStatusError Status unknown, need to query order to confirm
S0002 Invalid Public Key WaffoError Check if public key is valid Base64 encoded X509 format
S0003 RSA Signing Failed WaffoError Check if private key format is correct
S0004 Response Signature Verification Failed ApiResponse.error() Check Waffo public key config, contact Waffo
S0005 Request Serialization Failed ApiResponse.error() Check request parameter format
S0006 SDK Unknown Error ApiResponse.error() Check logs, contact technical support
S0007 Invalid Private Key WaffoError Check if private key is valid Base64 PKCS8 DER or unencrypted PKCS8 PEM format

Important: S0001 and E0001 (returned by server) indicate unknown status. Do not close order directly! Should call query API or wait for webhook to confirm actual status.

Merchant Related Errors (Axxxxx)

Code Description HTTP Status
0 Success 200
A0001 Invalid API Key 401
A0002 Invalid Signature 401
A0003 Parameter Validation Failed 400
A0004 Insufficient Permission 401
A0005 Merchant Limit Exceeded 400
A0006 Merchant Status Abnormal 400
A0007 Unsupported Transaction Currency 400
A0008 Transaction Amount Exceeded 400
A0009 Order Not Found 400
A0010 Merchant Contract Does Not Allow This Operation 400
A0011 Idempotent Parameter Mismatch 400
A0012 Merchant Account Insufficient Balance 400
A0013 Order Already Paid, Cannot Cancel 400
A0014 Refund Rules Do Not Allow Refund 400
A0015 Payment Channel Does Not Support Cancel 400
A0016 Payment Channel Rejected Cancel 400
A0017 Payment Channel Does Not Support Refund 400
A0018 Payment Method Does Not Match Merchant Contract 400
A0019 Cannot Refund Due to Chargeback Dispute 400
A0020 Payment Amount Exceeds Single Transaction Limit 400
A0021 Cumulative Payment Amount Exceeds Daily Limit 400
A0022 Multiple Products Exist, Need to Specify Product Name 400
A0023 Token Expired, Cannot Create Order 400
A0024 Exchange Rate Expired, Cannot Process Order 400
A0026 Unsupported Checkout Language 400
A0027 Refund Count Reached Limit (50 times) 400
A0029 Invalid Card Data Provided by Merchant 400
A0030 Card BIN Not Found 400
A0031 Unsupported Card Scheme or Card Type 400
A0032 Invalid Payment Token Data 400
A0033 Multiple Payment Methods with Same Name, Need to Specify Country 400
A0034 Order Expiry Time Provided by Merchant Has Passed 400
A0035 Current Order Does Not Support Capture Operation 400
A0036 Current Order Status Does Not Allow Capture Operation 400
A0037 User Payment Token Invalid or Expired 400
A0038 MIT Transaction Requires Verified User Payment Token 400
A0039 Order Already Refunded by Chargeback Prevention Service 400
A0040 Order Cannot Be Created Concurrently 400
A0045 MIT Transaction Cannot Process, tokenId Status Unverified 400

User Related Errors (Bxxxxx)

Code Description HTTP Status
B0001 User Status Abnormal 400
B0002 User Limit Exceeded 400
B0003 User Insufficient Balance 400
B0004 User Did Not Pay Within Timeout 400
B0005 User Authorization Failed 400
B0006 Invalid Phone Number 400
B0007 Invalid Email Format 400

System Related Errors (Cxxxxx)

Code Description HTTP Status
C0001 System Error 500
C0002 Merchant Contract Invalid 500
C0003 Order Status Invalid, Cannot Continue Processing 500
C0004 Order Information Mismatch 500
C0005 Payment Channel Rejected 503
C0006 Payment Channel Error 503
C0007 Payment Channel Under Maintenance 503

Risk Related Errors (Dxxxxx)

Code Description HTTP Status
D0001 Risk Control Rejected 406

Unknown Status Errors (Exxxxx)

Code Description HTTP Status
E0001 Unknown Status (Need to query or wait for callback) 500

Note: When receiving E0001 error code, it indicates transaction status is unknown. Do not close order directly, should call query API to confirm actual status, or wait for webhook callback notification.

Development

python -m pip install -e ".[dev]"
ruff check src tests examples
mypy src/waffo
pytest tests/unit -q

Support

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

waffo-0.2.0b1.tar.gz (80.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

waffo-0.2.0b1-py3-none-any.whl (39.2 kB view details)

Uploaded Python 3

File details

Details for the file waffo-0.2.0b1.tar.gz.

File metadata

  • Download URL: waffo-0.2.0b1.tar.gz
  • Upload date:
  • Size: 80.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for waffo-0.2.0b1.tar.gz
Algorithm Hash digest
SHA256 6e920b482ceec5e04c1b614fe8e273b926a7d13615233da6a61d6d4c32d8c363
MD5 b17e39969888dfd7c099149a9722a544
BLAKE2b-256 61bdaaf5ad637359b4ce9f87dedd1a46b8a6430d09c51390e7ab5c19a3b96800

See more details on using hashes here.

Provenance

The following attestation bundles were made for waffo-0.2.0b1.tar.gz:

Publisher: publish-python.yml on waffo-com/waffo-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file waffo-0.2.0b1-py3-none-any.whl.

File metadata

  • Download URL: waffo-0.2.0b1-py3-none-any.whl
  • Upload date:
  • Size: 39.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for waffo-0.2.0b1-py3-none-any.whl
Algorithm Hash digest
SHA256 739fff82b4c1d5ccd445713d9562bb64fe0898b26a97ccac9d2261a7da2b6a94
MD5 e60ba04bc9571832c4033d8c91e76fde
BLAKE2b-256 625ffc01d9ab3b3ac9fc96ab84c0f9a86ed792264ddbd8114d0244fbd6b67df6

See more details on using hashes here.

Provenance

The following attestation bundles were made for waffo-0.2.0b1-py3-none-any.whl:

Publisher: publish-python.yml on waffo-com/waffo-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page