PlutoPay Docs
API Reference Dashboard ↗

#Card payments

Create a card payment with POST /v1/transactions (alias: POST /v1/payment-intents — identical behavior). All amounts are integers in cents. The platform fee is applied automatically to charges routed to your connected account and depends on the payment method — see Fees.

#Fees

The fee is set by the payment method, computed once when the payment is created, and stored on the transaction. The dashboard, reports, the API (platform_fee_cents on the transaction) and the payment.succeeded webhook all read that stored value, so what you are shown is what was charged.

Payment method PlutoPay fee Notes
Card, online (API, Elements, payment links, hosted checkout) 3% Wallets paid online (Apple Pay, Google Pay) are cards.
Card, in person (Terminal, tap-to-pay) 3% No fixed component.
ACH Direct Debit (standard, T+4) 1%, capped at $7.00 See the ACH guide.

The fee is computed once when the payment is created and stored with it, so a later rate change never restates history.

#Create a payment

Field Type Required Description
amount integer Amount in cents. Minimum 50.
currency string ISO 3-letter code. Defaults to your account currency.
payment_method string A processor payment-method id to charge immediately.
payment_method_type string One of card, wallet, bank_transfer, ach, terminal. Defaults to card.
confirm boolean Confirm immediately (requires payment_method). Default false.
capture_method string automatic (default) or manual (authorize now, capture later).
customer_id uuid An existing PlutoPay customer.
description string Appears on the transaction and receipts.
receipt_email string Email to send a receipt to.
return_url string Redirect target after off-session confirmation.
metadata object Arbitrary key/value pairs echoed back on the object and in webhooks.

Pass an Idempotency-Key header to make creation safely retryable.

curl https://plutopayus.com/api/v1/transactions \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_1001" \
  -d '{
    "amount": 4750,
    "currency": "usd",
    "payment_method_type": "card",
    "description": "Order #1001",
    "receipt_email": "customer@example.com",
    "metadata": { "order_id": "1001" }
  }'

#Response 201

{
  "data": {
    "id": "019c8044-2ab5-7396-b5af-216ef14dc582",
    "reference": "txn_8f3kQ2mZ0pLxWvA1bC9d",
    "type": "payment",
    "status": "pending",
    "amount": 4750,
    "amount_refunded": 0,
    "currency": "usd",
    "payment_method_type": "card",
    "description": "Order #1001",
    "receipt_email": "customer@example.com",
    "source": "api",
    "is_test": true,
    "metadata": { "order_id": "1001" },
    "created_at": "2026-07-05T14:12:03+00:00",
    "updated_at": "2026-07-05T14:12:03+00:00"
  },
  "client_secret": "pi_3T2..._secret_9Yx..."
}

The response also includes status_badge, and (for card payments) card_brand / card_last_four. Timestamp fields captured_at, failed_at, refunded_at, disputed_at populate as the transaction moves through those states.

#Confirming on the client

Use the client_secret on your own page to confirm the payment with Stripe.js’ Payment Element, so the card never touches your server. This is the Elements integration from Choosing an integration.

Stripe.js needs a Stripe publishable key. Get it from GET /v1/merchantdata.processor.publishable_key. It is PlutoPay’s platform key, and it is the correct one: payments are created on the PlutoPay platform account and settled to yours, so Stripe.js is platform-scoped. Do not pass a stripeAccount option, and do not use data.processor.connected_account_id here.

Your PlutoPay pk_… key is not a Stripe key. The two look alike — both start pk_test_ / pk_live_ — and that resemblance has cost integrators an afternoon. Passing a PlutoPay publishable key to Stripe() cannot work. See Authentication.

#The key for Stripe.js

curl https://plutopayus.com/api/v1/merchant \
  -H "Authorization: Bearer sk_test_your_key_here"
{
  "data": {
    "id": "019e0571-0d7d-7184-99d1-8db60fe7cb8d",
    "business_name": "Your Business",
    "processor": {
      "name": "stripe",
      "publishable_key": "pk_test_51PkCCO05Fd8NpHG6…",
      "connected_account_id": "acct_1TUcyD0Twm5slnRp"
    }
  }
}

The key follows the mode of the key you asked with: a sk_test_… request returns the test publishable key, a sk_live_… request the live one. The route requires a secret key, so fetch it from your server and hand it to your frontend — cache it rather than calling this on every page load.

// publishableKey came from GET /v1/merchant; clientSecret from POST /v1/transactions
const stripe = Stripe(publishableKey);          // no stripeAccount — see above
const elements = stripe.elements({ clientSecret });
elements.create('payment').mount('#payment-element');

// on submit:
const { error, paymentIntent } = await stripe.confirmPayment({
  elements,
  confirmParams: { return_url },
  redirect: 'if_required',
});

The final state arrives by webhookpayment.succeeded or payment.failed.

#Capture, cancel, retrieve

POST /v1/transactions/{id}/capture    # for capture_method: "manual"
POST /v1/transactions/{id}/cancel      # cancel an uncaptured payment
GET  /v1/transactions/{id}
GET  /v1/transactions?status=succeeded&page=1&size=20

{id} accepts either our UUID (data.id) or the reference (data.reference, txn_…). Both resolve to the same payment. An identifier that doesn't exist returns 404 with code: resource_missing — never a 500 — so you can distinguish "this payment is unknown" from "the API is unavailable" and retry only the second.

#Pagination

size (1–100, default 10) and page. The response carries totalCount, page, size and lastPage.

per_page is accepted as an alias for size on this endpoint and on /v1/customers, because earlier versions of this guide documented that name. Prefer size — it is the canonical parameter and the one the response echoes back.

#Refund

POST /v1/transactions/{transaction}/refunds
Field Type Description
amount integer Amount in cents to refund. Defaults to the full refundable balance.
reason string Optional reason.

Refunds on connected-account charges automatically reverse the transfer and the application fee, so the refund cost lands on the correct account. A full refund sets the transaction status to refunded; a partial refund sets partially_refunded.

Next: Hosted checkout & links · Webhooks