Documentation Index Fetch the complete documentation index at: https://mintlify.com/mutuiris/voicepact/llms.txt
Use this file to discover all available pages before exploring further.
Overview
VoicePact integrates with mobile money platforms to enable secure escrow payments for contracts. Funds are held in escrow until both parties confirm the transaction is complete, protecting buyers and sellers.
Escrow Protection Funds held securely until delivery is confirmed by both parties
M-Pesa Integration Direct integration with Kenya’s leading mobile money platform
Automated Release Payment released automatically when delivery is confirmed
Transaction Tracking Real-time status updates for all payment transactions
How It Works
Contract Created
After voice negotiation, a contract is created with payment amount specified.
Payment Request
Buyer receives mobile money payment request (STK Push) on their phone.
Funds Locked
Payment is held in escrow - not accessible to buyer or seller.
Delivery Confirmed
Both parties confirm delivery through SMS or USSD.
Payment Released
Funds automatically transferred to seller’s mobile money account.
Payment Flow
Mobile Checkout
Initiate a payment from the buyer:
import httpx
response = httpx.post(
"https://api.voicepact.com/api/v1/payments/checkout" ,
json = {
"contract_id" : "AG-2024-001234" ,
"phone_number" : "+254712345678" ,
"amount" : 350000 ,
"currency_code" : "KES" ,
"metadata" : {
"product" : "Maize" ,
"quantity" : "100 bags" ,
"seller_phone" : "+254723456789"
}
}
)
result = response.json()
print ( f "Transaction ID: { result[ 'transaction_id' ] } " )
print ( f "Status: { result[ 'status' ] } " )
print ( f "Description: { result[ 'description' ] } " )
Checkout Flow
API call initiates payment request
M-Pesa sends STK Push to buyer’s phone
Buyer enters M-Pesa PIN
Payment confirmed and locked in escrow
Webhook notification sent to VoicePact
Contract status updated to “payment_locked”
See africastalking_client.py:240 for checkout implementation.
Checkout Response
{
"transaction_id" : "TXN-AG-2024-001234-001" ,
"external_transaction_id" : "AT-TXN-abc123xyz" ,
"status" : "pending" ,
"description" : "Waiting for user to confirm payment" ,
"amount" : 350000 ,
"currency" : "KES" ,
"phone_number" : "+254712345678" ,
"created_at" : "2024-03-06T10:30:00Z"
}
Payment Status
Query the status of a payment transaction:
transaction_id = "TXN-AG-2024-001234-001"
response = httpx.get(
f "https://api.voicepact.com/api/v1/payments/status/ { transaction_id } "
)
status = response.json()
print ( f "Status: { status[ 'status' ] } " )
print ( f "Amount: { status[ 'amount' ] } { status[ 'currency' ] } " )
print ( f "Payer: { status[ 'payer_phone' ] } " )
print ( f "Recipient: { status[ 'recipient_phone' ] } " )
if status[ 'status' ] == 'released' :
print ( f "Released at: { status[ 'released_at' ] } " )
Status Values
pending : Payment requested, awaiting user confirmation
locked : Payment received and held in escrow
released : Payment transferred to seller
refunded : Payment returned to buyer
failed : Payment failed or was cancelled
See africastalking_client.py:306 for status query.
Webhook Notifications
VoicePact receives real-time payment updates via webhooks:
# Webhook payload at /api/v1/payments/webhook
{
"transactionId" : "AT-TXN-abc123xyz" ,
"phoneNumber" : "+254712345678" ,
"amount" : "KES 350000.00" ,
"status" : "Success" ,
"description" : "Payment received from +254712345678" ,
"requestMetadata" : {
"contract_id" : "AG-2024-001234" ,
"product" : "Maize" ,
"seller_phone" : "+254723456789"
},
"sourceType" : "Mpesa" ,
"provider" : "Mpesa" ,
"providerChannel" : "525900" ,
"value" : "KES 350000.00"
}
Webhook Processing
The webhook handler:
Validates webhook signature for security
Extracts transaction details
Updates payment record in database
Updates contract status
Notifies relevant parties via SMS
Triggers next workflow step
See africastalking_client.py:494 for webhook processing.
Payment Release
Release escrowed funds to the seller:
response = httpx.post(
"https://api.voicepact.com/api/v1/payments/release" ,
json = {
"contract_id" : "AG-2024-001234" ,
"transaction_id" : "TXN-AG-2024-001234-001" ,
"confirmed_by" : [
"+254712345678" , # Buyer
"+254723456789" # Seller
]
}
)
result = response.json()
print ( f "Release status: { result[ 'status' ] } " )
print ( f "Amount released: { result[ 'amount' ] } { result[ 'currency' ] } " )
print ( f "Recipient: { result[ 'recipient_phone' ] } " )
Release Conditions
Payment is released when:
Seller confirms delivery (via SMS/USSD)
Buyer accepts delivery (via SMS/USSD)
No disputes have been raised
Contract is marked as “completed”
Payment Refund
Refund payment to buyer if contract is cancelled or disputed:
response = httpx.post(
"https://api.voicepact.com/api/v1/payments/refund" ,
json = {
"transaction_id" : "TXN-AG-2024-001234-001" ,
"reason" : "Contract cancelled by mutual agreement" ,
"initiated_by" : "+254712345678"
}
)
result = response.json()
print ( f "Refund status: { result[ 'status' ] } " )
print ( f "Amount refunded: { result[ 'amount' ] } { result[ 'currency' ] } " )
Transaction History
Retrieve payment history for a contract:
contract_id = "AG-2024-001234"
response = httpx.get(
f "https://api.voicepact.com/api/v1/payments/history/ { contract_id } "
)
payments = response.json()
for payment in payments[ 'transactions' ]:
print ( f "ID: { payment[ 'transaction_id' ] } " )
print ( f "Amount: { payment[ 'amount' ] } { payment[ 'currency' ] } " )
print ( f "Status: { payment[ 'status' ] } " )
print ( f "Date: { payment[ 'created_at' ] } " )
print ( "---" )
Wallet Balance
Check VoicePact’s mobile money wallet balance:
response = httpx.get(
"https://api.voicepact.com/api/v1/payments/balance"
)
balance = response.json()
print ( f "Balance: { balance[ 'balance' ] } " )
print ( f "Currency: { balance[ 'currency' ] } " )
print ( f "Last updated: { balance[ 'updated_at' ] } " )
See africastalking_client.py:322 for balance query.
Payment Reference Generation
Generate unique payment reference codes:
from app.services.crypto_service import get_crypto_service
crypto = get_crypto_service()
reference = crypto.generate_payment_reference(
contract_id = "AG-2024-001234" ,
amount = 350000.00 ,
phone_number = "+254712345678"
)
print ( f "Payment Reference: { reference } " ) # e.g., "A7F5C3D8E9B2"
See crypto_service.py:121 for reference generation.
SMS Notifications
Automatic SMS notifications for payment events:
Payment Received
VoicePact Payment Received:
Contract: AG-2024-001234
Amount: KES 350,000.00
Status: Processing
You will receive confirmation shortly.
Payment Released
VoicePact Payment Released:
Contract: AG-2024-001234
Amount: KES 350,000.00
To: +254723456789
Transaction ID: TXN-AG-2024-001234-001
Payment Template
from app.services.africastalking_client import AfricasTalkingClient
client = AfricasTalkingClient()
message = client.generate_payment_sms(
contract_id = "AG-2024-001234" ,
amount = 350000 ,
currency = "KES" ,
action = "received"
)
See africastalking_client.py:432 for SMS templates.
Database Model
Payment records are stored in the database:
class Payment ( Base ):
__tablename__ = "payments"
id : Mapped[ int ] = mapped_column(Integer, primary_key = True )
contract_id: Mapped[ str ] = mapped_column(String( 50 ), ForeignKey( "contracts.id" ))
transaction_id: Mapped[Optional[ str ]] = mapped_column(String( 100 ), unique = True )
external_transaction_id: Mapped[Optional[ str ]] = mapped_column(String( 100 ))
payer_phone: Mapped[ str ] = mapped_column(String( 20 ))
recipient_phone: Mapped[Optional[ str ]] = mapped_column(String( 20 ))
amount: Mapped[Decimal] = mapped_column(Numeric( precision = 15 , scale = 2 ))
currency: Mapped[ str ] = mapped_column(String( 3 ), default = "KES" )
payment_type: Mapped[ str ] = mapped_column(String( 20 ), default = "escrow" )
status: Mapped[PaymentStatus] = mapped_column(Enum(PaymentStatus), default = PaymentStatus. PENDING )
payment_method: Mapped[Optional[ str ]] = mapped_column(String( 50 ))
created_at: Mapped[datetime] = mapped_column(DateTime, default = datetime.utcnow)
confirmed_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
released_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
failure_reason: Mapped[Optional[ str ]] = mapped_column(String( 200 ))
retry_count: Mapped[ int ] = mapped_column(Integer, default = 0 )
payment_metadata: Mapped[Optional[ dict ]] = mapped_column( JSON , default = dict )
See contract.py:219 for model definition.
Circuit Breaker Pattern
Payment service uses circuit breaker for reliability:
class CircuitBreaker :
def __init__ ( self , failure_threshold : int = 5 , recovery_timeout : int = 60 ):
self .failure_threshold = failure_threshold
self .recovery_timeout = recovery_timeout
self .failure_count = 0
self .last_failure_time = None
self .state = 'CLOSED' # CLOSED, OPEN, HALF_OPEN
async def call ( self , func , * args , ** kwargs ):
if self .state == 'OPEN' :
if time.time() - self .last_failure_time < self .recovery_timeout:
raise CircuitBreakerOpen( "Circuit breaker is OPEN" )
else :
self .state = 'HALF_OPEN'
try :
result = await func( * args, ** kwargs)
self .reset()
return result
except Exception as e:
self .record_failure()
raise e
See africastalking_client.py:31 for circuit breaker implementation.
Retry Logic
Payments are retried automatically on failure:
from tenacity import retry, stop_after_attempt, wait_exponential
@retry (
stop = stop_after_attempt( 3 ),
wait = wait_exponential( multiplier = 1 , min = 1 , max = 10 )
)
async def mobile_checkout (
self ,
phone_number : str ,
amount : Union[ int , float ],
currency_code : str = "KES" ,
metadata : Optional[Dict] = None
) -> Dict[ str , Any]:
# Payment logic here
pass
See africastalking_client.py:236 for retry configuration.
Error Handling
try :
response = httpx.post( "/api/v1/payments/checkout" , json = payload)
response.raise_for_status()
result = response.json()
except httpx.HTTPStatusError as e:
if e.response.status_code == 400 :
# Invalid payment request
print ( f "Invalid request: { e.response.json()[ 'detail' ] } " )
elif e.response.status_code == 402 :
# Payment required - insufficient funds
print ( "Insufficient funds in mobile money account" )
elif e.response.status_code == 503 :
# Service unavailable
print ( "Payment service temporarily unavailable" )
# Implement exponential backoff retry
except Exception as e:
print ( f "Unexpected error: { str (e) } " )
Best Practices
Verify contract exists and is valid
Check payment amount matches contract
Confirm phone number format
Validate currency code
Verify webhook signatures
Process asynchronously
Implement idempotency
Log all webhook data
Send 200 OK immediately
Monitor Transaction Status
Poll status for pending transactions
Set timeout limits (e.g., 5 minutes)
Handle expired transactions
Retry failed payments
Never release without confirmation from both parties
Implement dispute resolution process
Set escrow timeout periods
Log all state changes with audit trail
Security Considerations
Webhook Signature Verification
from app.services.crypto_service import CryptoService
crypto = CryptoService()
# Verify webhook signature
def verify_webhook ( payload : str , signature : str ) -> bool :
return crypto.verify_webhook_signature(payload, signature)
See crypto_service.py:176 for signature verification.
Audit Trail
All payment actions are logged:
audit_signature = crypto.create_audit_signature(
action = "payment_released" ,
contract_id = "AG-2024-001234" ,
actor = "+254712345678" ,
data = {
"amount" : 350000 ,
"transaction_id" : "TXN-AG-2024-001234-001" ,
"recipient" : "+254723456789"
}
)
See crypto_service.py:130 for audit signatures.
Testing
Use Africa’s Talking sandbox for testing:
# Set environment to sandbox
export AT_ENVIRONMENT = sandbox
export AT_API_KEY = your_sandbox_api_key
# Test payment with test phone numbers
test_phones = [
"+254711000000" , # Always succeeds
"+254711000001" , # Always fails
"+254711000002" , # Timeout
]
Cost Structure
Mobile money transaction fees:
M-Pesa to Business : ~1-3% transaction fee
Business to M-Pesa : Flat rate + percentage
Balance Inquiry : Free
Failed Transactions : No charge
Next Steps
Voice Contracts Create payment-enabled contracts
SMS Verification Confirm payments via SMS
USSD Integration Check payment status via USSD
Digital Signatures Cryptographic payment verification