#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 fieldscaptured_at,failed_at,refunded_at,disputed_atpopulate 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/merchant — data.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 startpk_test_/pk_live_— and that resemblance has cost integrators an afternoon. Passing a PlutoPay publishable key toStripe()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 webhook — payment.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_pageis accepted as an alias forsizeon this endpoint and on/v1/customers, because earlier versions of this guide documented that name. Prefersize— 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