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

# Process a Donation

> The end-to-end workflow for submitting a donation through POST /api/Raise/give — the unique gift-creation path in Raise, from building the request to handling the response and downstream events.

This is the canonical workflow page for the most distinctive operation in the Raise API: `POST /api/Raise/give`. It's the only endpoint that creates Gifts. There is no `POST /api/Gift` — donations enter Raise exclusively through this path.

This page walks through the full flow: building the request, what happens server-side, handling the response, and what events fire downstream. For the resource-level reference, see [Gifts](/raise/concepts/gifts) and [Donors](/raise/concepts/donors).

## When to use this workflow

Use `POST /api/Raise/give` when:

* A donor completes a donation form on a Raise-hosted or partner-hosted page.
* A partner integration captures a donation through its own UI and submits to Raise for payment processing.
* An integration replays a stored donation (e.g., re-attempting a previously-failed submission) — though the response will produce a new Gift, not modify an existing one.

Do **not** use this workflow for:

* Importing historical gifts that have already been processed elsewhere. There's no API for that — coordinate with the customer's admin team for bulk imports.
* Recording a manual gift entered by staff. That happens through the Raise admin UI.
* Updating an existing Gift. There is no Gift PUT — see [Gifts](/raise/concepts/gifts).

## What `POST /api/Raise/give` actually does

The endpoint runs five operations as a single transaction:

```mermaid theme={null}
sequenceDiagram
  participant Client as Partner integration
  participant Raise as POST /api/Raise/give
  participant Gateway as Payment gateway
  participant Webhook as Webhook subscribers
  participant Sync as Platform sync (to CRM+)

  Client->>Raise: Donation request (amount, payment method, donor)
  Raise->>Gateway: Charge tokenized payment method
  Gateway->>Raise: Gateway response (auth, transaction ID)
  Raise->>Raise: Match or create Donor record
  Raise->>Raise: Create Gift record
  Raise->>Client: Response (Gift details)
  Raise->>Webhook: Fire giftCreate event
  Note over Raise,Sync: Asynchronous — see "How Raise Data Flows to CRM+"
  Raise->>Sync: Queue for downstream sync
```

The five operations:

1. **Charge the payment method** through the configured gateway using the tokenized `paymentMethodId`.
2. **Match or create the Donor** from the embedded donor block (typically by email).
3. **Create the Gift record** with the gateway response.
4. **Fire the `giftCreate` webhook** to subscribers.
5. **Queue for sync** to downstream products like CRM+.

The first three happen synchronously and contribute to the response. The webhook and sync happen as a side effect.

## Building the request

### Required fields

Only three fields are required:

| Field               | Type   | Description                   |
| ------------------- | ------ | ----------------------------- |
| `amount`            | number | The donation amount.          |
| `paymentMethodId`   | string | The tokenized payment method. |
| `paymentMethodType` | enum   | The payment method type.      |

Everything else is optional and either has a sensible default or is supplemental context for downstream reporting.

### A minimal request

```bash cURL theme={null}
curl -X POST https://prod-api.raisedonors.com/api/Raise/give \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 50.00,
    "paymentMethodId": "tok_abc123",
    "paymentMethodType": "CreditCard"
  }'
```

This will succeed if the token is valid and the amount is positive, but it produces an anonymous gift with no donor information and the default Project allocation from the form context. For real donations, you almost always include the embedded donor block.

### A realistic request

```bash cURL theme={null}
curl -X POST https://prod-api.raisedonors.com/api/Raise/give \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 50.00,
    "currency": "USD",
    "paymentMethodId": "tok_abc123",
    "paymentMethodType": "CreditCard",
    "donor": {
      "firstName": "Bruce",
      "lastName": "Wayne",
      "email": "bruce@wayne.example",
      "phone": "(555) 555-0100"
    },
    "projects": [
      { "projectId": 12, "amount": 50.00 }
    ],
    "isAnonymous": false,
    "comments": "In support of the Gotham Outreach Program"
  }'
```

