Skip to content

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.

  • The tenant has a payment processor configured under Administration → Tenant Settings → Behaviors → Payment Processor (Stripe or Authorize.Net).
  • The tenant has the payment-processor:enabled feature flag on.
  • Your client-side page has loaded the matching processor JS SDK.
  • orders:create
  • payment-methods:manage (only if you set savePaymentMethod = 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 PaymentMethod id starting with pm_....
  • Authorize.Net — Accept.js with acceptUIHandler. The result is an opaque dataValue plus a dataDescriptor.

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.

Terminal window
PAYMENT_TOKEN="pm_1Q9w7B..." # whatever your client SDK produced
FUND_ID="00000000-0000-0000-0000-000000000001"
CONTACT_ID=1834
IDEMPOTENCY_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:

  1. Validates the request (contact exists, fund exists, totals balance, etc.).
  2. Calls the processor to charge the token. Stripe PaymentIntent or Authorize.Net createTransactionRequest, depending on the tenant’s configuration.
  3. On success — writes the Order, OrderDonation, and OrderPayment rows; attaches to a batch; returns 201 Created.
  4. On processor failure — returns 400 with 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
}
]
}

Card charges fail more often than any other call you’ll make. Branch on the typed code in the errors map:

Typed codeMeaningWhat to do
Payment.DeclinedByProcessorIssuer declined.Surface a “use a different card” message.
Payment.RequiresAuthentication3DS / Strong Customer Authentication required.Fall back to the processor’s authentication flow on the client; once completed, retry with the resulting token.
Payment.InvalidPaymentMethodThe token was malformed, expired, or already used.Tokenize again with a fresh card capture.
Payment.ProcessorUnavailableStripe / Authorize.Net is rate-limiting or returning 5xx.Retry with backoff, same idempotencyKey.
Order.HandlingOverrideForbiddenThe 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.

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.

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 201 to 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.

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.

  • Subscribe to order_payment.created, order_payment.updated, and refund.created to keep your warehouse current. See Listen for events.
  • Ops scenarios (refunds, voids) are operator-only — covered briefly in Resources.