Skip to content
Aller au contenu
Charitips

Integrate

Webhooks

Get a call on your server when a donation, a vote, a campaign or a receipt changes, check its signature, and handle repeats and retries.

A webhook sends a POST request to your server each time an object of your account changes: a donation is created or claimed, a campaign is activated, a fiscal receipt is ready. Listen to them instead of polling the API.

In short

  • Create a webhook in the dashboard, under Settings, API, or with Create a webhook: a URL, the events to receive, and how Charitips authenticates to your server.
  • Each call carries event, payload (the object) and diff (what changed).
  • Check the X-Charitips-Signature header with the webhook's signingKey, shown once, on creation.
  • A call can arrive twice: handle each change once. Answer 2xx within 10 seconds, then do the work.
  • A parent account's webhooks also receive the events of its child accounts.

Create a webhook

Give it a name, the target URL, the events to receive and an authentication: BASIC with a username and a password, or BEARER with an accessToken. Charitips sends them in the Authorization header of each call, so your server can refuse anyone else.

POST /v1/webhooks
curl -X POST https://api.sandbox.charitips.com/v1/webhooks \  -H "Authorization: Bearer $CHARITIPS_TOKEN" \  -H "Content-Type: application/json" \  -d '{    "name": "Donations",    "target": "https://example.com/webhooks/charitips",    "events": [      "donation_create",      "donation_update"    ],    "authenticationStrategy": "BEARER",    "accessToken": "<a secret of your own>"  }'
Built from the spec and the API code

Request bodies are written for this docs and checked against the API's validation; ids in angle brackets are placeholders. Response values are placeholders built from the schema.

The response holds the signingKey. It is shown this once: store it in your server's secrets. A lost key cannot be read again; delete the webhook and create a new one for the same URL.

Webhooks need API access on your plan (402 Plan upgrade required otherwise). A webhook belongs to one environment: a sandbox webhook only receives sandbox events. With account hierarchy, pass a child's accountId to create the webhook on that child account.

Events

EventSent when
donation_create, donation_updatea donation is created, then on every change: claim, validation, payment
vote_create, vote_updatea vote is created, then on every change, such as the cast
recurringdonation_create, recurringdonation_updatea recurring donation is created, then on every change
campaign_create, campaign_updatea campaign is created, then on every change: activation, closing
charitiesselection_create, charitiesselection_updatea selection is created, then when charities are added or removed
paymentmethod_createa payment method is added to the account
fiscalreceipt_generateda fiscal receipt is ready
file_generateda file you asked for, such as the PDF of printed cards, is ready
charity_publisha new charity joins the catalog
charityprofile_updatea charity updates one of its published profiles

Update events are also sent for changes Charitips makes itself, such as a donation assigned to a charity when its campaign closes. Missing an event? Tell us.

What a call holds

JSON
{  "event": "donation_update",  "payload": {    "donationId": "6f1c2a4e-3b5d-4e8f-9a0b-1c2d3e4f5a6b",    "campaignId": "c0ffee00-1234-4abc-8def-0123456789ab",    "status": "DONE",    "charityId": "85e36146-206f-4c89-86c4-90b11e744c22",    "amount": 2000,    "uniqueExternalId": "order_1042"  },  "diff": { "status": "DONE", "charityId": "85e36146-206f-4c89-86c4-90b11e744c22" }}
  • payload is the object after the change, as Charitips stores it, with its ids and its status (shortened here).
  • diff holds the fields that changed; on a create event, the whole object. Charity events have no diff.
  • fiscalreceipt_generated and file_generated carry no file link: get it with Generate a fiscal receipt download url or Generate a file download url.
  • Route the event with the object's own accountId and your ids (uniqueExternalId, externalId).

Check the signature

Each call has two headers:

  • X-Charitips-Timestamp: when it was sent, in milliseconds since 1970;
  • X-Charitips-Signature: sha256= followed by the HMAC-SHA256, in hex, of <timestamp>:<raw body>, keyed with the webhook's signingKey.

Compute it on the raw body, before any JSON parsing: a parsed and serialized body no longer matches.

JavaScript
import crypto from 'node:crypto';import express from 'express'; const FIVE_MINUTES = 5 * 60 * 1000; export function isValidCharitipsSignature(rawBody, timestamp, signature, secret = process.env.CHARITIPS_WEBHOOK_SIGNING_KEY) {  if (!rawBody || !timestamp || !signature) return false;  const sentAt = Number(timestamp);  if (!Number.isFinite(sentAt) || Math.abs(Date.now() - sentAt) > FIVE_MINUTES) return false;  const hash = crypto.createHmac('sha256', secret).update(`${timestamp}:${rawBody}`).digest('hex');  const expected = Buffer.from(`sha256=${hash}`);  const received = Buffer.from(signature);  return received.length === expected.length && crypto.timingSafeEqual(received, expected);} const app = express(); // The raw body: the signature is computed on the bytes as sent, before any JSON parsing.app.post('/webhooks/charitips', express.raw({ type: 'application/json' }), (req, res) => {  const rawBody = req.body.toString();  if (!isValidCharitipsSignature(rawBody, req.header('X-Charitips-Timestamp'), req.header('X-Charitips-Signature'))) {    return res.status(401).send('Invalid signature');  }  const { event, payload } = JSON.parse(rawBody);  // Act on event and payload once per resource and status: a delivery can come twice.  res.sendStatus(200);});

The check refuses a timestamp more than five minutes away from now, in the past or in the future, which stops a replayed call. It compares the lengths before timingSafeEqual, which throws on buffers of different sizes.

Delivery and retries

  • At least once. The same change can arrive twice. Store what you processed, such as the donationId and its status, and skip a repeat.
  • Answer within 10 seconds. Reply with a 2xx first, then do the work, in a queue for example. Charitips waits 10 seconds at most, and a call that times out is not retried.
  • Retries. After 429, 500, 502, 503 or 504, the call is retried, up to twice, within a few minutes. Any other status, such as 401 or 404, means the webhook is misconfigured and is not retried.
  • Order. Calls can arrive out of order: rely on the status in payload, not on the order of arrival.

List webhooks and Get a webhook show your webhooks; Delete a webhook removes one.

Account hierarchy

An event on a child account's object goes to the webhooks of that child account and to those of its parent. One webhook on your account receives the events of all your customers: route each one with the accountId of its payload. See Account hierarchy.

Example: act when a donation is claimed

When a person picks a charity, the donation becomes DONE. To decrease a points balance or mark an order on your side when that happens, listen to both events: a donation created with its charity is DONE from the start.

JavaScript
const { event, payload, diff } = JSON.parse(rawBody);const claimed = (event === 'donation_update' && diff.status === 'DONE') || (event === 'donation_create' && payload.status === 'DONE');if (claimed) {  // Find your own record with payload.uniqueExternalId or payload.externalId,  // and process it unless it is already done.}

Donation lifecycle, Troubleshooting, Create a webhook.