### The full request body

The `DonatePaymentRequest` body has many optional fields beyond the basics. Group them by purpose:

| Group                     | Fields                                                                                                   |
| ------------------------- | -------------------------------------------------------------------------------------------------------- |
| **Required**              | `amount`, `paymentMethodId`, `paymentMethodType`                                                         |
| **Currency**              | `currency`, `exchangeRate`                                                                               |
| **Donor**                 | `donor` (embedded `DonorRequest` block)                                                                  |
| **Designation**           | `projects[]`, `projectOverrideCode`, `premiumId`                                                         |
| **Recurring setup**       | `isRecurring`, `frequency`, `startDate`, `startDateTimeUtc`                                              |
| **Attribution**           | `segment`, `segmentOverrideCode`, `motivationCodeId`, `motivationCodeGroupId`, `googleGclid`, UTM fields |
| **Fee handling**          | `donorPaidCosts`, `coverAdminFee`, `adminFee`, `adminFeeProjectId`                                       |
| **Tribute**               | `tribute`                                                                                                |
| **Gateway**               | `gatewayId`, `creditCardType`, `paypalPaymentSource`, `nonce`                                            |
| **Anonymous / test mode** | `isAnonymous`, `isTestMode`                                                                              |
| **External linkage**      | `crmKey` (seed the CRM+ Contact ID on creation)                                                          |
| **Form-flow tracking**    | `publicId`, `pageRequestId`, `paymentId`                                                                 |
| **Browser context**       | `visitorId`, `sessionId`, `clientId`, `timeZone`                                                         |
| **Employer match**        | `doubleTheDonationCompanyId`, `doubleTheDonationEnteredText`                                             |
| **UK Gift Aid**           | `giftAidRequested`                                                                                       |
| **Comments**              | `comments`                                                                                               |

