Integrating Payments
A complete guide to integrating PayDirect payments into your backend. Covers the REST API, SDKs, idempotency, webhook handling, and production best practices.
Server-Side Payment Creation
Always create payments from your backend to keep API keys secure
TypeScript SDK
import { PayDirectClient } from "@paydirect/sdk";
const client = new PayDirectClient({
apiKey: process.env.PAYDIRECT_API_KEY, // pd_test_ or pd_live_
baseUrl: "https://paydirect.com/api/v1",
});
async function createInvoicePayment(orderId: string, amount: string) {
const { payment, receivingAddress } = await client.createPayment({
tokenSymbol: "USDC",
amount,
merchantWallet: process.env.MERCHANT_WALLET,
description: `Invoice for order ${orderId}`,
metadata: { orderId },
});
return { paymentId: payment.id, receivingAddress, netAmount: payment.net_amount };
}Python SDK
from paydirect import PayDirectClient
client = PayDirectClient(
api_key=os.environ["PAYDIRECT_API_KEY"],
base_url="https://paydirect.com/api/v1",
)
def create_invoice_payment(order_id: str, amount: str):
result = client.create_payment(
token_symbol="USDC",
amount=amount,
merchant_wallet=os.environ["MERCHANT_WALLET"],
description=f"Invoice for order {order_id}",
metadata={"orderId": order_id},
)
return resultREST API (cURL)
curl -X POST https://paydirect.com/api/v1/payments \
-H "Authorization: Bearer $PAYDIRECT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-$ORDER_ID" \
-d '{
"tokenSymbol": "USDC",
"amount": "50.00",
"merchantWallet": "0xYourBaseAddress...",
"description": "Order payment",
"metadata": {"orderId": "'$ORDER_ID'"}
}'Idempotency
Prevent duplicate payments with idempotency keys
Pass a unique Idempotency-Key header (or idempotencyKey body field) with each payment creation request. If a payment with the same key already exists, the API returns the existing payment instead of creating a duplicate.
// Good: Use a deterministic key based on your order
const { payment } = await client.createPayment({
tokenSymbol: "USDC",
amount: "50.00",
merchantWallet: "0x...",
idempotencyKey: `order-${orderId}-payment`,
});Tip: Use your order ID or invoice number as the idempotency key. This ensures retries from network errors or timeouts never create duplicate payments.
Webhook Integration
React to payment status changes in real time
Instead of polling for payment status, register a webhook to receive push notifications. The most important event for order fulfillment is payment.forwarded.
1. Register a Webhook
curl -X POST https://paydirect.com/api/v1/webhooks \
-H "Authorization: Bearer pd_test_abc123..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/paydirect",
"events": ["payment.forwarded", "payment.failed", "payment.expired"]
}'
# Save the signingSecret from the response!2. Handle Webhook Events
import { PayDirectClient } from "@paydirect/sdk";
import express from "express";
const app = express();
app.use(express.json());
app.post("/webhooks/paydirect", (req, res) => {
const signature = req.headers["x-paydirect-signature"] as string;
const body = JSON.stringify(req.body);
if (!PayDirectClient.verifyWebhookSignature(
process.env.WEBHOOK_SECRET,
body,
signature
)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = req.headers["x-paydirect-event"];
const deliveryId = req.headers["x-paydirect-delivery"];
const payment = req.body.payment;
switch (event) {
case "payment.forwarded":
// Settlement complete — fulfill the order
fulfillOrder(payment.metadata.orderId);
break;
case "payment.failed":
// Notify customer of failure
notifyPaymentFailed(payment.metadata.orderId);
break;
case "payment.expired":
// Release reserved inventory
releaseInventory(payment.metadata.orderId);
break;
}
res.status(200).json({ received: true });
});Checking Payment Status
Retrieve or verify payments at any time
Get Payment by ID
const { payment } = await client.getPayment("a1b2c3d4-...");
console.log(payment.status); // "forwarded"Verify Settlement
const { verified } = await client.verifyPayment("a1b2c3d4-...");
if (verified) {
// Safe to fulfill the order
}List Payments with Filters
const { payments, total } = await client.listPayments({
status: "forwarded",
limit: 20,
offset: 0,
});Error Handling
Common errors and how to handle them
| HTTP Status | Meaning | Action |
|---|---|---|
400 | Bad request | Check required fields (tokenSymbol, amount, merchantWallet) |
401 | Unauthorized | Verify your API key is correct and active |
409 | Conflict | Idempotency key already used — existing payment returned |
429 | Rate limited | Wait for Retry-After seconds, then retry |
500 | Server error | Retry with exponential backoff |
try {
const { payment } = await client.createPayment({ ... });
} catch (error) {
if (error.status === 429) {
const retryAfter = error.headers?.["retry-after"] || 5;
await sleep(retryAfter * 1000);
// retry
} else {
console.error("Payment creation failed:", error.message);
}
}Going Live
Checklist for switching from sandbox to production
- Switch to a
pd_live_API key from your dashboard - Verify your
merchantWalletis a Base address you control - Ensure your webhook endpoint is publicly accessible over HTTPS
- Implement signature verification on your webhook handler
- Use idempotency keys to prevent duplicate payments in production
- Test with a small real payment before processing large amounts
