Gaya API sends real-time webhook notifications for clipboard events to your specified endpoint. Instantly trigger custom actions when data is created, modified, or exported. Integrate seamlessly with your systems, update databases, or power automations. Use this playground to test your webhook integration before deployment.
Currently, the Gaya API sends webhooks for the following event:
Event type | Trigger description |
---|---|
EXPORT_CLIPBOARD | Triggered once clipboard data has been exported |
The following example illustrates the structure of webhook payloads, providing information about the event that occurred:
{
"hook": {
"id": "9c3fba86-38e5-4655-bf2e-3ef0015c0c44",
"event": "EXPORT_CLIPBOARD",
"target": "https://yoururl.com"
},
"organization": {
"user": "[email protected]",
"name": "John Doe Organization",
"origination_clipboard": "Progressive",
"office": "Austin HQ"
},
"entities": [
{
"entity": "customer",
"index": 1,
"fields": [
{
"name": "email",
"value": "[email protected]"
},
{
"name": "first_name",
"value": "John"
},
{
"name": "last_name",
"value": "Doe"
}
]
},
{
"entity": "car",
"index": 1,
"fields": [
{
"name": "make",
"value": "Toyota"
},
{
"name": "model",
"value": "Corolla"
}
]
},
{
"entity": "car",
"index": 2,
"fields": [
{
"name": "make",
"value": "Honda"
},
{
"name": "model",
"value": "Civic"
}
]
}
],
"created_at": "2024-09-26T02:47:16.894Z"
}
At a high level, in order to implement webhooks, you'll need to:
Contact Gaya to register a URL to receive the webhooks.
Validate webhook deliveries to ensure they come from Gaya.
Grab the relevant pieces of payload you need and then perform your business logic.
After registering your URL to receive webhooks, Gaya will provide a secret token to validate webhook deliveries. You'll need to store it in a safe place that your server can access. Never hardcode a token into an application or push a token to any repository.
Please reach out to Gaya whenever you want to discontinue receiving webhook events.
Gaya will use your secret token to create a hash signature that's sent to your public POST endpoint with each payload. The hash signature will appear in each delivery as the value of the X-Gaya-Signature-256
header.
The signature is sensitive to any changes, so even a small change in the payload will cause the signature to be completely different. This means that you should not change the payload in any way before verifying.
In your code that handles webhook deliveries, you should calculate a hash using your secret token. Then, compare the hash that Gaya sent with the expected hash that you calculated, and ensure that they match. For examples showing how to validate the hashes in various programming languages, see "Examples."
There are a few important things to keep in mind when validating webhook payloads:
Gaya uses an HMAC hex digest to compute the hash.
The hash signature always starts with sha256=
.
The hash signature is generated using your webhook's secret token and the payload contents.
Never use a plain ==
operator. Instead consider using a method like secure_compare
or crypto.timingSafeEqual
, which performs a "constant time" string comparison to help mitigate certain timing attacks against regular equality operators, or regular loops in JIT-optimized languages.
You can use the programming language of your choice to implement HMAC verification in your code. Below are a few examples of how an implementation can look in various programming languages.
import express, { Request, Response } from 'express';
import * as crypto from 'crypto';
const app = express();
app.use(express.json()); // Middleware to parse JSON bodies
/**
* Verifies if the HMAC signature in the request header matches the expected signature.
*
* @param req - The request object from Express.js containing the webhook data.
* @param secretKey - The secret key used for verifying the HMAC signature.
* @returns true if the signature is valid, false otherwise.
* @throws {Error} - Throws an error if the secret key is missing or if any other issue occurs during verification.
*/
function verifySignature(req: Request, secretKey: string): boolean {
if (!secretKey) {
throw new Error('Secret key for HMAC verification is missing.');
}
const signatureHeader = req.headers['x-gaya-signature-256'];
if (!signatureHeader || typeof signatureHeader !== 'string') {
throw new Error('Signature header "x-gaya-signature-256" is missing or not in correct format.');
}
if (!req.body || Object.keys(req.body).length === 0) {
throw new Error('Request body is empty.');
}
const payload = JSON.stringify(req.body);
let generatedSignature: string;
try {
const hmac = crypto.createHmac('sha256', secretKey);
generatedSignature = hmac.update(payload).digest('hex');
} catch (error) {
throw new Error('Error occurred during HMAC signature generation.');
}
const trustedSignature = `sha256=${generatedSignature}`;
return crypto.timingSafeEqual(Buffer.from(trustedSignature, 'utf8'), Buffer.from(signatureHeader, 'utf8'));
}
/**
* Handles incoming webhooks and processes them based on the event type.
*
* @param req - The request object from Express.js.
* @param res - The response object from Express.js.
* @throws {Error} - Throws an error if the webhook signature verification fails.
*/
function webhookProcessingEndpoint(req: Request, res: Response): void {
try {
const WEBHOOK_SECRET: string = process.env.WEBHOOK_SECRET || '';
if (!verifySignature(req, WEBHOOK_SECRET)) {
res.status(401).send('Webhook verification failed');
return;
}
const { event_type: eventType } = req.body;
switch (eventType) {
case 'EXPORT_CLIPBOARD':
// Business logic for EXPORT_CLIPBOARD event
console.log('Processing EXPORT_CLIPBOARD event');
// Add your EXPORT_CLIPBOARD logic here
res.status(200).send('EXPORT_CLIPBOARD event processed successfully');
break;
default:
res.status(422).send('The Webhook event type is unknown');
return;
}
} catch (error) {
console.error(`Error processing webhook: ${error.message}`);
res.status(500).send('Internal server error while processing the webhook');
}
}
// Define the route for the webhook endpoint
app.post('/webhook', webhookProcessingEndpoint);
// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import os
app = FastAPI()
# Middleware to parse JSON bodies is not required in FastAPI as it automatically handles JSON requests
def verify_signature(request: Request, secret_key: str) -> bool:
"""
Verifies if the HMAC signature in the request header matches the expected signature.
Args:
request: The request object from FastAPI containing the webhook data.
secret_key: The secret key used for verifying the HMAC signature.
Returns:
True if the signature is valid, False otherwise.
Raises:
HTTPException: If the secret key is missing or if any other issue occurs during verification.
"""
if not secret_key:
raise HTTPException(status_code=400, detail='Secret key for HMAC verification is missing.')
signature_header = request.headers.get('x-gaya-signature-256')
if not signature_header:
raise HTTPException(status_code=400, detail='Signature header "x-gaya-signature-256" is missing or not in correct format.')
body = await request.body()
if not body:
raise HTTPException(status_code=400, detail='Request body is empty.')
try:
hmac_obj = hmac.new(secret_key.encode(), body, hashlib.sha256)
generated_signature = hmac_obj.hexdigest()
except Exception as error:
raise HTTPException(status_code=500, detail='Error occurred during HMAC signature generation.')
trusted_signature = f"sha256={generated_signature}"
return hmac.compare_digest(trusted_signature, signature_header)
@app.post("/webhook")
async def webhook_processing_endpoint(request: Request):
"""
Handles incoming webhooks and processes them based on the event type.
Args:
request: The request object from FastAPI.
Raises:
HTTPException: If the webhook signature verification fails.
"""
try:
WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "")
# Verify the HMAC signature
if not await verify_signature(request, WEBHOOK_SECRET):
raise HTTPException(status_code=401, detail='Webhook verification failed')
payload = await request.json()
event_type = payload.get("event_type")
if event_type == 'EXPORT_CLIPBOARD':
# Business logic for EXPORT_CLIPBOARD event
print('Processing EXPORT_CLIPBOARD event')
# Add your EXPORT_CLIPBOARD logic here
return {"message": "EXPORT_CLIPBOARD event processed successfully"}
else:
raise HTTPException(status_code=422, detail='The Webhook event type is unknown')
except HTTPException as http_err:
raise http_err
except Exception as error:
print(f"Error processing webhook: {error}")
raise HTTPException(status_code=500, detail='Internal server error while processing the webhook')
# To start the server, use: uvicorn filename:app --reload
# Example: uvicorn main:app --reload
package com.example.webhook;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
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.Map;
@SpringBootApplication
public class WebhookApplication {
public static void main(String[] args) {
SpringApplication.run(WebhookApplication.class, args);
}
}
@RestController
@RequestMapping("/webhook")
class WebhookController {
private static final String HMAC_SHA256 = "HmacSHA256";
/**
* Verifies if the HMAC signature in the request header matches the expected signature.
*
* @param body The request body as a byte array.
* @param secretKey The secret key used for verifying the HMAC signature.
* @param signatureHeader The signature from the request header.
* @return true if the signature is valid, false otherwise.
* @throws IllegalArgumentException If the secret key is missing or an error occurs during verification.
*/
private boolean verifySignature(byte[] body, String secretKey, String signatureHeader) {
if (!StringUtils.hasText(secretKey)) {
throw new IllegalArgumentException("Secret key for HMAC verification is missing.");
}
if (!StringUtils.hasText(signatureHeader)) {
throw new IllegalArgumentException("Signature header 'x-gaya-signature-256' is missing or not in correct format.");
}
if (body == null || body.length == 0) {
throw new IllegalArgumentException("Request body is empty.");
}
try {
Mac hmac = Mac.getInstance(HMAC_SHA256);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
hmac.init(secretKeySpec);
byte[] hmacBytes = hmac.doFinal(body);
String generatedSignature = bytesToHex(hmacBytes);
String trustedSignature = "sha256=" + generatedSignature;
return trustedSignature.equals(signatureHeader);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IllegalArgumentException("Error occurred during HMAC signature generation.", e);
}
}
/**
* Converts byte array to hexadecimal string.
*
* @param bytes The byte array to be converted.
* @return The hexadecimal string representation.
*/
private String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder(2 * bytes.length);
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
/**
* Handles incoming webhooks and processes them based on the event type.
*
* @param body The request body containing the webhook data.
* @param headers The request headers containing the HMAC signature.
* @return ResponseEntity indicating the result of the processing.
*/
@PostMapping
public ResponseEntity<String> webhookProcessingEndpoint(@RequestBody byte[] body, @RequestHeader Map<String, String> headers) {
try {
String WEBHOOK_SECRET = System.getenv("WEBHOOK_SECRET");
// Verify the HMAC signature
String signatureHeader = headers.get("x-gaya-signature-256");
if (!verifySignature(body, WEBHOOK_SECRET, signatureHeader)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Webhook verification failed");
}
String bodyString = new String(body, StandardCharsets.UTF_8);
Map<String, Object> payload = new ObjectMapper().readValue(bodyString, Map.class);
String eventType = (String) payload.get("event_type");
if ("EXPORT_CLIPBOARD".equals(eventType)) {
// Business logic for EXPORT_CLIPBOARD event
System.out.println("Processing EXPORT_CLIPBOARD event");
// Add your EXPORT_CLIPBOARD logic here
return ResponseEntity.ok("EXPORT_CLIPBOARD event processed successfully");
} else {
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body("The Webhook event type is unknown");
}
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
} catch (Exception e) {
System.err.println("Error processing webhook: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Internal server error while processing the webhook");
}
}
}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"github.com/gin-gonic/gin"
)
// VerifySignature verifies if the HMAC signature in the request header matches the expected signature.
//
// Arguments:
// - c: The context object from Gin containing the request data.
// - secretKey: The secret key used for verifying the HMAC signature.
//
// Returns:
// - true if the signature is valid, false otherwise.
// - error if the secret key is missing or if any other issue occurs during verification.
func verifySignature(c *gin.Context, secretKey string) (bool, error) {
if secretKey == "" {
return false, errors.New("secret key for HMAC verification is missing")
}
signatureHeader := c.GetHeader("x-gaya-signature-256")
if signatureHeader == "" {
return false, errors.New("signature header 'x-gaya-signature-256' is missing or not in correct format")
}
body, err := ioutil.ReadAll(c.Request.Body)
if err != nil || len(body) == 0 {
return false, errors.New("request body is empty or could not be read")
}
// Reset the request body so it can be read again in the handler
c.Request.Body = ioutil.NopCloser(c.Request.Body)
// Generate HMAC signature
h := hmac.New(sha256.New, []byte(secretKey))
h.Write(body)
generatedSignature := hex.EncodeToString(h.Sum(nil))
trustedSignature := fmt.Sprintf("sha256=%s", generatedSignature)
return hmac.Equal([]byte(trustedSignature), []byte(signatureHeader)), nil
}
// WebhookProcessingEndpoint handles incoming webhooks and processes them based on the event type.
//
// Arguments:
// - c: The context object from Gin.
//
// It throws an error if the webhook signature verification fails or if the event type is unknown.
func webhookProcessingEndpoint(c *gin.Context) {
WEBHOOK_SECRET := os.Getenv("WEBHOOK_SECRET")
if WEBHOOK_SECRET == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Webhook secret is not set"})
return
}
// Verify the HMAC signature
valid, err := verifySignature(c, WEBHOOK_SECRET)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if !valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Webhook verification failed"})
return
}
var payload map[string]interface{}
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON payload"})
return
}
eventType, ok := payload["event_type"].(string)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "Event type is missing or not a string"})
return
}
switch eventType {
case "EXPORT_CLIPBOARD":
// Business logic for EXPORT_CLIPBOARD event
fmt.Println("Processing EXPORT_CLIPBOARD event")
// Add your EXPORT_CLIPBOARD logic here
c.JSON(http.StatusOK, gin.H{"message": "EXPORT_CLIPBOARD event processed successfully"})
default:
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "The Webhook event type is unknown"})
}
}
func main() {
r := gin.Default()
// Define the route for the webhook endpoint
r.POST("/webhook", webhookProcessingEndpoint)
// Start the server on port 3000
PORT := os.Getenv("PORT")
if PORT == "" {
PORT = "3000"
}
r.Run(fmt.Sprintf(":%s", PORT)) // Default is localhost:3000
}
<?php
namespace AppHttpControllers;
use IlluminateHttpRequest;
use IlluminateSupportFacadesLog;
use SymfonyComponentHttpFoundationResponse;
use Exception;
class WebhookController extends Controller
{
/**
* Verifies if the HMAC signature in the request header matches the expected signature.
*
* @param Request $request The request object from Laravel containing the webhook data.
* @param string $secretKey The secret key used for verifying the HMAC signature.
* @return bool True if the signature is valid, false otherwise.
* @throws Exception If the secret key is missing or if any other issue occurs during verification.
*/
private function verifySignature(Request $request, string $secretKey): bool
{
if (empty($secretKey)) {
throw new Exception('Secret key for HMAC verification is missing.');
}
$signatureHeader = $request->header('x-gaya-signature-256');
if (empty($signatureHeader)) {
throw new Exception('Signature header "x-gaya-signature-256" is missing or not in the correct format.');
}
$payload = $request->getContent();
if (empty($payload)) {
throw new Exception('Request body is empty.');
}
try {
$generatedSignature = hash_hmac('sha256', $payload, $secretKey);
} catch (Exception $e) {
throw new Exception('Error occurred during HMAC signature generation.');
}
$trustedSignature = "sha256={$generatedSignature}";
return hash_equals($trustedSignature, $signatureHeader);
}
/**
* Handles incoming webhooks and processes them based on the event type.
*
* @param Request $request The request object from Laravel.
* @return Response The response indicating the result of the webhook processing.
*/
public function webhookProcessingEndpoint(Request $request): Response
{
try {
$WEBHOOK_SECRET = env('WEBHOOK_SECRET', '');
// Verify the HMAC signature
if (!$this->verifySignature($request, $WEBHOOK_SECRET)) {
return response()->json(['error' => 'Webhook verification failed'], Response::HTTP_UNAUTHORIZED);
}
$payload = $request->json()->all();
$eventType = $payload['event_type'] ?? null;
if ($eventType === 'EXPORT_CLIPBOARD') {
// Business logic for EXPORT_CLIPBOARD event
Log::info('Processing EXPORT_CLIPBOARD event');
// Add your EXPORT_CLIPBOARD logic here
return response()->json(['message' => 'EXPORT_CLIPBOARD event processed successfully']);
} else {
return response()->json(['error' => 'The Webhook event type is unknown'], Response::HTTP_UNPROCESSABLE_ENTITY);
}
} catch (Exception $e) {
Log::error("Error processing webhook: {$e->getMessage()}");
return response()->json(['error' => 'Internal server error while processing the webhook'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
}
<?php
use IlluminateSupportFacadesRoute;
use AppHttpControllersWebhookController;
// Define the route for the webhook endpoint
Route::post('/webhook', [WebhookController::class, 'webhookProcessingEndpoint']);
require 'sinatra'
require 'json'
require 'openssl'
# Configure Sinatra to parse JSON request bodies
set :protection, except: :json_csrf
# Verifies if the HMAC signature in the request header matches the expected signature.
#
# @param request [Sinatra::Request] The request object from Sinatra containing the webhook data.
# @param secret_key [String] The secret key used for verifying the HMAC signature.
# @return [Boolean] true if the signature is valid, false otherwise.
# @raise [StandardError] if the secret key is missing or if any other issue occurs during verification.
def verify_signature(request, secret_key)
raise 'Secret key for HMAC verification is missing.' if secret_key.nil? || secret_key.empty?
signature_header = request.env['HTTP_X_GAYA_SIGNATURE_256']
raise 'Signature header "x-gaya-signature-256" is missing or not in correct format.' if signature_header.nil?
request_body = request.body.read
raise 'Request body is empty.' if request_body.empty?
begin
digest = OpenSSL::Digest.new('sha256')
generated_signature = OpenSSL::HMAC.hexdigest(digest, secret_key, request_body)
rescue => e
raise "Error occurred during HMAC signature generation: #{e.message}"
end
trusted_signature = "sha256=#{generated_signature}"
Rack::Utils.secure_compare(trusted_signature, signature_header)
end
# Handles incoming webhooks and processes them based on the event type.
#
# @param request [Sinatra::Request] The request object from Sinatra.
# @return [Hash] The response indicating the result of the webhook processing.
post '/webhook' do
content_type :json
begin
WEBHOOK_SECRET = ENV['WEBHOOK_SECRET'] || ''
# Verify the HMAC signature
unless verify_signature(request, WEBHOOK_SECRET)
status 401
return { error: 'Webhook verification failed' }.to_json
end
payload = JSON.parse(request.body.read)
event_type = payload['event_type']
case event_type
when 'EXPORT_CLIPBOARD'
# Business logic for EXPORT_CLIPBOARD event
puts 'Processing EXPORT_CLIPBOARD event'
# Add your EXPORT_CLIPBOARD logic here
status 200
{ message: 'EXPORT_CLIPBOARD event processed successfully' }.to_json
else
status 422
{ error: 'The Webhook event type is unknown' }.to_json
end
rescue StandardError => e
puts "Error processing webhook: #{e.message}"
status 500
{ error: 'Internal server error while processing the webhook' }.to_json
end
end
# Start the Sinatra server if this file is executed directly
# Run the application: ruby app.rb
# Sinatra defaults to port 4567
if __FILE__ == $0
set :port, ENV['PORT'] || 4567
run!
end
If you are sure that the payload is from Gaya but the signature verification fails:
Make sure you are using the correct header. Gaya recommends that you use the X-Gaya-Signature-256
header, which uses the HMAC-SHA256 algorithm.
Make sure that you are using the correct algorithm. If you are using the X-Gaya-Signature-256
header, you should use the HMAC-SHA256
algorithm.
Make sure you are using the correct webhook secret.
Make sure that the payload and headers are not modified before verification. For example, if you use a proxy or load balancer, make sure that the proxy or load balancer does not modify the payload or headers.
If your language and server implementation define a character encoding, make sure to process the payload as UTF-8
.