> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wegive.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Processing API reference

> Create donations and recurring plans from your server, with customer, profile, payment-method, and retry examples.

The Pay processing API is for your backend. Complete
[access and keys](/external/onboarding/wegive-pay/setup-requirements) and the
[Payframes integration](/external/onboarding/wegive-pay/payframes-api) first.
All identifiers below are examples; use IDs returned for your own organization.

## Authentication and conventions

Base URL: `https://api.wegive.com`. Send these headers on requests:

```http theme={null}
Authorization: Bearer sk_test_YOUR_SERVER_SIDE_KEY
Accept: application/json
Content-Type: application/json
```

Use your live secret key for live payments. The secret key determines the
organization and payment mode. Browser tokenization and backend processing must
use matching organization and mode credentials.

Transaction and recurring-plan creation additionally require an
`Idempotency-Key` header and a valid `fraudSessionId` in the body. Forward the
initiating browser's `User-Agent` on processing requests, retaining it for
retries. See the [backend guide](/external/onboarding/wegive-pay/payframes-api#implement-your-backend-route).

Amounts are integer minor units. `5000` represents 50.00 for a two-decimal
currency. Optional `currency` must match the organization's ISO currency code.
The API applies supported amount and account limits; handle validation responses
rather than assuming every positive amount is accepted.

Dates use `YYYY-MM-DD`; timestamps in responses use ISO 8601. Optional response
fields may be absent or `null`. Preserve identifiers returned by WeGive and
tolerate additional response fields.

## Endpoints

| Method | Path | Purpose |
| - | - | - |
| POST | `/api/pay/customers` | Find or create a customer by email |
| GET | `/api/pay/customers/{user}/profiles` | List that customer's profiles visible to your organization |
| POST | `/api/pay/customers/{user}/profiles` | Find or create an individual or company profile |
| POST | `/api/pay/customers/{user}/cards` | Attach a freshly tokenized card |
| POST | `/api/pay/customers/{user}/banks` | Attach an approved tokenized bank method |
| POST | `/api/pay/transactions` | Process a donation |
| POST | `/api/pay/recurring-plans` | Create a recurring plan, potentially taking its first payment |
| GET | `/api/pay/recurring-plans/{id}` | Read a recurring plan |
| GET | `/api/pay/customers/{user}/recurring-plans` | List active, non-cancelled plans for a customer |
| DELETE | `/api/pay/recurring-plans/{id}` | Cancel future payments on a plan |

The documented Pay API does not expose a transaction polling, refund, or
recurring-plan update endpoint. Reconcile payment records in WeGive or through
the reporting integration agreed for your account. Do not invent a
`GET /api/pay/transactions/{id}` URL.

## One-time donation with inline card details

This is the shortest server-side charge flow. Post the values from Payframes,
donor details, and billing information to `POST /api/pay/transactions`:

```http theme={null}
Idempotency-Key: donation-attempt-8c4a926f-2a1b-42d7-aaf7-994d87016e52
```

```json theme={null}
{
  "tokens": {
    "cardNumber": "TOKENIZED_CARD_NUMBER",
    "cardExpiration": "MMYY_FROM_PAYFRAMES",
    "cardCvv": "TOKENIZED_SECURITY_CODE"
  },
  "donor": {
    "email": "donor@example.org",
    "firstName": "Alex",
    "lastName": "Donor"
  },
  "cardholder": { "name": "Alex Donor", "zip": "94110" },
  "amount": 5000,
  "currency": "USD",
  "frequency": "once",
  "fraudSessionId": "FRAUD_SESSION_ID_FROM_SDK"
}
```

Supply all three `tokens` fields. `donor.email`, `cardholder.name`, and
`cardholder.zip` are required for this shape. `donor.phone` and
`cardholder.state` are optional. Pass the tokenization result unchanged; do not
send raw card numbers or security codes.

Example `200` response:

```json theme={null}
{
  "data": {
    "id": 12345,
    "status": "success",
    "amount": 5000,
    "donor": { "id": 114, "email": "donor@example.org" },
    "card": { "id": 5432, "brand": "visa", "last4": "4242" },
    "created_at": "2026-10-12T18:30:00Z"
  }
}
```

Check `data.status` as well as the HTTP status. `pending` or `processing` is not
confirmed success. Failed or other non-success transaction states must not be
displayed as completed donations. `donor` or `card` may be `null`; bank
transactions do not have a `card` object. Retain `data.id` for reconciliation.

## By-reference donation

Use this flow when your backend needs explicit customer, profile, and payment
method records, including when creating recurring plans.

### 1. Find or create the customer

`POST /api/pay/customers` accepts required `email` and optional `phone`:

```json theme={null}
{ "email": "donor@example.org", "phone": "+14155550123" }
```

Example `200` response:

```json theme={null}
{ "data": { "id": 123, "email": "donor@example.org" } }
```

The response contains only `id` and `email`, whether the customer is new or
already exists, including when your organization has a giving profile for them.
Read your organization's contact details through the profile endpoints below.
The optional `phone` initializes a new customer only; it does not update an
existing customer's WeGive account. Email lookup and profile creation are not
donor authentication or permission to use a person's saved payment methods.

### 2. Find or create the giving profile

You can first call `GET /api/pay/customers/123/profiles`, which returns a `data`
array. An empty array means no visible profiles were found.

For an individual, send `POST /api/pay/customers/123/profiles`:

```json theme={null}
{
  "type": "individual",
  "firstName": "Alex",
  "lastName": "Donor",
  "email": "donor@example.org"
}
```

For a company, use `type: "company"` and required `companyName` instead of the
individual's required first and last names. `email` and `phone` are optional.

The `200` response wraps a profile under `data`, with `id`, `type`, `name`,
`firstName`, `lastName`, `email`, `phone`, `organizationId`, `userId`, and
`createdAt`. Save its `id`. Creation can return an existing matching profile;
it is not an endpoint for overwriting an existing profile's details.

### 3. Attach the freshly tokenized payment method

For a card, send `POST /api/pay/customers/123/cards`:

```json theme={null}
{
  "tokens": {
    "cardNumber": "TOKENIZED_CARD_NUMBER",
    "cardExpiration": "MMYY_FROM_PAYFRAMES",
    "cardCvv": "TOKENIZED_SECURITY_CODE"
  },
  "cardholder": { "name": "Alex Donor", "zip": "94110" }
}
```

The `200` response contains `data.id`, `type: "card"`, `brand`, `last4`,
`expMonth`, `expYear`, ownership identifiers, and `createdAt`. Keep the returned
ID for this attempt. Do not create a new card record every time an uncertain
charge is retried.

For an approved bank-tokenization integration, send
`POST /api/pay/customers/123/banks`:

```json theme={null}
{
  "tokens": {
    "accountNumber": "TOKENIZED_ACCOUNT_NUMBER",
    "routingNumber": "TOKENIZED_ROUTING_NUMBER"
  },
  "bankholder": { "name": "Alex Donor", "zip": "94110" }
}
```

`bankholder.name` is required; `zip` is optional. The `200` response includes
`data.id`, `type: "bank"`, `bankName`, `last4`, ownership identifiers, and
`createdAt`. This is the approved bank-method API flow; the Payframes SDK itself
only mounts card fields. Obtain the correct tokenization integration from WeGive.

Create a fresh payment method for a new donor-authorized donation. Do not treat
these endpoints as permission to build an unrestricted saved-wallet charging
service. WeGive manages the payment method's use by a recurring plan.

### 4. Submit the donation

Send `POST /api/pay/transactions` with a stable idempotency key:

```json theme={null}
{
  "profile": { "id": 114 },
  "paymentMethod": { "type": "card", "id": 5432 },
  "amount": 5000,
  "frequency": "once",
  "fraudSessionId": "FRAUD_SESSION_ID_FROM_SDK"
}
```

Supply `paymentMethod.type` and `id` together. Use `bank` for a bank method.
The profile and method must be authorized for the same customer relationship
and organization. Choose either the inline or by-reference shape, not both.
The response uses the same transaction shape as the inline flow.

## Recurring plans

Create the customer, profile, and payment method first. Recurring-plan creation
uses **by-reference inputs** and requires its own idempotency key and valid fraud
session. Send `POST /api/pay/recurring-plans`:

```json theme={null}
{
  "profile": { "id": 114 },
  "paymentMethod": { "type": "card", "id": 5432 },
  "amount": 2500,
  "currency": "USD",
  "recurringFrequency": "monthly",
  "fraudSessionId": "FRAUD_SESSION_ID_FROM_SDK"
}
```

`recurringFrequency` accepts `daily`, `weekly`, `1_and_15`, `bimonthly`,
`monthly`, `quarterly`, `semiannual`, or `yearly`. Optional `startDate` and
`endDate` use `YYYY-MM-DD`; supply both when setting an end date.

**Omitting `startDate` starts today and can charge immediately.** To schedule
without charging today, pass a future start date agreed with the donor.
Do not call `/transactions` first to take the same initial payment.
Setting `frequency: "recurring"` on a transaction does not create a recurring
schedule; use this recurring-plan endpoint to create the plan.

A `201` response contains `data` for the plan and `initialTransaction` for a first
payment, or `null` when no payment was taken. An abbreviated example for a future
start is:

```json theme={null}
{
  "data": {
    "id": 789,
    "cancelled": false,
    "amount": 2500,
    "frequency": "monthly",
    "startDate": "2026-12-01",
    "endsAt": null,
    "profile": { "id": 114, "email": "donor@example.org" },
    "paymentMethod": { "type": "card", "id": 5432, "brand": "visa", "last4": "4242" }
  },
  "initialTransaction": null
}
```

Plan responses also include `organizationBrandId`, `pausedAt`, `pausedUntil`,
`coverFees`, `feeAmount`, `anonymous`, `isTaxDeductible`, attribution identifiers,
`createdAt`, and `updatedAt`. Note the response names `frequency`, `endsAt`, and
`isTaxDeductible`, which differ from request names.

Check both `data.cancelled` and `initialTransaction.status` before confirming
the plan to the donor. A first-payment failure can return a cancelled new plan.
An HTTP `201` alone does not prove an initial payment succeeded.

Read a plan with `GET /api/pay/recurring-plans/789`. List a customer's active,
non-cancelled plans with `GET /api/pay/customers/123/recurring-plans`; its response
is a `data` array. Cancel with `DELETE /api/pay/recurring-plans/789`:

```json theme={null}
{ "data": { "id": 789, "cancelled": true } }
```

Cancellation stops future scheduled donations. It is not a refund and does not
reverse a payment already in progress. Changing a schedule or payment method is
not exposed as a Pay API update operation; manage those changes through WeGive.

## Optional donation fields

These fields apply to transaction and recurring-plan creation unless noted:

| Field | Format and purpose |
| - | - |
| `organizationBrandId` | Numeric organization financial-brand ID |
| `designation`, `campaign`, `campaignFundraiser`, `fundraiser`, `checkout` | Objects with numeric `id`; use references authorized for your organization |
| `coverFees` | Boolean fee-coverage selection |
| `feeAmount` | Nonnegative integer minor units; use the fee calculation agreed for your integration |
| `anonymous` | Boolean anonymous-giving selection |
| `taxDeductible` | Boolean tax-deductibility setting |
| `trackingUrl` | Source URL, up to 2,048 characters; omit sensitive query parameters |
| `tribute` | Object with `name`, optional `email` and `message` |
| `supporterInputtedNotes` | Donor-provided note |
| `metadata` | Your integration's non-sensitive structured metadata |
| `campaignEventId` | Numeric event identifier; transactions only |
| `messageId`, `messageDonorId` | Message attribution strings; transactions only |

SDK pop-up options use names such as `campaignId`. Backend requests use nested
objects such as `"campaign": { "id": 123 }`; do not copy all pop-up options
directly into the API body. For custom-checkout UTM attribution, forward the
validated values from `pay.getAttribution()` as top-level `utm_*` fields on the
transaction request. Reporting availability follows your account's tracking settings.

## Idempotency and retries

An idempotency key identifies **one donor-authorized attempt**. Generate it on
your backend and save it before the first request. Use a nonempty unique value
of at most 255 characters. Transaction and recurring-plan creation require it.
Do not assume this protection applies to every other POST endpoint.

For a retry, keep all of these unchanged:

* The original secret-key identity.
* The HTTP method and endpoint path.
* The `Idempotency-Key` header.
* The **exact serialized request-body bytes**, including token and fraud-session values.

Store the serialized JSON once. Reformatting whitespace, changing key order,
adding metadata, regenerating tokens, or refreshing the fraud session can make a
retry a different body. Do not build the request again from a changed form.

When an attempt has a recorded response, an identical retry returns that response.
A reused key with a changed request returns `409`. A request still in progress or
awaiting reconciliation also returns `409`. Read the error to distinguish them.
Waiting does not make an uncertain attempt safe to execute under a new key.

If a connection times out, you receive a 5xx response, or you cannot establish
the payment outcome, retain the attempt and reconcile it. You can retry the exact
request under its original key to retrieve a recorded response; if it remains
unresolved, contact WeGive with the attempt and transaction identifiers. Never
issue a new key merely to get past a conflict.

A definitive validation rejection may require corrected inputs and a new
donor-authorized attempt after confirming no payment occurred. Replaying the
old key can return its original rejection. Do not repeatedly mint sessions or
new keys to work around a security block. Coordinate credential rotation with
unresolved attempts because deduplication does not carry across secret keys.

## Errors

Send `Accept: application/json` and inspect the response body. Validation errors
normally include a `message` and field-keyed `errors` object; other failures may
only provide a message. Do not rely on a single universal error-code schema.

| HTTP status | Action |
| - | - |
| `400` | Check required headers and request formatting, including the idempotency key |
| `401` | Check the secret key and whether it is active |
| `403` | Check account access, key mode, and browser verification context; contact WeGive for a continuing block |
| `404` | Check the ID and its availability to your organization |
| `409` | Resolve an idempotency mismatch or an in-progress/uncertain attempt; do not create another charge |
| `422` | Correct the reported input fields or unsupported donation configuration |
| `429` | Back off; honor `Retry-After` when present and retain the original attempt |
| `5xx` or network timeout | Treat an already submitted payment as uncertain until reconciled |

Keep your UI's payment state separate from HTTP success. Even a `200` or `201`
response must be checked for transaction status and, for a plan, its cancellation
and initial-payment state.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.