Docus Logo
Charcoles Payments

API Endpoints

Complete reference for all 4 payment endpoints with request and response examples.

POST /payments/create-intent

Create a payment intent. This endpoint returns either a clientSecret (Stripe) or checkoutUrl (LemonSqueezy) for the frontend to proceed with payment.

Authentication: JWT required (Bearer token)

Request

{
  "amount": 2999,
  "currency": "usd",
  "metadata": {
    "orderId": "order_123",
    "variantId": "78901"
  }
}

Fields:

  • amount (number, required) — Amount in the smallest currency unit (cents for USD, paisas for PKR). Example: 2999 = $29.99
  • currency (string, required) — ISO 4217 currency code, 3 letters, case-insensitive. Examples: usd, pkr, gbp
  • metadata (object, optional) — Custom data to attach to the payment
    • orderId — Your internal order ID (optional but recommended)
    • variantId (string) — LemonSqueezy only — Required for LemonSqueezy, ignored by Stripe. The product variant ID in your LemonSqueezy store.

Response (Stripe)

{
  "success": true,
  "data": {
    "id": "pi_3abc...",
    "clientSecret": "pi_3abc..._secret_xyz",
    "status": "requires_payment_method",
    "amount": 2999,
    "currency": "usd"
  }
}

Pass clientSecret to Stripe.js on the frontend to confirm the payment.

Response (LemonSqueezy)

{
  "success": true,
  "data": {
    "id": "abc123",
    "checkoutUrl": "https://store.lemonsqueezy.com/checkout/buy/...",
    "status": "created",
    "amount": 2999,
    "currency": "usd"
  }
}

Redirect the user to checkoutUrl to complete checkout on LemonSqueezy's hosted page.


POST /payments/refund

Refund a payment, either fully or partially.

Authentication: JWT required (Bearer token)

Request

{
  "paymentId": "pi_3abc...",
  "amount": 1000
}

Fields:

  • paymentId (string, required) — The payment ID to refund
  • amount (number, optional) — Refund amount in smallest currency unit. Omit for full refund.

Response

{
  "success": true,
  "data": {
    "id": "re_456...",
    "status": "succeeded",
    "amount": 1000
  }
}

GET /payments/status/:paymentId

Check the current status of a payment.

Authentication: JWT required (Bearer token)

Request

GET /payments/status/pi_3abc...

No request body.

Response

{
  "success": true,
  "data": {
    "id": "pi_3abc...",
    "status": "paid",
    "amount": 2999,
    "currency": "usd"
  }
}

Status values (normalized across both providers):

StatusMeaning
pendingPayment created but not yet confirmed
paidPayment successfully charged
failedPayment failed or was cancelled
refundedPayment was refunded

POST /payments/webhook

Receive and process payment events from Stripe or LemonSqueezy.

Authentication: None — authenticated by provider signature (Stripe-Signature or X-Signature header)

This endpoint is called by your payment provider, not your frontend. Always return 200 OK to acknowledge receipt.

Signature Verification

The module automatically verifies webhook signatures using STRIPE_WEBHOOK_SECRET or LEMONSQUEEZY_WEBHOOK_SECRET. Unsigned or tampered webhooks are rejected.

Events

Stripe Events:

  • payment_intent.succeeded — Payment confirmed, fulfill order
  • payment_intent.payment_failed — Payment failed
  • charge.refunded — Refund processed

LemonSqueezy Events:

  • order_created — Payment confirmed, fulfill order
  • order_refunded — Refund processed
  • subscription_cancelled — Subscription ended

Response

Always return 200 OK:

{
  "received": true
}
Never return 4xx from a webhook unless signature verification fails. If you return 4xx, payment providers will retry the webhook and create a retry storm. If your fulfillment logic fails, log the error, return 200, and handle the retry manually.

Webhook Reliability

Never rely on frontend callbacks alone. Users close tabs, lose internet, and refresh pages. The only reliable confirmation is the webhook.

Always fulfill orders based on webhook events, not on frontend callbacks. Your server's controller handles deduplication automatically — if the same event arrives twice, it processes only once.


Example Usage

Create a Stripe Payment

curl -X POST http://localhost:3000/payments/create-intent \
  -H "Authorization: Bearer your-jwt-token" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 2999,
    "currency": "usd",
    "metadata": {"orderId": "order_123"}
  }'

Create a LemonSqueezy Payment

curl -X POST http://localhost:3000/payments/create-intent \
  -H "Authorization: Bearer your-jwt-token" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 2999,
    "currency": "usd",
    "metadata": {"variantId": "78901", "orderId": "order_456"}
  }'

Check Payment Status

curl -X GET http://localhost:3000/payments/status/pi_3abc... \
  -H "Authorization: Bearer your-jwt-token"

Refund a Payment

curl -X POST http://localhost:3000/payments/refund \
  -H "Authorization: Bearer your-jwt-token" \
  -H "Content-Type: application/json" \
  -d '{"paymentId": "pi_3abc..."}'

What Comes Next

  • Webhooks — Handle payment confirmations and events
  • Examples — Copy-paste examples for common scenarios