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 result

REST 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 StatusMeaningAction
400Bad requestCheck required fields (tokenSymbol, amount, merchantWallet)
401UnauthorizedVerify your API key is correct and active
409ConflictIdempotency key already used — existing payment returned
429Rate limitedWait for Retry-After seconds, then retry
500Server errorRetry 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 merchantWallet is 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