For the field-by-field reference, see [Gifts: The full DonatePaymentRequest body](/raise/concepts/gifts#the-request-body).

***

## Step-by-step walkthrough

### Step 1: obtain a payment method token

Raise uses tokenization to keep partner integrations out of PCI scope. Your integration never sees a raw card number — instead, the donor's payment information is exchanged for an opaque `paymentMethodId` token that you submit to `POST /api/Raise/give`.

For **development and testing**, use the dedicated test-payment-method generator:

```bash cURL theme={null}
curl -X POST https://prod-api.raisedonors.com/api/Raise/generate-test-payment-method \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

The response contains a `paymentMethodId` you can submit to `/api/Raise/give` in test mode (`isTestMode: true`) without touching real card data.

For **production**, the customer's payment gateway provides tokenization through its hosted fields, drop-in UI, or client-side SDK. The donor's card data is captured by the gateway's tools, tokenized client-side, and the resulting `paymentMethodId` flows through your integration to `POST /api/Raise/give`.

<Warning>
  Never accept raw card numbers in your integration's API or store them server-side. Doing so puts your integration in PCI DSS scope. Use the gateway's tokenization tooling for the donor-facing capture, and submit only the resulting token to Raise.
</Warning>

### Step 2: prepare the donor block

The `donor` field on the request is an embedded block that Raise uses for matching or creating the Donor record. Include the fields you have:

```json theme={null}
{
  "donor": {
    "firstName": "Bruce",
    "lastName": "Wayne",
    "email": "bruce@wayne.example",
    "phone": "(555) 555-0100",
    "billingAddress": {
      "address1": "1007 Mountain Drive",
      "city": "Gotham",
      "state": "NJ",
      "postalCode": "10001",
      "country": "US"
    }
  }
}
```

**Email is the most important field for donor matching.** Raise's matching algorithm typically uses email as the primary key — if the email matches an existing Donor, the gift is attached to that Donor rather than creating a new one. If the email doesn't match, a new Donor is created with the embedded block's data.

If you already have a Raise donor ID (e.g., from a previous gift), you can include it explicitly to skip the matching step — see [Create or Find a Donor](/raise/workflows/create-or-find-a-donor).

### Step 3: prepare the project allocation

The `projects[]` array specifies how the donation's `amount` should be designated across one or more Projects. Each entry has a Project identifier and an amount:

```json theme={null}
{
  "projects": [
    { "projectId": 12, "amount": 50.00 }
  ]
}
```

For a split gift:

```json theme={null}
{
  "projects": [
    { "projectId": 12, "amount": 30.00 },
    { "projectId": 47, "amount": 20.00 }
  ]
}
```

The sum of all entries must equal the gift's `amount`. If you omit `projects[]` entirely, the gift designates to the default Project configured on the Form (or the Campaign, depending on configuration).

### Step 4: submit the request

```javascript JavaScript theme={null}
async function processDonation({ amount, paymentMethodId, paymentMethodType, donor, projects, isTestMode }) {
  const response = await fetch(
    'https://prod-api.raisedonors.com/api/Raise/give',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.RAISE_API_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        amount,
        paymentMethodId,
        paymentMethodType,
        currency: 'USD',
        donor,
        projects,
        isTestMode: isTestMode ?? false,
      }),
    }
  );

  if (!response.ok) {
    const problem = await response.json();
    throw new DonationError(problem.detail || problem.title, response.status, problem);
  }

  return response.json();
}
```

### Step 5: handle the response

A successful donation returns `200 OK` with the created Gift record. The most important fields:

| Field                   | Why it matters                                                                                                                          |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                    | The Gift's Raise primary key. Store this for reconciliation and audit.                                                                  |
| `donorId`               | The Donor that the gift attached to (existing or newly created).                                                                        |
| `status` / `statusText` | The Gift's processing state — see [Statuses and Lifecycle States](/raise/concepts/statuses-and-lifecycle-states#gift-lifecycle-states). |
| `transactionId`         | The payment gateway's transaction reference.                                                                                            |
| `authorizationNumber`   | The gateway authorization.                                                                                                              |
| `canRefund`             | Whether the Gift can be refunded through `POST /api/Gift/{id}/refund`.                                                                  |
| `recurringGiftId`       | If `isRecurring: true` was set in the request, the ID of the newly-created RecurringGift schedule.                                      |

```javascript JavaScript theme={null}
const gift = await processDonation({ /* ... */ });

