Webhooks
Use webhooks to subscribe to updates on particular events that occur in Goodstack. Each time an event that you have subscribed to occurs, Goodstack submits a POST request to the designated webhook URL with information about the event.
To receive webhook notifications, use the webhook subscriptions API.
Attributes
At the top level Webhook payloads will contain object and data fields. The data record contains the following fields:
| Parameter | Type | Description |
|---|---|---|
id | string | Id of the event |
createdAt | string | Timestamp of when the event was created |
eventType | string | The type of the event e.g. validation_request.approved |
eventData | object | Data associated with the event |
Event types
| Event | Description |
|---|---|
validation_request.approved | Validation request was approved |
validation_request.rejected | Validation request was rejected |
agent_verification.pending_review | Agent has passed email verification and is awaiting Goodstack review |
agent_verification.pending_user_verification | Agent has passed Goodstack review and is awaiting email verification |
agent_verification.approved | Agent verification was approved |
agent_verification.rejected | Agent verification was rejected |
monitoring_subscription.updated | Monitoring subscription was updated |
eligibility_subscription.updated | Eligibility subscription was updated |
validation_submission.created | Validation submission was created |
validation_submission.succeeded | Validation submission has passed all checks |
validation_submission.failed | Validation submission has failed |
validation_submission.updated | Validation submission status has updated |
donation.hosted.payment_received | Hosted donation payment was received |
donation.created | Donation was created |
donation.settled | Donation was settled |
donation.disbursed | Donation was disbursed |
donation.payment_successful | Donation payment was successful |
donation.cancelled | Donation was cancelled |
donation.reassigned | Donation was reassigned |
Webhook source IP addresses
The full list of IP addresses that webhook notifications may come from.
Production environment:
54.228.234.20454.76.67.16834.248.188.8934.243.152.218
Sandbox environment:
79.125.45.12454.76.168.24099.81.243.14554.220.118.167
Webhook responses
To acknowledge receipt of a webhook notification, your endpoint must return a 2xx HTTP status code.
If the webhook is not received successfully then Goodstack will resend the webhook 4 times over the next 14 hours with increasing delays between retries.
The id of the event can be used to uniquely identify the event. We recommend making the processing of these events idempotent. While rare it is possible for multiple webhooks to be sent for the same event.
Verifying webhooks
To verify that the webhook payload is sent from Goodstack, a header Goodstack-Signature is included in the request, this signature is generated using HMAC with SHA-256 hashing algorithm and Hex encoded. The webhook subscription secret is used as the key.
You can compute this signature using HMAC with the SHA-256 hash function, using the webhook subscription secret as the key, and the request body as the message. You can then check that the Goodstack-Signature value matches the computed value, verifying that the webhook was sent from Goodstack.
If you are subscribed to webhook notifications for updates on the status of verification or donation events, you can verify that webhooks sent to your infrastructure are genuine by verifying the webhook signature.
Get in contact for support with webhook verification on any systems not listed below
- JavaScript
- PHP
- Python
- Go
- Java
const crypto = require('crypto')
const express = require('express')
const app = express()
const endpointSecret = 'sk_xxxxxxxxxxxxxxxxxxxxxxxx'
const verifySignature = (secret, payload, signature) => {
const hmac = crypto.createHmac('sha256', secret)
hmac.write(payload)
hmac.end()
return hmac.read().toString('hex') === signature
}
app.post(
'/webhook',
express.raw({ type: 'application/json' }),
(request, response) => {
const payload = request.body
const sig = request.headers['goodstack-signature']
if (!verifySignature(endpointSecret, payload, sig)) {
return response.status(401).send()
}
// perform business logic based on event
response.status(202).send()
},
)
<?php
define('API_SECRET_KEY', 'my_webhook_api_secret');
function verify_webhook($data, $hmac_header)
{
# Calculate HMAC (hex encoded)
$calculated_hmac = hash_hmac('sha256', $data, API_SECRET_KEY);
return hash_equals($hmac_header, $calculated_hmac);
}
# Extract the signature header
$hmac_header = getallheaders()['Goodstack-Signature'];
# Get the raw body
$data = file_get_contents('php://input');
# Compare HMACs
$verified = verify_webhook($data, $hmac_header);
error_log('Webhook verified: '.var_export($verified, true));
if ($verified) {
# Do something with the webhook
} else {
http_response_code(401);
}
?>
from flask import Flask, request, abort
import hmac
import hashlib
app = Flask(__name__)
API_SECRET_KEY = 'my_webhook_api_secret'
def verify_webhook(data, hmac_header):
# Calculate HMAC (hex encoded)
digest = hmac.new(API_SECRET_KEY.encode('utf-8'), data, digestmod=hashlib.sha256).hexdigest()
return hmac.compare_digest(digest, hmac_header)
@app.route('/webhook', methods=['POST'])
def handle_webhook():
# Get raw body
data = request.get_data()
# Compare HMACs
verified = verify_webhook(data, request.headers.get('Goodstack-Signature'))
if not verified:
abort(401)
# Do something with the webhook
return ('', 200)
package verifywebhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"io"
"net/http"
)
// Hook is an inbound webhook
type Hook struct {
Signature string
Payload []byte
}
func signBody(secret, body []byte) string {
// Calculate HMAC-SHA256 (hex encoded)
computed := hmac.New(sha256.New, secret)
computed.Write(body)
return hex.EncodeToString(computed.Sum(nil))
}
func (h *Hook) SignedBy(secret []byte) bool {
expected := signBody(secret, h.Payload)
return hmac.Equal([]byte(expected), []byte(h.Signature))
}
func (h *Hook) Extract(dst interface{}) error {
return json.Unmarshal(h.Payload, dst)
}
func New(req *http.Request) (hook *Hook, err error) {
hook = new(Hook)
if req.Method != "POST" {
return nil, errors.New("Unknown method!")
}
// Extract signature
if hook.Signature = req.Header.Get("Goodstack-Signature"); len(hook.Signature) == 0 {
return nil, errors.New("No signature!")
}
// Get raw body
hook.Payload, err = io.ReadAll(req.Body)
return
}
func Parse(secret []byte, req *http.Request) (hook *Hook, err error) {
hook, err = New(req)
// Compare HMACs
if err == nil && !hook.SignedBy(secret) {
err = errors.New("Invalid signature")
}
return
}
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
class SignatureVerificationHandler implements HttpHandler {
private String encodingAlgorithm = "HmacSHA256";
private String secretKey = "someSecretKeyThatShouldBeSecure";
private String headerThatContainsSignature = "Goodstack-Signature";
private boolean verifySignature(String payload, String signature) throws NoSuchAlgorithmException, InvalidKeyException {
var sha256_HMAC = Mac.getInstance(encodingAlgorithm);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), encodingAlgorithm);
sha256_HMAC.init(secretKeySpec);
byte[] hash = sha256_HMAC.doFinal(payload.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
hexString.append(String.format("%02x", b));
}
return hexString.toString().equals(signature);
}
@Override
public void handle(HttpExchange httpExchange) throws IOException {
Headers headers = httpExchange.getRequestHeaders();
String signature = headers.getFirst(headerThatContainsSignature);
String payload = readBody(httpExchange);
try {
boolean isValidMessage = verifySignature(payload, signature);
if (isValidMessage){
returnWithStatus(httpExchange, 200);
return;
}
} catch (Exception e) {
returnWithStatus(httpExchange, 500);
return;
}
returnWithStatus(httpExchange, 401);
}
private String readBody(HttpExchange httpExchange) throws IOException {
BufferedInputStream stream = new BufferedInputStream(httpExchange.getRequestBody());
ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
for (int result = stream.read(); result != -1; result = stream.read()) {
byteBuffer.write((byte) result);
}
return byteBuffer.toString(StandardCharsets.UTF_8);
}
private void returnWithStatus(HttpExchange httpExchange, int httpStatusCode) throws IOException {
httpExchange.sendResponseHeaders(httpStatusCode, 0);
httpExchange.getResponseBody().close();
}
}