How to set up webhooks on AccessGrid - AccessGrid Guides

How to set up webhooks on AccessGrid

June 10, 2026

Auston Bunsen

Overview

Webhooks let AccessGrid push real-time event notifications to your own backend the moment something happens — a pass is issued, a card template is published, an account balance drops below threshold, and so on. Instead of polling our API, you register an HTTPS endpoint and we POST a CloudEvents-formatted payload to it as events occur.

This guide walks through creating a webhook, completing the one-time verification handshake, authenticating deliveries, and reading the payloads you'll receive.

Creating a webhook

In the AccessGrid console, open Webhooks from the left navigation and create a new webhook. You'll provide:

Field Description
Name
Copy
A human-readable label (e.g. Production badge sync).
Copy
URL
Copy
The HTTPS endpoint we deliver to. Must be a valid, publicly resolvable http/https URL.
Copy
Authentication
Copy
How deliveries are authenticated — Bearer token (default) or mTLS.
Copy

Once saved, the webhook is created in a Pending state and a Private Key is generated for you. Deliveries do not start flowing yet — the endpoint must first prove it owns the URL by completing the verification handshake described below.

Security note: The private key shown on this screen is the bearer token we'll send with every delivery. Treat it like a password — copy it into your endpoint's configuration and never commit it to source control.

Verifying your endpoint

Before AccessGrid sends any real events, your endpoint must prove ownership by completing a challenge–response handshake. This prevents events from being delivered to a URL you don't actually control. It works in a few steps.