console.log('Donation processed:', {
  giftId: gift.id,
  donorId: gift.donorId,
  statusText: gift.statusText,
  amount: gift.formattedAmount,
  transactionId: gift.transactionId,
});
```

***

## Handling errors

The most common error categories and how to respond:

### 400: validation failure

The request body is syntactically valid but failed validation — missing required field, invalid combination, etc. The response includes the `errors` map identifying which fields failed:

```json theme={null}
{
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "amount": ["The amount field must be greater than zero."],
    "paymentMethodType": ["The paymentMethodType field is required."]
  }
}
```

Don't retry the same request — fix the body and re-submit. Surface the field-level errors to your integration's UI where applicable.

### 400: payment processing failure

A `400` can also indicate that the payment gateway rejected the charge — card declined, insufficient funds, expired card. The `detail` or `errors` map will identify the specific reason. The Gift record was not created in this case.

Don't retry the same payment method — the result will be the same. The right path is to surface the failure to the donor and let them try a different method (or contact their card issuer).

### 401: unauthorized

The token is invalid, expired, or revoked. See [Authentication: errors](/raise/authentication#authentication-errors). Don't retry; fix the credential.

### 429: rate limited

Too many requests in too short a time. Honor `Retry-After` and back off — see [Rate Limits](/raise/rate-limits). Retries should not multiply the customer's charge — see "Idempotency considerations" below.

### 500 / 502 / 503: server errors

Transient. Retry with exponential backoff. **But** see "Idempotency considerations" below — a request that the server received but couldn't acknowledge may have created a Gift even though your code raised an error.

***

## Idempotency considerations

`POST /api/Raise/give` charges a payment method. A naive retry on a network error or transient failure can result in **double charges** if the original request actually succeeded but the response didn't reach you.

The Raise spec does not currently document an idempotency-key header (like Stripe's `Idempotency-Key`). Until one is published, partner integrations should defend against duplicate submissions client-side:

| Defense                          | Description                                                                                                                                                                   |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Pre-submission deduplication** | Generate a unique key per intended donation (e.g., a UUID stored before submission). On retry, check whether your local record already shows the donation as submitted.       |
| **Webhook reconciliation**       | Subscribe to `giftCreate` webhooks. On a retry where the original may have succeeded, check whether a Gift with your tracking identifier already exists before re-submitting. |
| **Limited retries**              | Cap retries strictly — 2 attempts at most for `POST /api/Raise/give`. Persistent failures should surface for human review rather than continue retrying.                      |
| **Test mode for validation**     | When uncertain whether to retry, switch to `isTestMode: true` for a validation submission to confirm the request shape works before retrying production.                      |

<Warning>
  **Never retry `POST /api/Raise/give` automatically on a network error without one of the defenses above.** A naive retry produces double charges. The cost of failing to retry is one missed donation; the cost of duplicating a charge is a customer service incident.

  **⚠️ Human input required:** Confirm with the platform team whether `POST /api/Raise/give` supports an idempotency-key header. If so, document it here; if not, this defensive guidance is the recommended approach.
</Warning>

***

## Webhook events

A successful `POST /api/Raise/give` triggers a `giftCreate` event to subscribers. Partner integrations that need to react to donations beyond the immediate response should subscribe rather than poll:

```javascript JavaScript theme={null}
// Webhook handler example
async function handleGiftCreate(event) {
  const gift = event.payload;
  await emailService.sendThankYou(gift.donor.email, {
    amount: gift.formattedAmount,
    project: gift.projects[0]?.projectName,
  });
}
```

If the donation includes `isRecurring: true`, a corresponding event for the RecurringGift schedule also fires. See [Event Types](/raise/webhooks/event-types) for the full event list.

***

## Test-mode flow for development

For end-to-end testing without real charges:

```javascript JavaScript theme={null}
// 1. Generate a test payment method
const tokenResponse = await fetch(
  'https://prod-api.raisedonors.com/api/Raise/generate-test-payment-method',
  {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.RAISE_API_TOKEN}` },
  }
);
const testToken = await tokenResponse.json();

// 2. Submit a test donation
const giftResponse = await fetch(
  'https://prod-api.raisedonors.com/api/Raise/give',
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.RAISE_API_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      amount: 10.00,
      paymentMethodId: testToken.paymentMethodId,
      paymentMethodType: 'CreditCard',
      isTestMode: true,
      donor: {
        firstName: 'Test',
        lastName: 'Donor',
        email: 'test@example.com',
      },
    }),
  }
);
const gift = await giftResponse.json();
console.log('Test donation processed:', gift);
```

The resulting Gift carries `isTestMode: true` and won't appear in production-filtered reports. See [Base URLs and Environments: Use the test payment method generator](/raise/base-urls#use-the-test-payment-method-generator).

***

## Where to go next

<CardGroup cols={2}>
  <Card title="Create or Find a Donor" icon="user-plus" href="/raise/workflows/create-or-find-a-donor">
    The workflow for ensuring a donor exists before submitting a donation — useful when matching needs to happen explicitly before the donation step.
  </Card>

  <Card title="Configure a Recurring Gift" icon="arrows-rotate" href="/raise/workflows/configure-a-recurring-gift">
    Use the same `POST /api/Raise/give` path to set up a recurring schedule.
  </Card>

  <Card title="Handle Failed Payments" icon="triangle-exclamation" href="/raise/workflows/handle-failed-payments">
    The full failure-handling workflow including recurring payment failures.
  </Card>

  <Card title="Webhooks Overview" icon="webhook" href="/raise/webhooks/overview">
    React to donations in real time through `giftCreate` events.
  </Card>
</CardGroup>
