> ## 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.

# Event Types

> The Raise webhook event type catalog — the 15 documented event type integers, the resource-group pattern they follow, and the discovery path for the integer-to-label mapping.

The Raise webhook system supports 15 distinct event types. The OpenAPI spec exposes them as an integer enum (`[10, 11, 12, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 52]`) without labels. This page covers what the integer pattern suggests, the discovery path for confirming the labels, and the practical patterns for subscribing to and handling events.

## The 15 event type integers

The full enum from the spec:

```text theme={null}
10, 11, 12,
20, 21, 22,
30, 31, 32,
40, 41, 42,
50, 51, 52
```

The grouping is striking — five tens (10s, 20s, 30s, 40s, 50s) with three consecutive integers in each group. This pattern almost certainly corresponds to **5 resource families × 3 event actions per family**.

<Warning>
  **⚠️ Spec gap:** The Raise OpenAPI spec doesn't label the EventType enum integers. The pattern suggests resource × action semantics (e.g., 10 = gift created, 11 = gift updated, 12 = gift deleted), but the canonical mapping is not documented.

  The discovery path below identifies labels by inspecting webhook delivery logs — the `eventTypeDisplay` field on `WebhookLogListModel` provides the human-readable label for each integer. Subscribe to a broad set of events on a test webhook, generate sample activity, and read the resulting log entries to learn the labels.
</Warning>

## The likely resource groups

Based on the integer pattern and the Raise data model, the 5 groups likely correspond to:

| Integer group | Likely resource family |
| ------------- | ---------------------- |
| 10, 11, 12    | Gifts                  |
| 20, 21, 22    | RecurringGifts         |
| 30, 31, 32    | Donors                 |
| 40, 41, 42    | Campaigns              |
| 50, 51, 52    | Pages / Donation Forms |

And the three per-family integers likely correspond to lifecycle actions — typically *created, updated, deleted* or similar. So `10` would be "gift created", `11` "gift updated", `12` "gift deleted" — with the same pattern applying to each family.

**Treat this mapping as a working hypothesis, not as the spec's contract.** The discovery path below confirms it for any specific event integer your integration cares about.

## Discovering the mapping

The `WebhookLogListModel` schema (returned by `GET /api/Webhook/log/list` and the per-webhook log endpoints) carries both `eventType` (integer) and `eventTypeDisplay` (human-readable label). Inspecting log entries reveals the mapping for each integer that has fired in the customer's environment.

### A discovery script

Run this once against a customer's webhook subscription to learn the integer-to-label mapping:

```javascript JavaScript theme={null}
async function discoverEventTypeLabels(webhookId) {
  const params = new URLSearchParams({ Take: '500', SortBy: 'createddatetime' });
  const response = await fetch(
    `https://prod-api.raisedonors.com/api/Webhook/${webhookId}/log/list?${params}`,
    { headers: { Authorization: `Bearer ${process.env.RAISE_API_TOKEN}` } }
  );
  const logs = await response.json();

  // Build the integer-to-label map from observed events
  const mapping = new Map();
  for (const log of logs.items) {
    if (!mapping.has(log.eventType)) {
      mapping.set(log.eventType, log.eventTypeDisplay);
    }
  }

  return Object.fromEntries(mapping);
}

const mapping = await discoverEventTypeLabels(123);
console.log('Event type labels:', mapping);
// Example output:
// {
//   10: 'Gift Created',
//   11: 'Gift Updated',
//   ...
// }
```

The script returns whatever event types have been delivered through the subscription. For a complete mapping of all 15 event types, the customer's account needs to have generated activity across all 5 resource families with each action type — which may take time to accumulate naturally.

### Forcing event generation in a test environment

For development environments where you need to discover the full mapping faster, generate test activity that triggers each event type:

| Event integer                     | How to generate                                                                   |
| --------------------------------- | --------------------------------------------------------------------------------- |
| 10, 11, 12 (likely Gift)          | Submit a donation via `POST /api/Raise/give`; update or refund the resulting gift |
| 20, 21, 22 (likely RecurringGift) | Submit a recurring donation; update the schedule; cancel it                       |
| 30, 31, 32 (likely Donor)         | Donations create donors; PATCH a donor's email; archive a donor                   |
| 40, 41, 42 (likely Campaign)      | Create a campaign via `POST /api/Campaign`; update it; delete it                  |
| 50, 51, 52 (likely Page)          | Page events likely fire from form publish or page-generation actions              |

Subscribe a test webhook to all 15 integers, trigger the activity, and inspect the resulting logs to learn the labels.

***

## Subscribing to specific events

Once you know which integers correspond to the events your integration cares about, subscribe via the `eventTypesList` field on the WebhookRequest:

```javascript JavaScript theme={null}
async function subscribeToGiftEvents() {
  return fetch(
    'https://prod-api.raisedonors.com/api/Webhook',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.RAISE_API_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        name: 'Gift event subscription',
        notificationUrl: 'https://partner.example.com/raise-webhooks',
        eventTypesList: [10, 11, 12], // Subscribe to all gift events (per the working hypothesis)
        format: 1,
        status: 1,
        securityToken: process.env.WEBHOOK_SECRET,
      }),
    }
  ).then((r) => r.json());
}
```

For broad subscriptions that capture all events from a customer:

```javascript JavaScript theme={null}
const ALL_EVENT_TYPES = [10, 11, 12, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 52];

