Payments API

Base URL: https://pay.codeappear.com/api/v1. Authenticate every request with your API key as a bearer token: Authorization: Bearer fk_live_.... All amounts are in BDT. An active subscription is required.

Create a payment

POST /api/v1/payments

{
  "amount":         500,                                   // required — integer or decimal, min 1
  "invoice_number": "ORDER-42",                            // required — your order reference; max 100 chars; idempotency key
  "customer_id":    1,                                     // optional — an existing customer belonging to your merchant account
  "success_url":    "https://shop.example.com/thanks",     // optional — customer returns here after paying
  "cancel_url":     "https://shop.example.com/cart",       // optional — customer returns here if they cancel
  "metadata":       {"channel": "web"}                     // optional — up to 20 keys, max 500 chars per value
}

Pass customer_id to attach a payment to a customer you already created via the Customers API. It must belong to your merchant account — an unknown or foreign customer_id returns 422 validation_error.

Response 201 — a payment intent only returns the fields that are actually meaningful before checkout; provider, payment method, and verification details aren't known yet and appear once you retrieve the payment:

{
  "data": {
    "id":                     "01jxyz...",
    "amount":                 "500.00",
    "amount_due":             "505.00",   // amount the customer must actually send (includes any cash-out charge)
    "currency":               "BDT",
    "livemode":               true,
    "status":                 "pending",
    "invoice_number":         "ORDER-42",
    "customer_id":            null,       // the customer_id you passed, or set automatically via a payment link
    "metadata":               {"channel": "web"},
    "success_url":            "https://shop.example.com/thanks",
    "cancel_url":             "https://shop.example.com/cart",
    "checkout_url":           "https://pay.codeappear.com/pay/01jxyz...",
    "expires_at":             "2026-06-12T15:00:00+06:00",
    "created_at":             "2026-06-12T14:30:00+06:00"
  }
}

Redirect your customer to checkout_url. Payments expire after 15 minutes if the customer does not submit a transaction ID. When success_url/cancel_url are set, the customer is returned there once checkout resolves, with payment_id and invoice_number appended to the URL.

Idempotency

invoice_number is your order reference and acts as an idempotency key. If a payment already exists for that invoice_number and hasn't reached failed or expired, creating another one is rejected with HTTP 409 and code: "duplicate_invoice_number" — so a dropped connection never double-charges. Look up the existing payment with GET https://pay.codeappear.com/api/v1/payments?invoice_number=… instead of retrying blindly. A new payment can only be created once the previous one for that invoice has failed or expired.

Payment lifecycle

pending → customer submits TrxID → awaiting_verification → SMS matched → completed. Terminal states: failed (e.g. duplicate_trx_id, rejected_by_admin), expired, partially_refunded, and refunded. Failed payments carry a failure_reason string.

Retrieve a payment

GET /api/v1/payments/{id}

Returns HTTP 404 if the payment doesn't belong to your merchant, or if it was created in a different mode than the key you're using (a live key can't retrieve a test payment, and vice versa).

List payments

GET /api/v1/payments?status=completed&per_page=25
GET /api/v1/payments?invoice_number=ORDER-42      // look up by your own reference

Returns a paginated list, newest first, scoped to your key's live/test mode. status, invoice_number, and per_page (max 100, default 25) are optional filters.

Refund a payment

POST /api/v1/payments/{id}/refund

{
  "amount": 200   // optional — omit to refund the full remaining balance
}

Refunds a completed or partially_refunded payment. Omit amount (or the whole request body) to refund whatever is still refundable; pass a smaller amount to refund only part of it. Reaching the full amount — across one or more calls — marks the payment refunded; anything less marks it partially_refunded, which can still receive further refunds.

Returns the updated payment object, including refunded_amount, with HTTP 200. Returns HTTP 422 if the payment isn't completed or partially_refunded, if amount exceeds the remaining refundable balance, or HTTP 404 if it belongs to a different mode than your key (same rule as above). Firing this endpoint dispatches a payment.refunded webhook once the payment is fully refunded, or payment.partially_refunded otherwise, to all subscribed endpoints.

Refund history

GET /api/v1/payments/{id}/refunds

Every refund call is recorded as its own entry, so partial refunds don't just add up into one number — you can see exactly when each one happened and how much it was for:

{
  "data": [
    {
      "id":         2,
      "amount":     "600.00",
      "source":     "api",
      "created_at": "2026-06-13T09:15:00+06:00"
    },
    {
      "id":         1,
      "amount":     "400.00",
      "source":     "panel",
      "created_at": "2026-06-12T16:40:00+06:00"
    }
  ]
}

Returns a paginated list, newest first. source is api or panel depending on where the refund was issued from.

Polling vs webhooks

You can poll GET /api/v1/payments/{id} until the status is terminal, but we recommend webhooks for instant notification.