Webhooks
AccessGrid can send webhook notifications to your server when events occur. Webhooks use the CloudEvents specification for event delivery.
Configure webhooks in your AccessGrid console.
We use simple bearer tokens for authentication, which is generated whenever you create a new webhook. In general if your server responds with either 200, or 201, then we will not send the event occurrence again.
If we cannot reach your server, or you respond with non 200/201 response code, we will try again for up to 6 hours before dropping the delivery attempts.
spec_version
string
CloudEvents specification version (always "1.0")
id
string
Unique identifier for this event
source
string
Event source (always "accessgrid")
type
string
Event type. Possible values:
Access Pass:
ag.access_pass.issued
ag.access_pass.activated
ag.access_pass.updated
ag.access_pass.suspended
ag.access_pass.resumed
ag.access_pass.unlinked
ag.access_pass.deleted
ag.access_pass.expired
Card Template:
ag.card_template.created
ag.card_template.updated
ag.card_template.requested_publishing
ag.card_template.published
Landing Page:
ag.landing_page.created
ag.landing_page.updated
ag.landing_page.attached_to_template
Credential Profile:
ag.credential_profile.created
ag.credential_profile.attached_to_template
HID Org
ag.hid_org.created
ag.hid_org.activated
Account Balance:
ag.account_balance.low
data_content_type
string
Content type of the data payload (always "application/json")
time
string
ISO 8601 timestamp when the event occurred
data
object
Event-specific data payload
access_pass_id
nullable string
ID of the access pass (for access_pass events)
card_template_id
nullable string
ID of the card template (for card_template events)
landing_page_id
nullable string
ID of the landing page (for landing_page events)
credential_profile_id
nullable string
ID of the credential profile (for credential_profile events)
account_id
nullable string
API ID of the account (for account_balance events)
organization_name
nullable string
Name of the organization (for account_balance events)
current_balance
nullable number
Current balance in dollars (for account_balance events)
threshold
nullable number
Low balance threshold in dollars (for account_balance events)
amount_below_threshold
nullable number
How far below threshold the balance is in dollars (for account_balance events)
protocol
nullable string
Protocol type (desfire, seos, smart_tap)
metadata
nullable object
Custom metadata associated with the resource
device
nullable object
Device information (for access_pass device events)
card_number
nullable string
Card number from credential format, only populated if used directly during issuance
site_code
nullable string
Site code from credential format, only populated if used directly during issuance, otherwise 69
file_data
nullable string
Hex-encoded credential data, only populated if used directly or via credential pools
Request
# Example webhook payload (CloudEvents format)
# This is what AccessGrid will POST to your webhook URL
# Example: Access Pass Issued Event
{
"specversion": "1.0",
"id": "unique-event-id-12345",
"source": "accessgrid",
"type": "ag.access_pass.issued",
"datacontenttype": "application/json",
"time": "2025-01-15T10:30:00Z",
"data": {
"access_pass_id": "0xp455-3x1d",
"protocol": "desfire",
"card_number": "12345",
"site_code": "100",
"file_data": "0A1B2C3D4E5F",
"metadata": {
"custom_field": "value"
}
}
}
# Example: Card Template Published Event
{
"specversion": "1.0",
"id": "unique-event-id-67890",
"source": "accessgrid",
"type": "ag.card_template.published",
"datacontenttype": "application/json",
"time": "2025-01-15T11:00:00Z",
"data": {
"card_template_id": "0xt3mp14t3-3x1d",
"protocol": "seos",
"metadata": {}
}
}
# Verify webhook with your endpoint
curl -X POST https://your-server.com/webhooks \
-H "Content-Type: application/cloudevents+json" \
-H "User-Agent: AccessGrid-Webhooks/1.0" \
-d '{
"specversion": "1.0",
"id": "test-event-123",
"source": "accessgrid",
"type": "ag.access_pass.activated",
"datacontenttype": "application/json",
"time": "2025-01-15T12:00:00Z",
"data": {
"access_pass_id": "0xp455-3x1d",
"protocol": "desfire",
"card_number": "12345",
"site_code": "100",
"file_data": "0A1B2C3D4E5F",
"device": {
"type": "iphone",
"id": "device-hash-id"
}
}
}'
require 'sinatra'
require 'json'
# Webhook endpoint to receive AccessGrid events
post '/webhooks' do
request.body.rewind
payload = JSON.parse(request.body.read)
# Verify it's a CloudEvents payload
unless payload['specversion'] == '1.0'
halt 400, { error: 'Invalid CloudEvents format' }.to_json
end
# Handle different event types
case payload['type']
when 'ag.access_pass.issued'
handle_access_pass_issued(payload['data'])
when 'ag.access_pass.activated'
handle_access_pass_activated(payload['data'])
when 'ag.card_template.published'
handle_template_published(payload['data'])
else
puts "Unknown event type: #{payload['type']}"
end
# Always return 200 to acknowledge receipt
status 200
{ received: true }.to_json
end
def handle_access_pass_issued(data)
puts "Access pass issued: #{data['access_pass_id']}"
# Your custom logic here
end
def handle_access_pass_activated(data)
puts "Access pass activated: #{data['access_pass_id']}"
puts "Device: #{data['device']['type']}"
# Your custom logic here
end
def handle_template_published(data)
puts "Template published: #{data['card_template_id']}"
# Your custom logic here
end
const express = require('express');
const app = express();
app.use(express.json({
type: ['application/json', 'application/cloudevents+json']
}));
// Webhook endpoint to receive AccessGrid events
app.post('/webhooks', (req, res) => {
const payload = req.body;
// Verify it's a CloudEvents payload
if (payload.specversion !== '1.0') {
return res.status(400).json({ error: 'Invalid CloudEvents format' });
}
// Handle different event types
switch (payload.type) {
case 'ag.access_pass.issued':
handleAccessPassIssued(payload.data);
break;
case 'ag.access_pass.activated':
handleAccessPassActivated(payload.data);
break;
case 'ag.card_template.published':
handleTemplatePublished(payload.data);
break;
default:
console.log(`Unknown event type: ${payload.type}`);
}
// Always return 200 to acknowledge receipt
res.status(200).json({ received: true });
});
function handleAccessPassIssued(data) {
console.log(`Access pass issued: ${data.access_pass_id}`);
// Your custom logic here
}
function handleAccessPassActivated(data) {
console.log(`Access pass activated: ${data.access_pass_id}`);
console.log(`Device: ${data.device.type}`);
// Your custom logic here
}
function handleTemplatePublished(data) {
console.log(`Template published: ${data.card_template_id}`);
// Your custom logic here
}
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks', methods=['POST'])
def webhook():
payload = request.get_json()
# Verify it's a CloudEvents payload
if payload.get('specversion') != '1.0':
return jsonify({'error': 'Invalid CloudEvents format'}), 400
# Handle different event types
event_type = payload.get('type')
data = payload.get('data', {})
if event_type == 'ag.access_pass.issued':
handle_access_pass_issued(data)
elif event_type == 'ag.access_pass.activated':
handle_access_pass_activated(data)
elif event_type == 'ag.card_template.published':
handle_template_published(data)
else:
print(f"Unknown event type: {event_type}")
# Always return 200 to acknowledge receipt
return jsonify({'received': True}), 200
def handle_access_pass_issued(data):
print(f"Access pass issued: {data['access_pass_id']}")
# Your custom logic here
def handle_access_pass_activated(data):
print(f"Access pass activated: {data['access_pass_id']}")
print(f"Device: {data['device']['type']}")
# Your custom logic here
def handle_template_published(data):
print(f"Template published: {data['card_template_id']}")
# Your custom logic here
if __name__ == '__main__':
app.run(port=3000)
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
type CloudEvent struct {
SpecVersion string `json:"specversion"`
ID string `json:"id"`
Source string `json:"source"`
Type string `json:"type"`
Time string `json:"time"`
Data map[string]interface{} `json:"data"`
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
var event CloudEvent
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Verify it's a CloudEvents payload
if event.SpecVersion != "1.0" {
http.Error(w, "Invalid CloudEvents format", http.StatusBadRequest)
return
}
// Handle different event types
switch event.Type {
case "ag.access_pass.issued":
handleAccessPassIssued(event.Data)
case "ag.access_pass.activated":
handleAccessPassActivated(event.Data)
case "ag.card_template.published":
handleTemplatePublished(event.Data)
default:
log.Printf("Unknown event type: %s", event.Type)
}
// Always return 200 to acknowledge receipt
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"received": true})
}
func handleAccessPassIssued(data map[string]interface{}) {
fmt.Printf("Access pass issued: %v\n", data["access_pass_id"])
// Your custom logic here
}
func handleAccessPassActivated(data map[string]interface{}) {
fmt.Printf("Access pass activated: %v\n", data["access_pass_id"])
if device, ok := data["device"].(map[string]interface{}); ok {
fmt.Printf("Device: %v\n", device["type"])
}
// Your custom logic here
}
func handleTemplatePublished(data map[string]interface{}) {
fmt.Printf("Template published: %v\n", data["card_template_id"])
// Your custom logic here
}
func main() {
http.HandleFunc("/webhooks", webhookHandler)
log.Println("Webhook server listening on port 3000")
log.Fatal(http.ListenAndServe(":3000", nil))
}
using System;
using System.IO;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/webhooks", async (HttpContext context) =>
{
using var reader = new StreamReader(context.Request.Body);
var body = await reader.ReadToEndAsync();
var payload = JsonSerializer.Deserialize<CloudEvent>(body);
// Verify it's a CloudEvents payload
if (payload?.SpecVersion != "1.0")
{
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(new { error = "Invalid CloudEvents format" });
return;
}
// Handle different event types
switch (payload.Type)
{
case "ag.access_pass.issued":
HandleAccessPassIssued(payload.Data);
break;
case "ag.access_pass.activated":
HandleAccessPassActivated(payload.Data);
break;
case "ag.card_template.published":
HandleTemplatePublished(payload.Data);
break;
default:
Console.WriteLine($"Unknown event type: {payload.Type}");
break;
}
// Always return 200 to acknowledge receipt
await context.Response.WriteAsJsonAsync(new { received = true });
});
void HandleAccessPassIssued(JsonElement data)
{
Console.WriteLine($"Access pass issued: {data.GetProperty("access_pass_id").GetString()}");
// Your custom logic here
}
void HandleAccessPassActivated(JsonElement data)
{
Console.WriteLine($"Access pass activated: {data.GetProperty("access_pass_id").GetString()}");
if (data.TryGetProperty("device", out var device))
{
Console.WriteLine($"Device: {device.GetProperty("type").GetString()}");
}
// Your custom logic here
}
void HandleTemplatePublished(JsonElement data)
{
Console.WriteLine($"Template published: {data.GetProperty("card_template_id").GetString()}");
// Your custom logic here
}
app.Run("http://localhost:3000");
record CloudEvent(string SpecVersion, string Id, string Source, string Type, string Time, JsonElement Data);
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.io.*;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
public class WebhookServer {
private static final Gson gson = new Gson();
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0);
server.createContext("/webhooks", new WebhookHandler());
server.start();
System.out.println("Webhook server listening on port 3000");
}
static class WebhookHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
if (!"POST".equals(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(405, 0);
exchange.close();
return;
}
InputStream is = exchange.getRequestBody();
String body = new String(is.readAllBytes(), StandardCharsets.UTF_8);
JsonObject payload = gson.fromJson(body, JsonObject.class);
// Verify it's a CloudEvents payload
if (!"1.0".equals(payload.get("specversion").getAsString())) {
String response = "{\"error\": \"Invalid CloudEvents format\"}";
exchange.sendResponseHeaders(400, response.length());
OutputStream os = exchange.getResponseBody();
os.write(response.getBytes());
os.close();
return;
}
// Handle different event types
String type = payload.get("type").getAsString();
JsonObject data = payload.getAsJsonObject("data");
switch (type) {
case "ag.access_pass.issued":
handleAccessPassIssued(data);
break;
case "ag.access_pass.activated":
handleAccessPassActivated(data);
break;
case "ag.card_template.published":
handleTemplatePublished(data);
break;
default:
System.out.println("Unknown event type: " + type);
}
// Always return 200 to acknowledge receipt
String response = "{\"received\": true}";
exchange.sendResponseHeaders(200, response.length());
OutputStream os = exchange.getResponseBody();
os.write(response.getBytes());
os.close();
}
private void handleAccessPassIssued(JsonObject data) {
System.out.println("Access pass issued: " + data.get("access_pass_id").getAsString());
// Your custom logic here
}
private void handleAccessPassActivated(JsonObject data) {
System.out.println("Access pass activated: " + data.get("access_pass_id").getAsString());
if (data.has("device")) {
JsonObject device = data.getAsJsonObject("device");
System.out.println("Device: " + device.get("type").getAsString());
}
// Your custom logic here
}
private void handleTemplatePublished(JsonObject data) {
System.out.println("Template published: " + data.get("card_template_id").getAsString());
// Your custom logic here
}
}
}
<?php
require 'vendor/autoload.php';
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
$app = AppFactory::create();
$app->addBodyParsingMiddleware();
$app->post('/webhooks', function (Request $request, Response $response) {
$payload = $request->getParsedBody();
// Verify it's a CloudEvents payload
if (!isset($payload['specversion']) || $payload['specversion'] !== '1.0') {
$response->getBody()->write(json_encode(['error' => 'Invalid CloudEvents format']));
return $response->withStatus(400)->withHeader('Content-Type', 'application/json');
}
// Handle different event types
$type = $payload['type'] ?? '';
$data = $payload['data'] ?? [];
switch ($type) {
case 'ag.access_pass.issued':
handleAccessPassIssued($data);
break;
case 'ag.access_pass.activated':
handleAccessPassActivated($data);
break;
case 'ag.card_template.published':
handleTemplatePublished($data);
break;
default:
error_log("Unknown event type: $type");
}
// Always return 200 to acknowledge receipt
$response->getBody()->write(json_encode(['received' => true]));
return $response->withHeader('Content-Type', 'application/json');
});
function handleAccessPassIssued($data) {
error_log("Access pass issued: {$data['access_pass_id']}");
// Your custom logic here
}
function handleAccessPassActivated($data) {
error_log("Access pass activated: {$data['access_pass_id']}");
if (isset($data['device'])) {
error_log("Device: {$data['device']['type']}");
}
// Your custom logic here
}
function handleTemplatePublished($data) {
error_log("Template published: {$data['card_template_id']}");
// Your custom logic here
}
$app->run();
Response
Empty
List Webhooks
Retrieve a paginated list of webhooks configured for your account.
page
nullable integer
Page number (default: 1)
per_page
nullable integer
Results per page (default: 50, max: 100)
Request
curl -X GET \
-H "X-ACCT-ID: $ACCOUNT_ID" \
-H "X-PAYLOAD-SIG: $SIG" \
https://api.accessgrid.com/v1/console/webhooks
require 'accessgrid'
account_id = ENV['ACCOUNT_ID']
secret_key = ENV['SECRET_KEY']
client = AccessGrid::Client.new(account_id, secret_key)
webhooks = client.console.webhooks.list
webhooks.each do |webhook|
puts "ID: #{webhook.id}, Name: #{webhook.name}"
end
Response
Empty
Create Webhook
Create a new webhook to receive event notifications. URL must be reachable and at least one event must be subscribed.
name
string
Webhook name
url
string
HTTPS endpoint URL
auth_method
nullable string
'bearer_token' or 'mtls' (default: 'bearer_token')
subscribed_events
array
Event names (e.g., 'ag.access_pass.issued')
Request
curl -X POST \
-H "X-ACCT-ID: $ACCOUNT_ID" \
-H "X-PAYLOAD-SIG: $SIG" \
-H "Content-Type: application/json" \
-d '{"name":"Production","url":"https://example.com/webhooks","subscribed_events":["ag.access_pass.issued"]}' \
https://api.accessgrid.com/v1/console/webhooks
require 'accessgrid'
account_id = ENV['ACCOUNT_ID']
secret_key = ENV['SECRET_KEY']
client = AccessGrid::Client.new(account_id, secret_key)
webhook = client.console.webhooks.create(
name: 'Production',
url: 'https://example.com/webhooks',
subscribed_events: ['ag.access_pass.issued']
)
puts "Webhook created: #{webhook.id}"
puts "Private key: #{webhook.private_key}"
Response
Empty
Delete Webhook
Delete a webhook by ID.
webhook_id
string
Webhook ID to delete
Request
curl -X DELETE \
-H "X-ACCT-ID: $ACCOUNT_ID" \
-H "X-PAYLOAD-SIG: $SIG" \
https://api.accessgrid.com/v1/console/webhooks/abc123
require 'accessgrid'
account_id = ENV['ACCOUNT_ID']
secret_key = ENV['SECRET_KEY']
client = AccessGrid::Client.new(account_id, secret_key)
client.console.webhooks.delete('abc123')
puts "Webhook deleted"
Response
Empty