async function subscribeToEverything() {
  return fetch(
    'https://prod-api.raisedonors.com/api/Webhook',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.RAISE_API_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        name: 'All Raise events',
        notificationUrl: 'https://partner.example.com/raise-webhooks/all',
        eventTypesList: ALL_EVENT_TYPES,
        format: 1,
        status: 1,
        securityToken: process.env.WEBHOOK_SECRET,
      }),
    }
  ).then((r) => r.json());
}
```

For most integrations, subscribing to a focused subset is better than subscribing to everything — fewer events to filter, less noise in logs, and clearer integration semantics.

***

## What event payloads contain

The exact payload shape varies by event type. The Raise OpenAPI spec doesn't document each event's payload schema explicitly — the schemas come from inspecting actual deliveries via the log endpoints.

In general, partner integrations should expect:

| Element                 | Description                                                                                                                        |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Event type identifier   | The integer matching the EventType enum                                                                                            |
| The triggering resource | The Gift, Donor, RecurringGift, etc. that the event is about, typically as a complete record matching the relevant `*Model` schema |
| Timing metadata         | When the event occurred                                                                                                            |

### Inspecting a real payload

The fastest way to learn an event's exact shape:

```bash cURL theme={null}
# 1. Find a recent delivery log for the event type you're interested in
curl "https://prod-api.raisedonors.com/api/Webhook/123/log/list?Take=10" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

# 2. Fetch the specific log's payload
curl https://prod-api.raisedonors.com/api/Webhook/123/log/456 \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

The single-log response includes `payLoad` — the exact body delivered to the partner endpoint. Inspect this to understand the JSON structure for the event types your integration handles.

***

## Routing events on the partner side

A common pattern: a single webhook endpoint receives all event types and routes them to per-type handlers based on the event type integer.

```javascript JavaScript theme={null}
const handlers = {
  10: handleGiftCreated,
  11: handleGiftUpdated,
  12: handleGiftDeleted,
  20: handleRecurringGiftCreated,
  21: handleRecurringGiftUpdated,
  22: handleRecurringGiftCancelled,
  30: handleDonorCreated,
  31: handleDonorUpdated,
  // ...
};

app.post('/raise-webhooks', async (req, res) => {
  // 1. Verify signature before parsing — see Signature Verification
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid signature');
  }

  // 2. Acknowledge quickly
  res.status(200).send('OK');

  // 3. Process asynchronously
  await eventQueue.publish({
    eventType: req.body.eventType,
    payload: req.body,
    receivedAt: new Date(),
  });
});

// Worker that drains the queue
async function processEvent(event) {
  const handler = handlers[event.eventType];
  if (!handler) {
    console.warn(`No handler for event type ${event.eventType}`);
    return;
  }

  await handler(event.payload);
}
```

Three patterns this gets right:

