ℹ️ Note: Paycrest currently supports stablecoin-to-fiat (offramp) transactions only. Fiat-to-stablecoin (onramp) is coming in Q3 2025.In this guide, we demonstrate how to enable off-ramps for users with the Sender API. The main difference between the Sender API and the Gateway contract is that users get a receiving address to pay for rather than connecting their non-custodial wallets. This means users can off-ramp directly from any wallet.
Getting Started
Step 1: Obtain API Credentials
First, you need to get theAPI Key from your sender dashboard.
Visit your Sender Dashboard to retrieve your API Key and API Secret. If you’re a new user, sign up as a “sender” and complete our Know-Your-Business (KYB) process. Your API Secret should always be kept secret - we’ll get to this later in the article.
Step 2: Configure Tokens
Head over to the settings page of your Sender Dashboard to configure thefeePercent, feeAddress, and refundAddress across the tokens and blockchain networks you intend to use.
Step 3: Authentication Setup
Include yourAPI Key in the “API-Key” header of every request you make to Paycrest Offramp API.
Copy
const headers = {
"API-Key": "208a4aef-1320-4222-82b4-e3bca8781b4b",
};
401: Unauthorized.
Creating Payment Orders
Step 1: Fetch the Rate
Before creating a payment order, you must first fetch the current rate for your token/amount/currency combination. This ensures you’re using an up-to-date rate that’s achievable by the system.- cURL
- JavaScript
- Python
- Go
Copy
# Get rate for USDT on Base network
curl -X GET "https://api.paycrest.io/v1/rates/USDT/100/NGN?network=base" \
-H "Content-Type: application/json"
Copy
// Fetch the current rate
const fetchRate = async (token, amount, currency, network) => {
const response = await fetch(
`https://api.paycrest.io/v1/rates/${token}/${amount}/${currency}?network=${network}`,
{
method: "GET",
headers: {
"Content-Type": "application/json"
}
}
);
if (!response.ok) {
throw new Error(`Rate fetch failed: ${response.statusText}`);
}
const rateData = await response.json();
return rateData.data; // Returns the rate as a string
};
// Example usage
const rate = await fetchRate("USDT", 100, "NGN", "base");
console.log('Current rate:', rate); // e.g., "1500.50"
Copy
import requests
def fetch_rate(token, amount, currency, network):
"""Fetch the current rate for a token/amount/currency combination"""
response = requests.get(
f"https://api.paycrest.io/v1/rates/{token}/{amount}/{currency}",
params={"network": network},
headers={"Content-Type": "application/json"}
)
if not response.ok:
raise Exception(f"Rate fetch failed: {response.text}")
rate_data = response.json()
return rate_data["data"] # Returns the rate as a string
# Example usage
rate = fetch_rate("USDT", 100, "NGN", "base")
print('Current rate:', rate) # e.g., "1500.50"
Copy
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
type RateResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Data string `json:"data"`
}
func fetchRate(token, amount, currency, network string) (string, error) {
url := fmt.Sprintf("https://api.paycrest.io/v1/rates/%s/%s/%s?network=%s",
token, amount, currency, network)
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("rate fetch failed: %s", string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var rateResponse RateResponse
if err := json.Unmarshal(body, &rateResponse); err != nil {
return "", err
}
return rateResponse.Data, nil
}
// Example usage
func main() {
rate, err := fetchRate("USDT", 100, "NGN", "base")
if err != nil {
fmt.Printf("Error fetching rate: %v\n", err)
return
}
fmt.Printf("Current rate: %s\n", rate) // e.g., "1500.50"
}
The rate endpoint is publicly accessible and doesn’t require authentication. However, the rate you receive is only valid for a limited time and should be used immediately when creating your payment order.
Always fetch a fresh rate before creating each payment order. Rates can fluctuate frequently, and using an outdated rate will cause your order creation to fail.
Step 2: Create the Payment Order
Now that you have the current rate, you can create the payment order using that rate.- cURL
- JavaScript
- Python
- Go
Copy
# First, fetch the current rate
RATE=$(curl -s -X GET "https://api.paycrest.io/v1/rates/USDT/100/NGN?network=base" \
-H "Content-Type: application/json" | jq -r '.data')
# Then create the order with the fetched rate
curl -X POST "https://api.paycrest.io/v1/sender/orders" \
-H "API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 100,
"token": "USDT",
"network": "base",
"rate": "'$RATE'",
"recipient": {
"institution": "GTB",
"accountIdentifier": "1234567890",
"accountName": "John Doe",
"currency": "NGN",
"memo": "Salary payment for January 2024"
},
"reference": "payment-123",
"returnAddress": "0x1234567890123456789012345678901234567890"
}'
Copy
// First, fetch the current rate
const rate = await fetchRate("USDT", 100, "NGN", "base");
// Then create the payment order with the fetched rate
const orderData = {
amount: 100,
token: 'USDT',
network: 'base',
rate: rate, // Use the fetched rate
recipient: {
institution: 'GTB',
accountIdentifier: '1234567890',
accountName: 'John Doe',
currency: 'NGN',
memo: 'Salary payment for January 2024' // Optional: Purpose/narration for the payment
},
reference: 'payment-123',
returnAddress: '0x1234567890123456789012345678901234567890'
};
const response = await fetch("https://api.paycrest.io/v1/sender/orders", {
method: "POST",
headers: {
"API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify(orderData)
});
const order = await response.json();
console.log('Order created:', order);
Copy
import requests
# First, fetch the current rate
rate = fetch_rate("USDT", 100, "NGN", "base")
# Then create the payment order with the fetched rate
order_data = {
"amount": 100,
"token": "USDT",
"network": "base",
"rate": rate, # Use the fetched rate
"recipient": {
"institution": "GTB",
"accountIdentifier": "1234567890",
"accountName": "John Doe",
"currency": "NGN",
"memo": "Salary payment for January 2024" # Optional: Purpose/narration for the payment
},
"reference": "payment-123",
"returnAddress": "0x1234567890123456789012345678901234567890"
}
response = requests.post(
"https://api.paycrest.io/v1/sender/orders",
headers={
"API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
json=order_data
)
order = response.json()
print('Order created:', order)
Copy
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
// First, fetch the current rate
rate, err := fetchRate("USDT", 100, "NGN", "base")
if err != nil {
return fmt.Errorf("failed to fetch rate: %v", err)
}
// Then create the payment order with the fetched rate
func createPaymentOrder() error {
orderData := map[string]interface{}{
"amount": 100,
"token": "USDT",
"network": "base",
"rate": rate, // Use the fetched rate
"recipient": map[string]interface{}{
"institution": "GTB",
"accountIdentifier": "1234567890",
"accountName": "John Doe",
"currency": "NGN",
"memo": "Salary payment for January 2024", // Optional: Purpose/narration for the payment
},
"reference": "payment-123",
"returnAddress": "0x1234567890123456789012345678901234567890",
}
jsonData, _ := json.Marshal(orderData)
req, _ := http.NewRequest("POST", "https://api.paycrest.io/v1/sender/orders", bytes.NewBuffer(jsonData))
req.Header.Set("API-Key", "YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println("Order created:", string(body))
return nil
}
Handle the Response
- JavaScript
- Python
- Go
Copy
// The response includes important information
const {
id, // Order ID for tracking
receiveAddress, // Address to send tokens to
validUntil, // Expiration time
senderFee, // Fee amount
transactionFee // Network transaction fee
} = order;
// Store the order ID for tracking
await saveOrderToDatabase(order.id, order);
Copy
# The response includes important information
order_id = order['id'] # Order ID for tracking
receive_address = order['receiveAddress'] # Address to send tokens to
valid_until = order['validUntil'] # Expiration time
sender_fee = order['senderFee'] # Fee amount
transaction_fee = order['transactionFee'] # Network transaction fee
# Store the order ID for tracking
save_order_to_database(order['id'], order)
Copy
// The response includes important information
type OrderResponse struct {
ID string `json:"id"`
ReceiveAddress string `json:"receiveAddress"`
ValidUntil string `json:"validUntil"`
SenderFee float64 `json:"senderFee"`
TransactionFee float64 `json:"transactionFee"`
}
Send Tokens to Receive Address
- JavaScript
- Python
- Go
Copy
// Using viem to send tokens
import { createPublicClient, createWalletClient, http, getContract, parseUnits } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
const publicClient = createPublicClient({
chain: base,
transport: http('https://mainnet.base.org')
});
const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const walletClient = createWalletClient({
account,
chain: base,
transport: http('https://mainnet.base.org')
});
// USDT contract on Base
const usdtContract = getContract({
address: '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb', // USDT on Base
abi: [{
name: 'transfer',
type: 'function',
inputs: [
{ name: 'to', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [{ name: '', type: 'bool' }],
stateMutability: 'nonpayable'
}],
publicClient,
walletClient
});
// Send tokens to the receive address
const { request } = await usdtContract.simulate.transfer({
args: [order.receiveAddress, parseUnits(order.amount, 6)] // USDT has 6 decimals
});
const hash = await walletClient.writeContract(request);
console.log('Transaction hash:', hash);
Copy
# Using web3.py to send tokens
from web3 import Web3
from eth_account import Account
import os
# Connect to Base network
w3 = Web3(Web3.HTTPProvider('https://mainnet.base.org'))
# Load private key
account = Account.from_key(os.getenv('PRIVATE_KEY'))
# USDT contract on Base
usdt_address = '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb'
usdt_abi = [{
'name': 'transfer',
'type': 'function',
'inputs': [
{'name': 'to', 'type': 'address'},
{'name': 'amount', 'type': 'uint256'}
],
'outputs': [{'name': '', 'type': 'bool'}],
'stateMutability': 'nonpayable'
}]
usdt_contract = w3.eth.contract(address=usdt_address, abi=usdt_abi)
# Send tokens to the receive address
amount_wei = w3.to_wei(order['amount'], 'ether') # USDT has 6 decimals
tx = usdt_contract.functions.transfer(
order['receiveAddress'],
amount_wei
).build_transaction({
'from': account.address,
'gas': 100000,
'gasPrice': w3.eth.gas_price,
'nonce': w3.eth.get_transaction_count(account.address)
})
# Sign and send transaction
signed_tx = w3.eth.account.sign_transaction(tx, account.key)
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
print('Transaction hash:', tx_hash.hex())
Copy
// Using go-ethereum to send tokens
package main
import (
"context"
"fmt"
"log"
"math/big"
"os"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
)
func sendTokens(order map[string]interface{}) error {
// Connect to Base network
client, err := ethclient.Dial("https://mainnet.base.org")
if err != nil {
return err
}
// Load private key
privateKey, err := crypto.HexToECDSA(os.Getenv("PRIVATE_KEY"))
if err != nil {
return err
}
// USDT contract on Base
usdtAddress := common.HexToAddress("0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb")
// Transfer function signature
transferSig := crypto.Keccak256([]byte("transfer(address,uint256)"))
transferSig = transferSig[:4]
// Prepare transaction data
amount := new(big.Int)
amount.SetString(order["amount"].(string), 10)
amount.Mul(amount, new(big.Int).Exp(big.NewInt(10), big.NewInt(6), nil)) // USDT has 6 decimals
toAddress := common.HexToAddress(order["receiveAddress"].(string))
// Encode function call
data := append(transferSig, toAddress.Bytes()...)
data = append(data, common.LeftPadBytes(amount.Bytes(), 32)...)
// Get nonce
nonce, err := client.PendingNonceAt(context.Background(), crypto.PubkeyToAddress(privateKey.PublicKey))
if err != nil {
return err
}
// Create transaction
tx := &types.Transaction{
To: &usdtAddress,
Value: big.NewInt(0),
Gas: 100000,
GasPrice: big.NewInt(20000000000), // 20 gwei
Nonce: nonce,
Data: data,
}
// Sign transaction
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(big.NewInt(8453)), privateKey)
if err != nil {
return err
}
// Send transaction
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
return err
}
fmt.Printf("Transaction hash: %s\n", signedTx.Hash().Hex())
return nil
}
The amount you send to the receive address should be the sum of
amount, senderFee, and transactionFee as returned in the order response.Order Status Monitoring
Your status can either be any of the following:payment_order.pending- Order created, waiting for provider assignmentpayment_order.validated- Funds have been sent to recipient’s bank/mobile network (value transfer confirmed)payment_order.expired- Order expired without completionpayment_order.settled- Order fully completed on blockchainpayment_order.refunded- Funds refunded to sender
validated, expired, or refunded.
You can tell your user the transaction was successful (or provide value) at the
validated status, since this indicates funds have been sent to the recipient’s bank/mobile network. The settled status occurs when the provider has received the stablecoin on-chain, which is separate from the sender-to-recipient money flow.Webhook Implementation
- JavaScript
- Python
- Go
Copy
// Server setup and webhook endpoint
app.post("/webhook", async (req, res, next) => {
const signature = req.get("X-Paycrest-Signature");
if (!signature) return false;
if (!verifyPaycrestSignature(req.body, signature, process.env.API_SECRET!)) {
return res.status(401).send("Invalid signature");
}
console.log("Webhook received:", req.body);
try {
const transaction = await prisma.transaction.create({
data: {
id: req.body.data.id,
status: req.body.event,
},
});
res.json({ data: transaction });
} catch (err) {
next(err);
}
res.status(200).send("Webhook received");
});
function verifyPaycrestSignature(requestBody, signatureHeader, secretKey) {
const calculatedSignature = calculateHmacSignature(requestBody, secretKey);
return signatureHeader === calculatedSignature;
}
function calculateHmacSignature(data, secretKey) {
const crypto = require('crypto');
const key = Buffer.from(secretKey);
const hash = crypto.createHmac("sha256", key);
hash.update(data);
return hash.digest("hex");
}
Copy
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import os
app = Flask(__name__)
@app.route("/webhook", methods=["POST"])
def webhook():
signature = request.headers.get("X-Paycrest-Signature")
if not signature:
return jsonify({"error": "No signature"}), 401
if not verify_paycrest_signature(request.data, signature, os.getenv("API_SECRET")):
return jsonify({"error": "Invalid signature"}), 401
print("Webhook received:", request.json)
try:
transaction = Transaction(
id=request.json["data"]["id"],
status=request.json["event"]
)
db.session.add(transaction)
db.session.commit()
return jsonify({"data": transaction.to_dict()})
except Exception as e:
return jsonify({"error": str(e)}), 500
def verify_paycrest_signature(request_body, signature_header, secret_key):
calculated_signature = calculate_hmac_signature(request_body, secret_key)
return signature_header == calculated_signature
def calculate_hmac_signature(data, secret_key):
key = secret_key.encode('utf-8')
hash_obj = hmac.new(key, data, hashlib.sha256)
return hash_obj.hexdigest()
Copy
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type WebhookPayload struct {
Event string `json:"event"`
Data struct {
ID string `json:"id"`
} `json:"data"`
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-Paycrest-Signature")
if signature == "" {
http.Error(w, "No signature", http.StatusUnauthorized)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
if !verifyPaycrestSignature(body, signature, os.Getenv("API_SECRET")) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
fmt.Println("Webhook received:", string(body))
var payload WebhookPayload
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
transaction := Transaction{
ID: payload.Data.ID,
Status: payload.Event,
}
if err := db.Create(&transaction).Error; err != nil {
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Webhook received"))
}
func verifyPaycrestSignature(requestBody []byte, signatureHeader, secretKey string) bool {
calculatedSignature := calculateHmacSignature(requestBody, secretKey)
return signatureHeader == calculatedSignature
}
func calculateHmacSignature(data []byte, secretKey string) string {
key := []byte(secretKey)
hash := hmac.New(sha256.New, key)
hash.Write(data)
return hex.EncodeToString(hash.Sum(nil))
}
Webhook URLs are configured through the Sender Dashboard settings, not via API. Visit your dashboard to set up your webhook endpoint URL.
Polling Implementation
- JavaScript
- Python
- Go
Copy
// Status polling endpoint
app.get("/transactions/:id", async (req, res, next) => {
const { id } = req.params;
const transaction = await prisma.transaction.findUnique({
where: { id },
});
res.json({ data: transaction ? transaction : 'Non-existent transaction' });
});
// Poll for status updates
async function checkOrderStatus(orderId) {
try {
const response = await fetch(`https://api.paycrest.io/v1/sender/orders/${orderId}`, {
headers: { "API-Key": "YOUR_API_KEY" }
});
const order = await response.json();
switch (order.status) {
case 'pending':
console.log('Order is pending provider assignment');
break;
case 'validated':
console.log('Funds have been sent to recipient\'s bank/mobile network (value transfer confirmed)');
await handleOrderValidated(order);
break;
case 'settled':
console.log('Order has been settled on blockchain');
await handleOrderSettled(order);
break;
case 'refunded':
console.log('Order was refunded to the sender');
await handleOrderRefunded(order);
break;
case 'expired':
console.log('Order expired without completion');
await handleOrderExpired(order);
break;
}
return order;
} catch (error) {
console.error('Error checking order status:', error);
throw error;
}
}
Copy
# Status polling endpoint
@app.route("/transactions/<id>", methods=["GET"])
def get_transaction(id):
transaction = Transaction.query.filter_by(id=id).first()
if transaction:
return jsonify({"data": transaction.to_dict()})
else:
return jsonify({"data": "Non-existent transaction"})
# Poll for status updates
def check_order_status(order_id):
try:
response = requests.get(
f"https://api.paycrest.io/v1/sender/orders/{order_id}",
headers={"API-Key": "YOUR_API_KEY"}
)
order = response.json()
if order['status'] == 'pending':
print('Order is pending provider assignment')
elif order['status'] == 'validated':
print('Funds have been sent to recipient\'s bank/mobile network (value transfer confirmed)')
handle_order_validated(order)
elif order['status'] == 'settled':
print('Order has been settled on blockchain')
handle_order_settled(order)
elif order['status'] == 'refunded':
print('Order was refunded to the sender')
handle_order_refunded(order)
elif order['status'] == 'expired':
print('Order expired without completion')
handle_order_expired(order)
return order
except Exception as e:
print('Error checking order status:', str(e))
raise e
Copy
// Status polling endpoint
func getTransactionHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var transaction Transaction
result := db.Where("id = ?", id).First(&transaction)
if result.Error != nil {
json.NewEncoder(w).Encode(map[string]interface{}{
"data": "Non-existent transaction",
})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"data": transaction,
})
}
// Poll for status updates
type OrderStatus struct {
Status string `json:"status"`
CancellationReason string `json:"cancellationReason,omitempty"`
}
func checkOrderStatus(orderID string) (*OrderStatus, error) {
req, err := http.NewRequest("GET", fmt.Sprintf("https://api.paycrest.io/v1/sender/orders/%s", orderID), nil)
if err != nil {
return nil, err
}
req.Header.Set("API-Key", "YOUR_API_KEY")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var order OrderStatus
if err := json.Unmarshal(body, &order); err != nil {
return nil, err
}
switch order.Status {
case "pending":
fmt.Println("Order is pending provider assignment")
case "validated":
fmt.Println("Funds have been sent to recipient's bank/mobile network (value transfer confirmed)")
handleOrderValidated(order)
case "settled":
fmt.Println("Order has been settled on blockchain")
handleOrderSettled(order)
case "refunded":
fmt.Println("Order was refunded to the sender")
handleOrderRefunded(order)
case "expired":
fmt.Println("Order expired without completion")
handleOrderExpired(order)
}
return &order, nil
}
Error Handling
Rate Fetching Errors
When fetching rates, you may encounter these common errors:- 400 Bad Request: Invalid token, amount, or currency combination
- 404 Not Found: No provider available for the specified parameters
- Network Errors: Connection issues or timeouts
- JavaScript
- Python
- Go
Copy
async function fetchRateWithErrorHandling(token, amount, currency, network) {
try {
const response = await fetch(
`https://api.paycrest.io/v1/rates/${token}/${amount}/${currency}?network=${network}`,
{
method: "GET",
headers: {
"Content-Type": "application/json"
}
}
);
if (!response.ok) {
if (response.status === 400) {
const errorData = await response.json();
throw new Error(`Rate validation failed: ${errorData.message}`);
} else if (response.status === 404) {
throw new Error('No provider available for this token/amount/currency combination');
} else {
throw new Error(`Rate fetch failed: ${response.statusText}`);
}
}
const rateData = await response.json();
return rateData.data;
} catch (error) {
console.error('Error fetching rate:', error);
throw error;
}
}
Copy
def fetch_rate_with_error_handling(token, amount, currency, network):
try:
response = requests.get(
f"https://api.paycrest.io/v1/rates/{token}/{amount}/{currency}",
params={"network": network},
headers={"Content-Type": "application/json"}
)
if not response.ok:
if response.status_code == 400:
error_data = response.json()
raise Exception(f"Rate validation failed: {error_data['message']}")
elif response.status_code == 404:
raise Exception("No provider available for this token/amount/currency combination")
else:
raise Exception(f"Rate fetch failed: {response.text}")
rate_data = response.json()
return rate_data["data"]
except Exception as error:
print(f"Error fetching rate: {error}")
raise error
Copy
func fetchRateWithErrorHandling(token, amount, currency, network string) (string, error) {
url := fmt.Sprintf("https://api.paycrest.io/v1/rates/%s/%s/%s?network=%s",
token, amount, currency, network)
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("network error: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %v", err)
}
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == 400 {
return "", fmt.Errorf("rate validation failed: %s", string(body))
} else if resp.StatusCode == 404 {
return "", fmt.Errorf("no provider available for this token/amount/currency combination")
} else {
return "", fmt.Errorf("rate fetch failed: %s", resp.Status)
}
}
var rateResponse RateResponse
if err := json.Unmarshal(body, &rateResponse); err != nil {
return "", fmt.Errorf("failed to parse response: %v", err)
}
return rateResponse.Data, nil
}
Order Creation Error Handling
When creating payment orders, you may encounter these common errors:- 400 Bad Request: Invalid payload, missing required fields, or validation errors
- 401 Unauthorized: Invalid or missing API key
- 429 Too Many Requests: Rate limit exceeded
- 500 Internal Server Error: Server-side issues
Copy
{
"status": "error",
"message": "Failed to validate payload",
"data": {
"field": "Rate",
"message": "Provided rate 1500.00 is not achievable. Available rate is 1500.50"
}
}
API Error Handling
- JavaScript
- Python
- Go
- cURL
Copy
async function createPaymentOrder(orderData) {
try {
const response = await fetch("https://api.paycrest.io/v1/sender/orders", {
method: "POST",
headers: {
"API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify(orderData)
});
if (!response.ok) {
if (response.status === 400) {
// Validation error
const validationErrors = await response.json();
throw new Error(`Validation failed: ${JSON.stringify(validationErrors)}`);
} else if (response.status === 401) {
// Authentication error
throw new Error('Invalid API key');
} else if (response.status === 429) {
// Rate limit exceeded
throw new Error('Rate limit exceeded. Please try again later.');
} else {
// Other errors
throw new Error(`API error: ${response.statusText}`);
}
}
return await response.json();
} catch (error) {
console.error('Error creating payment order:', error);
throw error;
}
}
Copy
import requests
def create_payment_order(order_data):
try:
response = requests.post(
"https://api.paycrest.io/v1/sender/orders",
headers={
"API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
json=order_data
)
if not response.ok:
if response.status_code == 400:
# Validation error
validation_errors = response.json()
raise Exception(f"Validation failed: {validation_errors}")
elif response.status_code == 401:
# Authentication error
raise Exception("Invalid API key")
elif response.status_code == 429:
# Rate limit exceeded
raise Exception("Rate limit exceeded. Please try again later.")
else:
# Other errors
raise Exception(f"API error: {response.text}")
return response.json()
except Exception as error:
print(f"Error creating payment order: {error}")
raise error
Copy
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
func createPaymentOrder(orderData map[string]interface{}) (map[string]interface{}, error) {
jsonData, _ := json.Marshal(orderData)
req, _ := http.NewRequest("POST", "https://api.paycrest.io/v1/sender/orders", bytes.NewBuffer(jsonData))
req.Header.Set("API-Key", "YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
switch resp.StatusCode {
case 400:
// Validation error
return nil, fmt.Errorf("validation failed: %s", string(body))
case 401:
// Authentication error
return nil, fmt.Errorf("invalid API key")
case 429:
// Rate limit exceeded
return nil, fmt.Errorf("rate limit exceeded. Please try again later.")
default:
// Other errors
return nil, fmt.Errorf("API error: %s", resp.Status)
}
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
Copy
# Error handling with cURL
response=$(curl -s -w "%{http_code}" -X POST "https://api.paycrest.io/v1/sender/orders" \
-H "API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 100,
"token": "USDT",
"network": "base",
"recipient": {
"institution": "GTB",
"accountIdentifier": "1234567890",
"accountName": "John Doe",
"currency": "NGN"
}
}')
http_code="${response: -3}"
response_body="${response%???}"
if [ "$http_code" = "400" ]; then
echo "Validation failed: $response_body"
elif [ "$http_code" = "401" ]; then
echo "Invalid API key"
elif [ "$http_code" = "429" ]; then
echo "Rate limit exceeded"
else
echo "API error: $http_code"
fi
Retry Logic
- JavaScript
- Python
- Go
- cURL
Copy
async function createOrderWithRetry(orderData, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await createPaymentOrder(orderData);
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
// Wait before retrying (exponential backoff)
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Copy
import asyncio
import time
async def create_order_with_retry(order_data, max_retries=3):
for attempt in range(1, max_retries + 1):
try:
return create_payment_order(order_data)
except Exception as error:
if attempt == max_retries:
raise error
# Wait before retrying (exponential backoff)
delay = 2 ** attempt
await asyncio.sleep(delay)
Copy
# Synchronous version
def create_order_with_retry_sync(order_data, max_retries=3):
for attempt in range(1, max_retries + 1):
try:
return create_payment_order(order_data)
except Exception as error:
if attempt == max_retries:
raise error
# Wait before retrying (exponential backoff)
delay = 2 ** attempt
time.sleep(delay)
Copy
import (
"fmt"
"time"
)
func createOrderWithRetry(orderData map[string]interface{}, maxRetries int) (map[string]interface{}, error) {
for attempt := 1; attempt <= maxRetries; attempt++ {
result, err := createPaymentOrder(orderData)
if err == nil {
return result, nil
}
if attempt == maxRetries {
return nil, err
}
// Wait before retrying (exponential backoff)
delay := time.Duration(1<<uint(attempt)) * time.Second
time.Sleep(delay)
}
return nil, fmt.Errorf("max retries exceeded")
}
Copy
#!/bin/bash
create_order_with_retry() {
local order_data="$1"
local max_retries=3
for ((attempt=1; attempt<=max_retries; attempt++)); do
response=$(curl -s -w "%{http_code}" -X POST "https://api.paycrest.io/v1/sender/orders" \
-H "API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d "$order_data")
http_code="${response: -3}"
if [ "$http_code" = "200" ]; then
echo "${response%???}"
return 0
fi
if [ $attempt -eq $max_retries ]; then
echo "Failed after $max_retries attempts"
return 1
fi
# Wait before retrying (exponential backoff)
delay=$((2 ** attempt))
sleep $delay
done
}
# Usage
order_data='{
"amount": 100,
"token": "USDT",
"network": "base",
"recipient": {
"institution": "GTB",
"accountIdentifier": "1234567890",
"accountName": "John Doe",
"currency": "NGN"
}
}'
create_order_with_retry "$order_data"
Production Considerations
- JavaScript
- Python
- Go
- cURL
Copy
// Use environment variables for sensitive data
const config = {
apiKey: process.env.PAYCREST_API_KEY,
webhookSecret: process.env.PAYCREST_WEBHOOK_SECRET
};
// Validate webhook signatures
app.post('/webhooks/paycrest', async (req, res) => {
const signature = req.headers['x-paycrest-signature'];
if (!validateWebhookSignature(req.body, signature, config.webhookSecret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook
// ...
});
Copy
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
# Use environment variables for sensitive data
config = {
'api_key': os.environ.get('PAYCREST_API_KEY'),
'webhook_secret': os.environ.get('PAYCREST_WEBHOOK_SECRET')
}
# Validate webhook signatures
@app.route('/webhooks/paycrest', methods=['POST'])
def webhook_handler():
signature = request.headers.get('x-paycrest-signature')
if not validate_webhook_signature(request.json, signature, config['webhook_secret']):
return jsonify({'error': 'Invalid signature'}), 401
# Process webhook
# ...
return jsonify({'status': 'success'}), 200
Copy
package main
import (
"net/http"
"os"
)
// Use environment variables for sensitive data
type Config struct {
APIKey string
WebhookSecret string
}
func getConfig() Config {
return Config{
APIKey: os.Getenv("PAYCREST_API_KEY"),
WebhookSecret: os.Getenv("PAYCREST_WEBHOOK_SECRET"),
}
}
// Validate webhook signatures
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("x-paycrest-signature")
if !validateWebhookSignature(r.Body, signature, config.WebhookSecret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process webhook
// ...
w.WriteHeader(http.StatusOK)
}
Copy
# Set environment variables
export PAYCREST_API_KEY="your_api_key_here"
export PAYCREST_WEBHOOK_SECRET="your_webhook_secret_here"
# Use environment variables in requests
curl -X POST "https://api.paycrest.io/v1/sender/orders" \
-H "API-Key: $PAYCREST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 100,
"token": "USDT",
"network": "base",
"recipient": {
"institution": "GTB",
"accountIdentifier": "1234567890",
"accountName": "John Doe",
"currency": "NGN"
}
}'
Testing
- JavaScript
- Python
- Go
Copy
// Using Jest for testing
describe('Paycrest Sender API Integration', () => {
test('should create payment order successfully', async () => {
const orderData = {
amount: 100,
token: 'USDT',
network: 'base',
rate: 1500.50,
recipient: {
institution: 'GTB',
accountIdentifier: '1234567890',
accountName: 'Test User',
currency: 'NGN'
}
};
const order = await createPaymentOrder(orderData);
expect(order.id).toBeDefined();
expect(order.receiveAddress).toBeDefined();
expect(order.status).toBe('pending');
});
test('should handle API errors gracefully', async () => {
const invalidOrderData = {
amount: -100, // Invalid amount
token: 'USDT',
network: 'base',
rate: 1500.50
};
await expect(createPaymentOrder(invalidOrderData))
.rejects
.toThrow('Validation failed');
});
});
Copy
import unittest
from unittest.mock import patch
# Using unittest for testing
class TestPaycrestSenderAPI(unittest.TestCase):
def test_create_payment_order_successfully(self):
order_data = {
'amount': 100,
'token': 'USDT',
'network': 'base',
'rate': 1500.50,
'recipient': {
'institution': 'GTB',
'accountIdentifier': '1234567890',
'accountName': 'Test User',
'currency': 'NGN'
}
}
with patch('requests.post') as mock_post:
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {
'id': 'test-order-id',
'receiveAddress': '0x123...',
'status': 'pending'
}
order = create_payment_order(order_data)
self.assertIsNotNone(order['id'])
self.assertIsNotNone(order['receiveAddress'])
self.assertEqual(order['status'], 'pending')
def test_handle_api_errors_gracefully(self):
invalid_order_data = {
'amount': -100, # Invalid amount
'token': 'USDT',
'network': 'base',
'rate': 1500.50
}
with patch('requests.post') as mock_post:
mock_post.return_value.status_code = 400
mock_post.return_value.json.return_value = {'error': 'Validation failed'}
with self.assertRaises(Exception) as context:
create_payment_order(invalid_order_data)
self.assertIn('Validation failed', str(context.exception))
Copy
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
// Using Go's testing package
func TestCreatePaymentOrder(t *testing.T) {
orderData := map[string]interface{}{
"amount": 100,
"token": "USDT",
"network": "base",
"rate": 1500.50,
"recipient": map[string]interface{}{
"institution": "GTB",
"accountIdentifier": "1234567890",
"accountName": "Test User",
"currency": "NGN",
},
}
// Create a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"id": "test-order-id",
"receiveAddress": "0x123...",
"status": "pending"
}`))
}))
defer server.Close()
// Test successful order creation
result, err := createPaymentOrder(orderData)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if result["id"] == nil {
t.Error("Expected order ID to be defined")
}
if result["receiveAddress"] == nil {
t.Error("Expected receive address to be defined")
}
if result["status"] != "pending" {
t.Errorf("Expected status 'pending', got %v", result["status"])
}
}
func TestHandleAPIErrors(t *testing.T) {
invalidOrderData := map[string]interface{}{
"amount": -100, // Invalid amount
"token": "USDT",
"network": "base",
"rate": 1500.50,
}
// Create a test server that returns an error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "Validation failed"}`))
}))
defer server.Close()
_, err := createPaymentOrder(invalidOrderData)
if err == nil {
t.Error("Expected error for invalid order data")
}
}
Deployment Checklist
Before going live, ensure you have:- KYC verification completed
- API credentials secured
- Webhook endpoints configured and tested
- Error handling implemented
- Monitoring and logging set up
- Rate limiting configured
- Security measures implemented
- Testing completed with small amounts
- Documentation updated
This backend structure can be done in any custom way depending on your app as long as the webhook validates and stores the correct payload sent to it.
Keep your API key secure and never share it publicly. Consider using environment variables for production deployments.