AccessGrid sends a POST to your URL. The challenge token is carried in the X-AccessGrid-Webhook-Challenge header (it's also mirrored in the JSON body for convenience):

POST /your-webhook-endpoint HTTP/1.1

Content-Type: application/json
User-Agent: AccessGrid-Webhooks/1.0
X-AccessGrid-Webhook-Challenge: 9f2c1ab7e4d8...
# the challenge token
{ "challenge": "9f2c1ab7e4d8..." }

Your endpoint must respond with HTTP 200 and echo the token back as JSON:

{ "challenge": "9f2c1ab7e4d8..." }

Here is some sample code on how a verification would work:

// Express
app.post("/your-webhook-endpoint", express.json(), (req, res) => {
// Verification handshake: the challenge token arrives in the request header
const challenge = req.get("X-AccessGrid-Webhook-Challenge");
if (challenge) {
    return res.status(200).json({ challenge });
}
// ...otherwise handle a normal event delivery (see section 4)
res.sendStatus(200);
});

Once we receive a matching echo, the webhook flips to verified and deliveries begin automatically.

SSRF protection

For your safety and ours, the destination URL is resolved and validated to a public IP before any request — including the verification handshake — is sent. URLs that resolve to loopback, RFC 1918 private ranges, link-local, CGNAT, or other reserved ranges are rejected. We also pin the connection to the validated IP to prevent DNS-rebinding.

Testing with webhook.site? That host is trusted and skips the challenge handshake (it still goes through public-IP validation), so webhooks pointed at it verify automatically — handy for quick experiments.

Authenticating deliveries

Every delivery is authenticated so your endpoint can confirm the request genuinely came from AccessGrid. We have two authentication mechanisms: Bearer tokens and mutual TLS.

Bearer token (default)

We send your webhook's private key in the Authorization header:

Authorization: Bearer Fj6cXm13pWZxTPKtfPVMcDddU6joAQVfmMZLUai7uq12uqwvSLsZaapY5HFe1PjE

Your endpoint should compare this against the stored private key using a constant-time comparison and reject anything that doesn't match:

expected = ENV["ACCESSGRID_WEBHOOK_KEY"]
provided = request.headers["Authorization"].to_s.delete_prefix("Bearer ")

unless ActiveSupport::SecurityUtils.secure_compare(provided, expected)
halt 401
end

Mutual TLS

If you select mTLS authentication, AccessGrid presents a client certificate on each delivery, which your server validates at the TLS layer. This is the strongest option for high-security environments. Certificates are issued for you and can be rotated; after a rotation, both the old and new certificate are accepted for a 7-day grace period.

The delivery payload

Deliveries are sent as CloudEvents v1.0 in the JSON format. The headers on every delivery:

POST /your-webhook-endpoint HTTP/1.1

Content-Type: application/cloudevents+json
User-Agent: AccessGrid-Webhooks/1.0
Authorization: Bearer <your-private-key>     # bearer-token webhooks only

The body is a CloudEvents envelope. The standard envelope fields:

Field Description
specversion
Copy
Always "1.0".
Copy
id
Copy
Unique event ID. Use this for idempotency / de-duplication.
Copy
source
Copy
/accessgrid/customer for real events, /accessgrid/test for test sends.
Copy
type
Copy
The event type, e.g. ag.access_pass.issued.
Copy
dataschema
Copy
URI of the JSON schema describing data for this event type and version.
Copy
time
Copy
ISO-8601 timestamp of the event.
Copy
data
Copy
The event-specific payload.
Copy

Example: ag.access_pass.issued

{
"specversion": "1.0",
"id": "a-u_2Ui5g9zA1Q",
"source": "/accessgrid/customer",
"type": "ag.access_pass.issued",
"dataschema": "https://api.accessgrid.com/schemas/access_pass/v1.0.3.json",
"time": "2026-06-10T20:16:29Z",
"data": {
    "id": "access_pass_id",
    "card_template_id": "tpl_123",
    "state": "active",
    "full_name": "John Doe",
    "employee_id": "EMP-001",
    "title": "Engineer",
    "start_date": "2026-01-01T00:00:00Z",
    "expiration_date": "2027-01-01T00:00:00Z",
    "card_templates": [
      {
        "id": "tpl_123",
        "platform": "apple",
        "protocol": "desfire",
        "name": "Employee Badge"
      }
    ]
}
}

Example: ag.landing_page.created

{  "specversion": "1.0",
  "id": "lp_evt_002",
  "source": "/accessgrid/customer",
  "type": "ag.landing_page.created",
  "dataschema": "https://api.accessgrid.com/schemas/landing_page/v1.0.1.json",
  "time": "2026-06-10T12:30:00Z",
  "data": {
      "landing_page_id": "lp_xyz"
  }}

Example: ag.account_balance.low

{  "specversion": "1.0",
  "id": "bal_evt_003",
  "source": "/accessgrid/customer",
  "type": "ag.account_balance.low",
  "dataschema": "https://api.accessgrid.com/schemas/account_balance/v1.0.1.json",
  "time": "2026-06-10T11:00:00Z",
  "data": {
      "account_id": "acct_123",
      "organization_name": "Acme Corp",
      "current_balance": 50.0,
      "threshold": 100.0,
      "amount_below_threshold": 50.0
  }}

The dataschema URI points to the JSON Schema for that event type, so you can validate payloads programmatically and see exactly which fields a given version provides.

Retries and delivery guarantees

A delivery is considered successful when your endpoint returns HTTP 200 or 201. Anything else (or a connection error/timeout) triggers an automatic retry with exponential backoff:

Attempt Delay after previous
1
Copy
immediate
Copy
2
Copy
30 seconds
Copy
3
Copy
2 minutes
Copy
4
Copy
5 minutes
Copy
5
Copy
15 minutes
Copy
6
Copy
30 minutes
Copy
7
Copy
1 hour
Copy

Retries stop once the event is older than the 6-hour retry window, or after the schedule is exhausted. Because retries are expected, your handler should be idempotent — de-duplicate on the CloudEvents id so a redelivered event isn't processed twice.

Every attempt — success or failure — is recorded under Webhook attempts, showing the event type, event ID, response status, and timestamp. Use this to debug what your endpoint returned.

Sending a test event

Once a webhook is Verified, you can fire a synthetic event at it without waiting for real activity. Pick an event from the Select event… dropdown and click Send Test.

Test deliveries:

This is the fastest way to confirm your endpoint parses payloads and returns 200 OK end-to-end before relying on production events.

Summary

That's the full lifecycle of an AccessGrid webhook — create the endpoint, prove ownership through the challenge–response handshake, authenticate each delivery, and parse the CloudEvents payloads as mobile wallet events fire on your account. Before you go live, it's worth running through the essentials one more time:

Once those boxes are checked, your integration will receive events in real time with no polling required.