Payments API
Create and manage payments on Base mainnet (crypto) or via Stripe Checkout (USD cards). Both rails share the same payment lifecycle, webhooks, and verify semantics.
| Status | Description |
|---|---|
pending | Payment created, waiting for on-chain transfer to the workspace receiving address |
detected | Incoming transfer detected on Base (not yet confirmed) |
confirmed | Required block confirmations met |
forwarded | Net amount sent to merchant wallet, fee sent to treasury. Settlement complete. |
failed | Settlement failed (insufficient funds, chain error, etc.) |
expired | No transfer received within the expiry window (default: 60 minutes) |
cancelled | Merchant cancelled the payment before it was confirmed |
pd_test_ keys), crypto payments complete instantly (pending → forwarded). Stripe payments also auto-complete without a real Checkout session.Set paymentMethod on POST /api/v1/payments. Responses include paymentProvider (onchain or stripe).
| Rail | paymentMethod | tokenSymbol |
|---|---|---|
| Crypto (Base) | crypto (default) | USDC, ETH, ADAO |
| Stripe Checkout | stripe | USD only |
Live Stripe payments return checkoutUrl. Fulfillment is handled by POST /api/webhooks/stripe (configure STRIPE_WEBHOOK_SECRET in your deployment).
Merchant backend PayDirect Stripe
| | |
| POST /api/v1/payments | |
| (paymentMethod: stripe) | |
|----------------------------->| create payment row |
| | create Checkout Session |
| |--------------------------->|
|<-----------------------------| checkoutUrl + paymentUrl |
| redirect payer | |
|----------------------------->| /pay/:id (hosted) |
| | redirect to Stripe |
| | |
| |<----- checkout.session.completed
| | POST /api/webhooks/stripe |
| | completeStripePayment() |
|<----- payment.forwarded -----| outbound webhook |PayDirect deployment (ops)
STRIPE_API_KEY=sk_live_... # or STRIPE_SECRET_KEY
STRIPE_WEBHOOK_SECRET=whsec_... # from Stripe Dashboard destinationStripe Dashboard webhook destination
- URL:
https://www.paydirect.com/api/webhooks/stripe - Events from: Your account (not Connected accounts, unless you use Connect)
- API version:
2026-05-27.dahlia - Payload style: Snapshot
- Events:
checkout.session.completed,checkout.session.expired,payment_intent.payment_failed
Use one destination per environment. Multiple destinations with different signing secrets will break verification.
Live vs sandbox
| Environment | Stripe behavior |
|---|---|
pd_test_ sandbox | Auto-completes lifecycle instantly. No real Checkout session. |
pd_live_ live | Returns checkoutUrl. Fulfillment via Stripe webhook → forwarded. |
See also: Stripe inbound webhooks, Merchant checkout guide.
Merchants integrate PayDirect as the payment layer. You choose the rail at checkout time; fulfillment uses the same outbound webhooks and POST /api/v1/verify for both rails.
1. Register outbound webhooks (your server)
POST /api/v1/webhooks
{
"url": "https://your-app.com/webhooks/paydirect",
"events": ["payment.forwarded", "payment.failed", "payment.expired"]
}Fulfill orders on payment.forwarded. Check payment.paymentProvider (onchain or stripe) if you display different receipts.
2. Create payment when user checks out
// User chose "Pay with card"
const res = await fetch("https://www.paydirect.com/api/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${PAYDIRECT_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `order-${orderId}-stripe`,
},
body: JSON.stringify({
paymentMethod: "stripe",
tokenSymbol: "USD",
amount: "49.00",
description: "Pro listing — 30 days",
metadata: { orderId, userId, listingId },
}),
});
const { checkoutUrl, paymentUrl, payment } = await res.json();
// Option A: redirect to Stripe Checkout directly (live)
if (checkoutUrl) window.location.href = checkoutUrl;
// Option B: hosted PayDirect page (works for both rails)
// window.location.href = paymentUrl; // https://www.paydirect.com/pay/{id}3. Crypto checkout (unchanged)
const res = await fetch("https://www.paydirect.com/api/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${PAYDIRECT_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `order-${orderId}-crypto`,
},
body: JSON.stringify({
paymentMethod: "crypto",
tokenSymbol: "USDC",
amount: "49.00",
merchantWallet: MERCHANT_BASE_ADDRESS,
metadata: { orderId, userId, listingId },
}),
});
const { receivingAddress, paymentUrl, payment } = await res.json();
// Show QR / address, or send user to paymentUrl4. Confirm before fulfilling (poll or webhook)
// After user returns from Checkout, or on webhook receipt:
const { verified, payment } = await fetch("https://www.paydirect.com/api/v1/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${PAYDIRECT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ paymentId: payment.id }),
}).then((r) => r.json());
if (verified) {
await activateListing(payment.metadata.listingId);
}Integration checklist
- Use separate
Idempotency-Keyvalues per rail (e.g.order-123-stripevsorder-123-crypto). - Store
payment.idon your order record before redirecting the payer. - Prefer
payment.forwardedwebhook over polling for production fulfillment. - Stripe minimum charge: $0.50 USD.
- USD fee: 1.5% (Free tier), same model as USDC.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
paymentMethod | string | No | "crypto" (default) or "stripe" |
tokenSymbol | string | Yes* | Crypto: USDC, ETH, ADAO. Stripe: USD |
amount | string | Yes | Positive amount (e.g. "15.00") |
merchantWallet | string | Crypto: Yes | Destination Base address (0x...). Optional for Stripe (uses workspace settlement address). |
returnUrl | string | No | Customer return URL. Surfaces as a Return to merchant button on the hosted checkout (success, cancel, expired screens). Also used as the Stripe Checkout success_url for the Stripe rail. Must be http(s)://. Aliases: successUrl, merchantReturnUrl, or the same keys nested in metadata. |
cancelUrl | string | No | Stripe Checkout cancel redirect URL. Must be http(s)://. |
description | string | No | Human-readable description |
metadata | object | No | Arbitrary key-value metadata (e.g. orderId) |
expiresInMinutes | number | No | Minutes until expiry (default: 60) |
walletType | string | No | "eoa" or "smart_wallet". Which wallet receives the payment. Defaults to workspace setting. |
Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer pd_test_... or pd_live_... |
Idempotency-Key | No | Unique string to prevent duplicate payments |
Stripe cURL Example
curl -X POST https://www.paydirect.com/api/v1/payments \
-H "Authorization: Bearer pd_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"paymentMethod": "stripe",
"tokenSymbol": "USD",
"amount": "25.00",
"description": "Pro subscription"
}'Crypto cURL Example
curl -X POST https://www.paydirect.com/api/v1/payments \
-H "Authorization: Bearer pd_test_abc123..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-123-attempt-1" \
-d '{
"tokenSymbol": "USDC",
"amount": "100.00",
"merchantWallet": "0xYourBaseAddress...",
"description": "Invoice #1234",
"metadata": { "orderId": "ORD_123", "customer": "alice" }
}'TypeScript SDK
const { payment, receivingAddress, paymentUrl } = await client.createPayment({
tokenSymbol: "USDC",
amount: "100.00",
merchantWallet: "0xYourBaseAddress...",
description: "Invoice #1234",
metadata: { orderId: "ORD_123" },
});
// Share paymentUrl with your customer — they can pay without an API key
// e.g. https://www.paydirect.com/pay/a1b2c3d4-e5f6-...Response — Crypto (201 Created)
{
"payment": { "id": "...", "paymentProvider": "onchain", "tokenSymbol": "USDC", "status": "pending", ... },
"paymentMethod": "crypto",
"paymentProvider": "onchain",
"receivingAddress": "0xWorkspaceWalletAddress...",
"paymentUrl": "https://www.paydirect.com/pay/a1b2c3d4-e5f6-..."
}Response — Stripe live (201 Created)
{
"payment": { "id": "...", "paymentProvider": "stripe", "tokenSymbol": "USD", "status": "pending", ... },
"paymentMethod": "stripe",
"paymentProvider": "stripe",
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_live_...",
"stripeCheckoutSessionId": "cs_live_...",
"paymentUrl": "https://www.paydirect.com/pay/a1b2c3d4-e5f6-..."
}Query Parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status (pending, detected, confirmed, forwarded, failed, expired, cancelled) |
environment | string | Filter by environment (sandbox, live) |
limit | number | Max results (1-100, default: 50) |
offset | number | Pagination offset (default: 0) |
Example
curl "https://www.paydirect.com/api/v1/payments?status=forwarded&limit=10" \
-H "Authorization: Bearer pd_test_abc123..."Response (200 OK)
{
"payments": [ { ... }, { ... } ],
"total": 42
}Path Parameters
| Parameter | Description |
|---|---|
id | Payment UUID |
curl https://www.paydirect.com/api/v1/payments/a1b2c3d4-e5f6-... \
-H "Authorization: Bearer pd_test_abc123..."Response (200 OK)
{
"payment": {
"id": "a1b2c3d4-e5f6-...",
"status": "forwarded",
"tokenSymbol": "USDC",
"grossAmount": "100.00",
"feeAmount": "1.50",
"netAmount": "98.50",
"paymentProvider": "onchain",
...
}
}Returns 404 if the payment does not exist or does not belong to the authenticated workspace.
curl -X POST https://www.paydirect.com/api/v1/payments/a1b2c3d4.../cancel \
-H "Authorization: Bearer pd_test_abc123..."Response (200 OK)
{
"payment": {
"id": "a1b2c3d4-...",
"status": "cancelled",
...
}
}Only payments in pending status can be cancelled. Returns 400 if the payment is already confirmed or forwarded. Fires a payment.cancelled webhook event.
Crypto payments are verified when status is forwarded. Stripe payments verify at forwarded or confirmed.
Request Body
| Field | Type | Description |
|---|---|---|
paymentId | string | The payment UUID to verify |
curl -X POST https://www.paydirect.com/api/v1/verify \
-H "Authorization: Bearer pd_test_abc123..." \
-H "Content-Type: application/json" \
-d '{"paymentId": "a1b2c3d4-..."}'Response (200 OK)
{
"verified": true,
"payment": {
"id": "a1b2c3d4-...",
"status": "forwarded",
...
}
}verified is true only when the payment status is forwarded (fully settled). Use this endpoint to confirm settlement before fulfilling orders.
