Charge a card
Record a donation shows the simplest cash
case. The realistic flow involves a credit card or ACH charge through
the tenant’s configured payment processor (Stripe or Authorize.Net). The
key thing to understand: the API never sees raw card data. Your
client-side code tokenizes the card with the processor’s JS SDK; the
token is what flows to POST /orders.
Prerequisites
Section titled “Prerequisites”- The tenant has a payment processor configured under Administration → Tenant Settings → Behaviors → Payment Processor (Stripe or Authorize.Net).
- The tenant has the
payment-processor:enabledfeature flag on. - Your client-side page has loaded the matching processor JS SDK.
Required scopes
Section titled “Required scopes”orders:createpayment-methods:manage(only if you setsavePaymentMethod = true)
1. Tokenize on the client (out of scope for the API docs)
Section titled “1. Tokenize on the client (out of scope for the API docs)”The browser side uses the processor’s own SDK. Two short pointers:
- Stripe — Stripe.js with Elements or PaymentElement. The
result is a
PaymentMethodid starting withpm_.... - Authorize.Net — Accept.js with
acceptUIHandler. The result is an opaquedataValueplus adataDescriptor.
The token is one-shot. Pass it straight into the order create call — don’t store it, log it, or relay it through your own server beyond what the immediate next request needs.
2. Create the order with the token
Section titled “2. Create the order with the token”PAYMENT_TOKEN="pm_1Q9w7B..." # whatever your client SDK producedFUND_ID="00000000-0000-0000-0000-000000000001"CONTACT_ID=1834IDEMPOTENCY_KEY=$(uuidgen)
curl -sS -X POST \ -H "X-Api-Key: $AURA_API_KEY" \ -H "Content-Type: application/json" \ -d "$(cat <<JSON{ "contactId": $CONTACT_ID, "items": [], "donations": [ { "fundId": "$FUND_ID", "amount": 100.00 } ], "billingAddress": { "line1": "1 Market St", "city": "San Francisco", "stateProvince": "CA", "postalCode": "94105", "country": "US" }, "saveBillingToContact": false, "saveShippingToContact": false, "payments": [ { "paymentMethod": "CreditCard", "amount": 100.00, "paymentToken": "$PAYMENT_TOKEN", "savePaymentMethod": true } ], "idempotencyKey": "$IDEMPOTENCY_KEY"}JSON)" \ "https://api.auradonors.com/api/tenants/$TENANT_ID/orders"The handler:
- Validates the request (contact exists, fund exists, totals balance, etc.).
- Calls the processor to charge the token. Stripe
PaymentIntentor Authorize.NetcreateTransactionRequest, depending on the tenant’s configuration. - On success — writes the
Order,OrderDonation, andOrderPaymentrows; attaches to a batch; returns201 Created. - On processor failure — returns
400with a typed code (see below). No order is created; no money moved.
Successful response (truncated):
{ "id": "9c4a7b2d-...", "orderStatus": "Completed", "totalAmount": 100.00, "payments": [ { "paymentMethod": "CreditCard", "paymentStatus": "Captured", "amount": 100.00, "transactionId": "ch_3Q9wB...", // processor's id "authorizationCode": "auth-abc123", "last4": "4242", "cardBrand": "Visa", "isExternallyCaptured": false, // we made the charge, not the customer's clerk "contactPaymentMethodId": "..." // present when savePaymentMethod=true } ]}3. Handle declines and challenges
Section titled “3. Handle declines and challenges”Card charges fail more often than any other call you’ll make. Branch on
the typed code in the errors map:
| Typed code | Meaning | What to do |
|---|---|---|
Payment.DeclinedByProcessor | Issuer declined. | Surface a “use a different card” message. |
Payment.RequiresAuthentication | 3DS / Strong Customer Authentication required. | Fall back to the processor’s authentication flow on the client; once completed, retry with the resulting token. |
Payment.InvalidPaymentMethod | The token was malformed, expired, or already used. | Tokenize again with a fresh card capture. |
Payment.ProcessorUnavailable | Stripe / Authorize.Net is rate-limiting or returning 5xx. | Retry with backoff, same idempotencyKey. |
Order.HandlingOverrideForbidden | The handling amount you supplied diverges from the tenant’s policy and your key lacks orders:override-shipping. | Drop the override and let the server compute. |
Other failures share the standard
error envelope — same fields, same traceId for
support escalation.
4. Saving the card for next time
Section titled “4. Saving the card for next time”Set savePaymentMethod = true and the response carries
contactPaymentMethodId. Use that id on the next order to charge the
same card without re-tokenizing:
"payments": [ { "paymentMethod": "CreditCard", "amount": 50.00, "contactPaymentMethodId": "the-saved-id-from-before", "savePaymentMethod": false }]The processor stores the actual card; Aura stores only the customer profile id and last-four / brand for display.
Always set idempotencyKey
Section titled “Always set idempotencyKey”Card flows are the canonical reason idempotencyKey exists. A network
timeout in the middle of a charge is exactly the case the contract
guards against:
- The first attempt charged but never returned
201to your client. - Your retry sends the same body and the same
idempotencyKey. - The handler finds the existing order via the key and returns it unchanged. The donor is charged once, not twice.
See Idempotency for the full retry pattern.
Recurring schedules
Section titled “Recurring schedules”To set up a recurring monthly gift alongside the one-time charge, add a
recurringDonations entry:
"recurringDonations": [ { "fundId": "$FUND_ID", "amount": 100.00, "frequency": "Monthly", "chargeFirstToday": true, "startDate": null, "endDate": null }]When chargeFirstToday = true, the first charge happens as part of the
order you’re creating right now. Subsequent charges fire from the
recurring-donation background service.
When chargeFirstToday = false and startDate is in the future, no
payment row is created today — the processor’s subscription
(billing-cycle anchor) captures the first payment on startDate. Use
isSetupOnly = true on the payment row in that case to vault the card
without an immediate charge.
What’s next
Section titled “What’s next”- Subscribe to
order_payment.created,order_payment.updated, andrefund.createdto keep your warehouse current. See Listen for events. - Ops scenarios (refunds, voids) are operator-only — covered briefly in Resources.