* **Verify before parsing.** Don't trust the body's `eventType` field until the signature has been verified.
* **Acknowledge quickly.** Return `200 OK` before processing — see [Webhooks Overview: What the partner endpoint should do](/raise/webhooks/overview#what-the-partner-endpoint-should-do).
* **Tolerate unknown event types.** If Raise adds new event types in the future or the customer subscribes to additional types after deployment, your handler should log and skip unknown types rather than throw.

***

## Per-event handling patterns

The specific handlers depend on what your integration does. A few common patterns:

### Gift created (likely event 10)

The most common event partners subscribe to — a new donation completed. Typical handlers:

```javascript JavaScript theme={null}
async function handleGiftCreated(payload) {
  const gift = payload;

  // 1. Send a thank-you email (if your integration handles donor stewardship)
  await emailService.sendThankYou(gift.donor.email, {
    amount: gift.formattedAmount,
    project: gift.projects[0]?.projectName,
  });

  // 2. Sync to your external system (if your integration is a data pipeline)
  await externalSystem.recordDonation({
    raiseGiftId: gift.id,
    amount: gift.amount,
    date: gift.date,
    donorEmail: gift.donor.email,
  });

  // 3. Alert internal team for major gifts (if applicable)
  if (gift.amount >= 1000) {
    await slackNotifier.alert(`Major gift: ${gift.formattedAmount} from ${gift.donor.name}`);
  }
}
```

### Recurring gift created or cancelled

Useful for partner integrations that maintain donor stewardship state:

```javascript JavaScript theme={null}
async function handleRecurringGiftCreated(payload) {
  const schedule = payload;
  await donorJourney.recordEvent({
    donorId: schedule.donor.id,
    event: 'started_recurring',
    amount: schedule.amount,
    frequency: schedule.formattedFrequency,
  });
}

async function handleRecurringGiftCancelled(payload) {
  const schedule = payload;
  await donorJourney.recordEvent({
    donorId: schedule.donor.id,
    event: 'cancelled_recurring',
    cancelledAt: new Date(),
  });
  // Trigger retention workflow
  await retentionService.scheduleOutreach(schedule.donor.id);
}
```

### Donor updated

Most useful for partner integrations doing two-way sync with an external CRM:

```javascript JavaScript theme={null}
async function handleDonorUpdated(payload) {
  const donor = payload;

  // Sync changes to your external system
  await externalCrm.updateContact({
    raiseId: donor.id,
    email: donor.email,
    firstName: donor.firstName,
    lastName: donor.lastName,
  });
}
```

***

## Distinguishing recurring vs. one-time gift events

A common integration question: when a Gift event fires (event 10, likely "Gift Created"), is the gift one-time or one of a recurring schedule's payments?

The answer is in the Gift payload itself: check the `recurringGiftId` field.

```javascript JavaScript theme={null}
async function handleGiftCreated(payload) {
  const gift = payload;

  if (gift.recurringGiftId) {
    // This gift is a payment in a recurring schedule
    await handleRecurringPayment(gift);
  } else {
    // This is a one-time donation
    await handleOneTimeDonation(gift);
  }
}
```

This pattern matters for thank-you flows — donors signing up for recurring giving typically get a "thanks for signing up for monthly support!" email, while donors making subsequent recurring payments get a lighter-touch acknowledgment. Distinguishing the two cases at the integration level produces a better donor experience.

***

## Handling event ordering

Webhook events for the same resource are typically delivered in order — a `gift created` event arrives before a `gift updated` event for the same gift. But ordering across resources isn't guaranteed, and rare delivery hiccups can cause out-of-order arrival.

Two patterns help:

### Tolerate out-of-order events with state checks

For workflows that depend on event order:

```javascript JavaScript theme={null}
async function handleGiftUpdated(payload) {
  const gift = payload;

  // Check if we've already seen this gift
  const existing = await db.findByRaiseGiftId(gift.id);

  if (!existing) {
    // We're seeing an update before the create — treat as a create
    await handleGiftCreated(gift);
    return;
  }

  // Normal update flow
  await db.update(existing.id, gift);
}
```

This pattern handles out-of-order updates by upgrading the update to a create if needed.

### Use `modifiedDate` for tie-breaking

When an update event arrives for a record you already have, compare timestamps before applying:

```javascript JavaScript theme={null}
async function handleGiftUpdated(payload) {
  const gift = payload;
  const existing = await db.findByRaiseGiftId(gift.id);

  if (existing && existing.modifiedDate >= gift.modifiedDate) {
    // We already have a newer version of this record — ignore the older update
    return;
  }

  await db.update(existing.id, gift);
}
```

This produces last-writer-wins semantics keyed on `modifiedDate` rather than on arrival order.

***

## A complete subscription strategy

Pulling the patterns together: for a typical partner integration that needs to react to donation activity, the recommended subscription strategy:

| Event                               | Subscribe?                       | Why                                                   |
| ----------------------------------- | -------------------------------- | ----------------------------------------------------- |
| Gift created (likely 10)            | **Yes**                          | Primary signal — new donations                        |
| Gift updated (likely 11)            | Optional                         | Useful for integrations syncing gift state            |
| Gift deleted (likely 12)            | Optional                         | Useful for integrations that need to handle deletions |
| RecurringGift created (likely 20)   | Yes for stewardship integrations | Signals donor commitment                              |
| RecurringGift updated (likely 21)   | Optional                         | Catches `hasPaymentFailed` changes if no polling      |
| RecurringGift cancelled (likely 22) | Yes for stewardship              | Triggers retention workflows                          |
| Donor events (30, 31, 32)           | Yes for sync integrations        | Two-way sync with external CRM                        |
| Campaign events (40, 41, 42)        | Rare                             | Most integrations don't react to Campaign changes     |
| Page events (50, 51, 52)            | Rare                             | Most integrations focus on Donor/Gift activity        |

A typical partner subscription might cover `[10, 11, 20, 21, 22, 30, 31]` — the events that drive most donor-and-gift workflows without subscribing to everything.

***

## Where to go next

<CardGroup cols={2}>
  <Card title="Webhooks Overview" icon="webhook" href="/raise/webhooks/overview">
    The full subscription management surface — create, update, delete, inspect logs.
  </Card>

  <Card title="Signature Verification" icon="shield-check" href="/raise/webhooks/signature-verification">
    Validate that incoming requests actually came from Raise.
  </Card>

  <Card title="Idempotency and Safe Reprocessing" icon="check-double" href="/raise/webhooks/idempotency-and-safe-reprocessing">
    Handle duplicate deliveries without side effects.
  </Card>

  <Card title="Retry Behavior" icon="arrows-rotate" href="/raise/webhooks/retry-behavior">
    What happens when the partner endpoint doesn't respond successfully.
  </Card>
</CardGroup>
