Developers
Guide

Webhooks

Webhooks let your software react the moment something changes in Argenta. You register an endpoint (a URL on your server), choose which events it should receive, and Argenta sends that URL a signed HTTPS request the instant one of those events happens. One endpoint can subscribe to as many events as you like, or to all of them.

Set up an endpoint
Every queued delivery carries its own copy of the target URL and the signing secret, taken at the moment the event was queued. So rotating a secret only covers events queued after the rotation, and anything already sitting in the queue is still signed with the old one. Keep your handler accepting both for a short while after you rotate.

In Argenta, go to Subscription Settings → Webhooks → Add Endpoint and provide:

  • Endpoint URL — an https:// address on your server that will receive the events.
  • Events — pick the specific events you want, or turn on Receive all events (this includes any new event types Argenta adds later).
  • Signing secret — generated for you. Copy it and store it securely; you use it to verify that each request really came from Argenta. You can rotate it at any time.

You can pause an endpoint (Active off) to stop delivery without deleting it.

Events

Each event has a type in resource.action form. Today's catalog:

Constituents

EventWhen it fires
constituent.createdA new constituent is added.
constituent.updatedAn existing constituent's details change.
constituent.deletedA constituent is deleted.

Volunteers

EventWhen it fires
volunteer.createdA constituent is designated as a volunteer.
volunteer.updatedA volunteer's details change.
volunteer.deletedA volunteer is removed (the constituent record remains).

Volunteer tasks

EventWhen it fires
volunteer_task.createdA new volunteer task is created.
volunteer_task.updatedA volunteer task's details change.
volunteer_task.deletedA volunteer task is deleted.
volunteer_task.signed_upA volunteer is assigned to (signs up for) a task. A sign-up also fires volunteer_task.updated, so subscribe to whichever fits your integration.
volunteer_task.canceledA volunteer cancels a task they signed up for (through the cancel form or volunteer portal).
volunteer_task.unassignedAn Argenta staff user unassigns the volunteer from a task.

Donations

EventWhen it fires
donation.createdA new money donation is recorded, from any source: online form, Stripe, PayPal, import, or hand-entered.
donation.updatedAn existing donation's details change.
donation.deletedA donation is deleted.
donation.refundedA refund is issued against a donation. The payload carries the refund amount and the refunded donation's id.
donation.receiptedA receipt is issued for a gift (the receipt flag flips on).
donation.thankedA thank-you is sent for a gift (the thank-you flag flips on).
donation.recurring_startedA donor sets up a recurring (e.g. monthly) gift.
donation.recurring_canceledA recurring gift schedule is canceled.

More event types (events, memberships, and others) will be added over time. If your endpoint is set to Receive all events, it automatically starts receiving new types as they ship — no change needed on your side.

The request we send

Every event is an HTTP POST to your endpoint URL with a JSON body and these headers:

HeaderMeaning
X-Argenta-EventThe event type, e.g. constituent.created.
X-Argenta-DeliveryA unique id for this delivery attempt.
X-Argenta-SignatureThe signature to verify the request (see below).
Content-Typeapplication/json

The JSON body is an envelope:

JSON
{
  "id": "evt_415109_1783299902",
  "type": "constituent.created",
  "created": 1783299902,
  "team": 6,
  "data": {
    "PkConstituent": 415109,
    "FkTeam": 6,
    "ConstituentName": "Jane Smith",
    "FirstName": "Jane",
    "LastName": "Smith",
    "Email1": "[email protected]",
    "Phone1": "5551234567",
    "DateCreated": "2026-07-05T21:04:31.263"
  }
}
  • id — unique id for the event.
  • type — the event type (matches X-Argenta-Event).
  • created — Unix timestamp (seconds, UTC) when the event was generated.
  • team — the Argenta team the record belongs to.
  • data — the record for that event. Constituent events carry the constituent's fields; volunteer and volunteer-task events carry that record's fields (a task event includes its title, start/end times, and the assigned volunteer); donation events carry the amount, gift date, designated purpose, and the linked donor. Update events add DateLastModified, delete events add DeletedUtc. The volunteer-task person events (volunteer_task.signed_up, volunteer_task.canceled, and volunteer_task.unassigned) instead nest their data into constituent, task, and taskGroup objects, plus a top-level EventUtc timestamp (see below).

Task person-event payload (sign-up, cancel, unassign)

The volunteer_task.signed_up, volunteer_task.canceled, and volunteer_task.unassigned events share one richer, nested data object: the constituent (the volunteer who signed up, or the one who was removed for a cancel or unassign), the task, and the taskGroup it belongs to, plus a top-level EventUtc timestamp. All three use the identical shape; the type field tells them apart, so a single handler can process every one. PkTask is a number; PkConstituent and PkTaskGroup are strings.

JSON
{
  "id": "evt_202156_1784222109",
  "type": "volunteer_task.signed_up",
  "created": 1784222109,
  "team": 6,
  "data": {
    "constituent": {
      "PkConstituent": "415109",
      "FirstName": "Jane",
      "LastName": "Smith",
      "Email1": "[email protected]",
      "Phone1": "5551234567"
    },
    "task": {
      "PkTask": 202156,
      "TaskStartDateTime": "2026-11-30T13:00:00",
      "TaskEndDateTime": "2026-11-30T16:00:00",
      "VolunteerTaskTitle": "HQ Assistant #1",
      "VolunteerTaskDescription": ""
    },
    "taskGroup": {
      "PkTaskGroup": "94058",
      "TaskGroupTitle": "HQ Staffing - Fall 2026",
      "GroupLocationName": "Main Office",
      "DistrictName": "District 5",
      "PrecinctName": "Precinct 12"
    },
    "EventUtc": "2026-07-16T17:15:05.197"
  }
}

Verify the signature

The X-Argenta-Signature header looks like:

X-Argenta-Signature
t=1783299902,v1=f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8
  • t — the Unix timestamp (seconds) when we signed the request.
  • v1 — an HMAC-SHA256, hex-encoded, of the string <t>.<body> using your endpoint's signing secret as the key. <body> is the exact raw request body.

To verify: compute the HMAC yourself and confirm it matches v1. Reject the request if it doesn't, or if t is too old for your tolerance (guards against replay).

const crypto = require('crypto');

function verify(rawBody, signatureHeader, secret) {
    const m = /t=(\d+),v1=([0-9a-f]+)/.exec(signatureHeader || '');
    if (!m) return false;
    const [, t, v1] = m;
    const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Verify against the raw body bytes, before any JSON parsing that might re-serialize and change them.

Responding, retries, and duplicates

  • Respond quickly with a 2xx. Any 2xx status tells us the delivery succeeded. Do your slow work after you respond (queue it), not before.
  • Retries. If your endpoint returns a non-2xx or times out, we retry with exponential backoff (about 30s, then 1m, 2m, 4m… up to roughly an hour) for several attempts before giving up.
  • At-least-once delivery. A webhook may occasionally be delivered more than once. Use the event id (or X-Argenta-Delivery) to make your handler idempotent — safe to receive twice.
  • Order is not guaranteed. Don't assume events arrive in the exact order they happened; use the record's own timestamps if order matters.

Checklist

  • Endpoint is https:// and returns 2xx fast.
  • You verify X-Argenta-Signature on every request with your signing secret.
  • Your handler is idempotent on the event id.
  • You store the signing secret securely and rotate it if it's ever exposed.
Reconnecting…

Whoops!

You've Encountered An Error

Something went wrong on our end. Don't worry, our development team has already been notified, and we'll get it fixed as soon as we can.

Please avoid repeating the same action for now. A quick reload usually gets you going again.