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 |
Webhook notifications​
The full list of IP addresses that webhook notifications may come from.
Production environment:
54.76.67.168
34.248.188.89
34.243.152.218
Sandbox environment:
54.76.168.240
99.81.243.145
54.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 app = require('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',
bodyParser.raw({ type: 'application/json' }),
(request, response) => {
const payload = request.body
const sig = request.headers['Goodstack-Signature']
if (!verifySignature(endpointSecret, payload, sig)) {
response.status(403).send()
}
// perform business logic based on event
response.status(202)
},
)
<?php
define('API_SECRET_KEY', 'my_webhook_api_secret');
function verify_webhook($data, $hmac_header)
{
# Calculate HMAC
$calculated_hmac = base64_encode(hash_hmac('sha256', $data, API_SECRET_KEY, true));
return hash_equals($hmac_header, $calculated_hmac);
}
# Extract the signature header
$hmac_header = $_SERVER['X-Signature-SHA256'];
# 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
import base64
app = Flask(__name__)
API_SECRET_KEY = 'my_webhook_api_secret'
def verify_webhook(data, hmac_header):
# Calculate HMAC
digest = hmac.new(API_SECRET_KEY.encode('utf-8'), data.encode('utf-8'), 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('X-Signature-SHA256'))
if not verified:
abort(401)
# Do something with the webhook
return ('', 200)
package verifywebhook
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"strings"
)
// Hook is an inbound webhook
type Hook struct {
Signature string
Payload []byte
}
const signaturePrefix = "" ////set this to your signature prefix if any
const signatureLength = // Your signature length = len(SignaturePrefix) + len(hex(sha1))
func signBody(secret, body []byte) []byte {
// Calculate HMAC
computed := hmac.New(sha1.New, secret)
computed.Write(body)
return []byte(computed.Sum(nil))
}
func (h *Hook) SignedBy(secret []byte) bool {
if len(h.Signature) != signatureLength || !strings.HasPrefix(h.Signature, signaturePrefix) {
return false
}
actual := make([]byte, 20)
hex.Decode(actual, []byte(h.Signature[5:]))
return hmac.Equal(signBody(secret, h.Payload), actual)
}
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 !strings.EqualFold(req.Method, "POST") {
return nil, errors.New("Unknown method!")
}
// Extract signature
if hook.Signature = req.Header.Get("X-Signature-SHA256"); len(hook.Signature) == 0 {
return nil, errors.New("No signature!")
}
// Get raw body
hook.Payload, err = ioutil.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;
import java.util.Base64;
class SignatureVerificationHandler implements HttpHandler {
private String encodingAlgorithm = "HmacSHA256";
private String secretKey = "someSecretKeyThatShouldBeSecure";
private String headerThatContainsSignature = "X-Signature-SHA256";
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());
String message = Base64.getEncoder().encodeToString(hash);
System.out.println("Payload : "+ payload);
System.out.println("Message : "+ message);
System.out.println("Signature : "+ signature);
return message.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){
System.out.println("Got valid signature, returning 200");
returnWithStatus(httpExchange, 200);
return;
}
} catch (Exception e) {
System.out.println("Exception encountered, return 500 server error");
returnWithStatus(httpExchange, 500);
return;
}
System.out.println("Invalid signature, returning 401 Unauthorized");
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();
}
}