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
A human-readable label (e.g. Production badge sync).
URL
The HTTPS endpoint we deliver to. Must be a valid, publicly resolvable http/https URL.
Authentication
How deliveries are authenticated — Bearer token (default) or mTLS.
›
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:
// Expressapp.post("/your-webhook-endpoint",express.json(),(req,res)=>{// Verification handshake: the challenge token arrives in the request headerconstchallenge=req.get("X-AccessGrid-Webhook-Challenge");if (challenge){returnres.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.
Other things to know
Tight timeouts. The verification request uses a 2-second connect timeout and a 3-second read timeout, and it does not follow redirects. Your endpoint must respond quickly and directly.
Re-verification on URL change. Changing a webhook's URL clears its verified status and issues a new challenge. The new URL must be verified again before deliveries resume.
Re-verify at any time. If verification didn't complete (endpoint wasn't ready, returned a non-200, etc.), use Resend verification to issue a fresh challenge.
Deliveries while pending. Events that fire while a webhook is still pending are deferred and retried, not dropped — they'll flow once verification succeeds (within the retry window described in section 5).
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:
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
Always "1.0".
id
Unique event ID. Use this for idempotency / de-duplication.
source
/accessgrid/customer for real events, /accessgrid/test for test sends.
type
The event type, e.g. ag.access_pass.issued.
dataschema
URI of the JSON schema describing data for this event type and version.
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.
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
immediate
2
30 seconds
3
2 minutes
4
5 minutes
5
15 minutes
6
30 minutes
7
1 hour
›
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:
Carry `source: "/accessgrid/test"` and a test flag, and are labeled with a Test badge in the attempts list.
Contain realistic synthetic data so you can exercise your parsing logic.
Are sent immediately and inline with no retries — what you see is the single result of that one attempt.
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:
Create the webhook with your HTTPS endpoint and choose Bearer token or mTLS.
Store the private key securely in your endpoint's config.
Implement the challenge handler — read the token from the `X-AccessGrid-Webhook-Challenge` header and echo it back with HTTP 200.
Wait for the status to flip to Verified (or hit Resend verification).
Validate the `Authorization` bearer token (or client cert) on every delivery.
Return `200`/`201` quickly; de-duplicate on the CloudEvents `id`.
Fire a Send Test to confirm the full round-trip.
Once those boxes are checked, your integration will receive events in real time with no polling required.
Have questions or run into something that doesn't behave as documented? Email us at [email protected] — we're happy to help.