GiveLinkDocs
Integrations

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

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 2xx status code within 30 seconds
  • Accept POST requests with a Content-Type: application/json body

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.

EventDescription
donation.createdA new donation was initiated (payment not yet confirmed)
donation.succeededPayment was successfully processed and funds captured
donation.failedPayment attempt failed (declined card, insufficient funds, etc.)
donation.refundedA full or partial refund was processed
donation.disputedA chargeback or dispute was opened on a donation
EventDescription
subscription.createdA new recurring donation subscription was created
subscription.renewedA scheduled recurring charge was successfully processed
subscription.failedA scheduled recurring charge failed
subscription.pausedA subscription was paused (by donor or admin)
subscription.resumedA paused subscription was resumed
subscription.canceledA subscription was permanently canceled
subscription.updatedA subscription amount or frequency was changed
EventDescription
donor.createdA new donor record was created (first-time donor)
donor.updatedDonor information was updated (name, email, address)
EventDescription
campaign.createdA new campaign was created
campaign.updatedCampaign settings, goal, or content was updated
campaign.completedA 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)
  }
}
FieldTypeDescription
idstringUnique event ID. Use this for idempotency — the same event ID is sent on retries
eventstringEvent type identifier
timestampstringISO 8601 timestamp of when the event occurred
apiVersionstringAPI version used to generate the payload
dataobjectEvent-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

  1. GiveLink computes an HMAC-SHA256 hash of the raw request body using your signing secret
  2. The resulting signature is sent in the X-GiveLink-Signature header
  3. 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}), 200
require '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
end

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

AttemptDelay After FailureCumulative Time
1st retry1 minute1 minute
2nd retry5 minutes6 minutes
3rd retry30 minutes36 minutes
4th retry2 hours~2.5 hours
5th retry12 hours~14.5 hours

After 5 failed attempts, the event is marked as failed and no further retries are attempted.

Handling Retries

Use a webhook ingestion service like Hookdeck, Svix, or a simple queue (SQS, Redis) as a buffer between GiveLink and your application logic. This ensures you never miss events, even if your backend is temporarily down.


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:

FieldDescription
Event IDUnique identifier for the event
Event typeThe event that triggered the delivery
TimestampWhen the delivery was attempted
StatusSuccess (2xx), failed (non-2xx), or timed out
Response codeHTTP status code returned by your endpoint
Response timeHow long your endpoint took to respond
PayloadFull JSON payload that was sent (click to expand)
Response bodyFull 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.

How is this guide?

On this page