Webhooks
Receive real-time HTTP notifications when events happen in GiveLink — donations, subscriptions, campaigns, and more.
Webhooks let your application receive real-time HTTP POST notifications when events happen in GiveLink. Use them to sync data to external systems, trigger workflows, update your CRM, or build custom notification pipelines — all without polling the API.
Webhooks are available to all GiveLink organizations. There is no limit on the number of webhook endpoints you can configure.
Setting Up Webhooks
Navigate to webhook settings
Go to Dashboard > Settings > Webhooks and click Add Endpoint.
Enter your endpoint URL
Provide the HTTPS URL where GiveLink should send event payloads. The endpoint must:
- Use HTTPS (HTTP endpoints are not accepted)
- Return a
2xxstatus code within 30 seconds - Accept
POSTrequests with aContent-Type: application/jsonbody
Select events to subscribe to
Choose which events trigger notifications to this endpoint. You can subscribe to all events or select specific ones. Subscribing to fewer events reduces noise and makes your handler simpler.
Copy your signing secret
GiveLink generates a unique signing secret for each endpoint. Copy it and store it securely — you will use it to verify that incoming payloads are authentic.
Verify with a test event
Click Send Test Event to send a sample payload to your endpoint. GiveLink confirms the endpoint is reachable and responding correctly before activating it.
Available Events
Subscribe to the events your integration needs. Events are grouped by resource type.
| Event | Description |
|---|---|
donation.created | A new donation was initiated (payment not yet confirmed) |
donation.succeeded | Payment was successfully processed and funds captured |
donation.failed | Payment attempt failed (declined card, insufficient funds, etc.) |
donation.refunded | A full or partial refund was processed |
donation.disputed | A chargeback or dispute was opened on a donation |
| Event | Description |
|---|---|
subscription.created | A new recurring donation subscription was created |
subscription.renewed | A scheduled recurring charge was successfully processed |
subscription.failed | A scheduled recurring charge failed |
subscription.paused | A subscription was paused (by donor or admin) |
subscription.resumed | A paused subscription was resumed |
subscription.canceled | A subscription was permanently canceled |
subscription.updated | A subscription amount or frequency was changed |
| Event | Description |
|---|---|
donor.created | A new donor record was created (first-time donor) |
donor.updated | Donor information was updated (name, email, address) |
| Event | Description |
|---|---|
campaign.created | A new campaign was created |
campaign.updated | Campaign settings, goal, or content was updated |
campaign.completed | A campaign reached its goal or end date |
Payload Format
All webhook payloads follow a consistent JSON structure.
Envelope
{
"id": "evt_2fGk8pQx1mNr4vYz",
"event": "donation.succeeded",
"timestamp": "2026-03-06T18:30:00.000Z",
"apiVersion": "2026-01-01",
"data": {
// Event-specific payload (see examples below)
}
}| Field | Type | Description |
|---|---|---|
id | string | Unique event ID. Use this for idempotency — the same event ID is sent on retries |
event | string | Event type identifier |
timestamp | string | ISO 8601 timestamp of when the event occurred |
apiVersion | string | API version used to generate the payload |
data | object | Event-specific data (varies by event type) |
Example: donation.succeeded
{
"id": "evt_2fGk8pQx1mNr4vYz",
"event": "donation.succeeded",
"timestamp": "2026-03-06T18:30:00.000Z",
"apiVersion": "2026-01-01",
"data": {
"id": "don_7hJm3nRs9tKw2xBv",
"amountCents": 5000,
"currency": "usd",
"feeCents": 50,
"netCents": 4950,
"donorId": "dnr_4kLp6qTs2uMv8yDx",
"donorEmail": "sarah@example.com",
"donorName": "Sarah Johnson",
"campaignId": "cmp_9nRw3vYz5aJm7pBt",
"campaignName": "Spring Appeal 2026",
"orgId": "org_1bDf4hKn6rTv8xAz",
"isRecurring": false,
"paymentMethod": "card",
"stripePaymentIntentId": "pi_3PqRsT4uVw5xYz",
"metadata": {},
"createdAt": "2026-03-06T18:29:55.000Z"
}
}Example: subscription.created
{
"id": "evt_5jNr8tWz3bFk6pQx",
"event": "subscription.created",
"timestamp": "2026-03-06T18:30:00.000Z",
"apiVersion": "2026-01-01",
"data": {
"id": "sub_3gHk7mPr1sVw5yBt",
"amountCents": 2500,
"currency": "usd",
"frequency": "monthly",
"status": "active",
"donorId": "dnr_4kLp6qTs2uMv8yDx",
"donorEmail": "sarah@example.com",
"donorName": "Sarah Johnson",
"campaignId": "cmp_9nRw3vYz5aJm7pBt",
"campaignName": "Spring Appeal 2026",
"orgId": "org_1bDf4hKn6rTv8xAz",
"stripeSubscriptionId": "sub_1AbCdEfGhIjKlM",
"currentPeriodStart": "2026-03-06T00:00:00.000Z",
"currentPeriodEnd": "2026-04-06T00:00:00.000Z",
"createdAt": "2026-03-06T18:29:55.000Z"
}
}Example: donation.refunded
{
"id": "evt_8mQw2vZx4cHk9rTy",
"event": "donation.refunded",
"timestamp": "2026-03-07T10:15:00.000Z",
"apiVersion": "2026-01-01",
"data": {
"id": "don_7hJm3nRs9tKw2xBv",
"originalAmountCents": 5000,
"refundAmountCents": 5000,
"refundType": "full",
"reason": "donor_request",
"donorId": "dnr_4kLp6qTs2uMv8yDx",
"donorEmail": "sarah@example.com",
"campaignId": "cmp_9nRw3vYz5aJm7pBt",
"orgId": "org_1bDf4hKn6rTv8xAz",
"stripeRefundId": "re_1NoPqRsTuVwXyZ",
"refundedAt": "2026-03-07T10:14:50.000Z"
}
}Webhook Signing and Verification
Every webhook payload is signed with your endpoint's unique signing secret. Always verify signatures to ensure payloads are authentic and have not been tampered with.
How Signing Works
- GiveLink computes an HMAC-SHA256 hash of the raw request body using your signing secret
- The resulting signature is sent in the
X-GiveLink-Signatureheader - Your application recomputes the hash using the same secret and compares it to the header value
Verification Code
import crypto from 'crypto';
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// In your webhook handler:
app.post('/webhooks/givelink', (req, res) => {
const signature = req.headers['x-givelink-signature'];
const rawBody = req.rawBody; // Must be the raw string, not parsed JSON
if (!verifyWebhookSignature(rawBody, signature, process.env.GIVELINK_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(rawBody);
// Process the event...
res.status(200).json({ received: true });
});import hmac
import hashlib
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# In your webhook handler (Flask example):
@app.route('/webhooks/givelink', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-GiveLink-Signature')
raw_body = request.get_data()
if not verify_webhook_signature(raw_body, signature, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 401
event = request.get_json()
# Process the event...
return jsonify({'received': True}), 200require 'openssl'
def verify_webhook_signature(payload, signature, secret)
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, payload)
Rack::Utils.secure_compare(signature, expected)
end
# In your webhook handler (Sinatra example):
post '/webhooks/givelink' do
signature = request.env['HTTP_X_GIVELINK_SIGNATURE']
raw_body = request.body.read
unless verify_webhook_signature(raw_body, signature, ENV['GIVELINK_WEBHOOK_SECRET'])
halt 401, { error: 'Invalid signature' }.to_json
end
event = JSON.parse(raw_body)
# Process the event...
{ received: true }.to_json
endAlways use the raw request body (the exact string received) when computing the signature, not a re-serialized version of the parsed JSON. Re-serialization can change key ordering or whitespace, producing a different hash.
Retry Policy
If your endpoint returns a non-2xx status code or does not respond within 30 seconds, GiveLink retries the delivery with exponential backoff.
| Attempt | Delay After Failure | Cumulative Time |
|---|---|---|
| 1st retry | 1 minute | 1 minute |
| 2nd retry | 5 minutes | 6 minutes |
| 3rd retry | 30 minutes | 36 minutes |
| 4th retry | 2 hours | ~2.5 hours |
| 5th retry | 12 hours | ~14.5 hours |
After 5 failed attempts, the event is marked as failed and no further retries are attempted.
Handling Retries
Testing Console
GiveLink includes a built-in testing console so you can develop and debug your webhook integration without processing real donations.
Using the Testing Console
Open the testing console
Navigate to Dashboard > Settings > Webhooks, select an endpoint, and click Test.
Choose an event type
Select the event type you want to test (e.g., donation.succeeded, subscription.created). The console generates a realistic sample payload.
Customize the payload (optional)
Edit the sample payload to match your test scenario. Change amounts, donor names, campaign IDs, or any other fields.
Send the test event
Click Send. GiveLink delivers the payload to your endpoint and displays the response status code, headers, and body.
Review the delivery log
Every test delivery (and live delivery) is logged with the full request payload, response, and timing. Use this log to debug issues with your handler.
Delivery Log
The delivery log for each endpoint shows:
| Field | Description |
|---|---|
| Event ID | Unique identifier for the event |
| Event type | The event that triggered the delivery |
| Timestamp | When the delivery was attempted |
| Status | Success (2xx), failed (non-2xx), or timed out |
| Response code | HTTP status code returned by your endpoint |
| Response time | How long your endpoint took to respond |
| Payload | Full JSON payload that was sent (click to expand) |
| Response body | Full response body from your endpoint (click to expand) |
Test events are clearly marked with "test": true in the payload so your handler can distinguish them from live events. Test events do not count toward your rate limit.
Best Practices
Verify every payload
Always validate the X-GiveLink-Signature header. Never trust a webhook payload without verifying its authenticity, even in development.
Respond with 200 immediately
Acknowledge receipt before processing. Queue the work for async handling. A slow handler causes unnecessary retries and duplicate processing.
Implement idempotency
Store and check the event id to prevent processing the same event twice. Retries are inevitable — your handler must be safe to run multiple times.
Use HTTPS only
GiveLink only delivers webhooks to HTTPS endpoints. This protects payloads in transit and prevents interception of sensitive donor data.
Last updated on 4/5/2026