Webhooks
Rev79 provides event notifications using webhooks. If you register a HTTP endpoint as a webhook, Rev79 will make requests to that endpoint when events occur. This page explains how to manage webhooks in Rev79, and what events will be notified.
OpenAPI Specification
The complete webhook API specification is available in OpenAPI 3.1.2 format. You can view the interactive documentation at Webhook API Swagger UI.
To access the raw JSON specification for use with tools like typegen, you can download it from /spec/rev79_webhook.json.
Request Structure
When delivering an event notification, Rev79 will provide information about the event in a JSON formatted HTTP body. The following is an example request:
POST https://example.org/your/webhook/endpoint
Content-Type: application/json
Authorization: Bearer <JWT_TOKEN>
{
"webhookId": "79b9cdb3-bd0b-4de6-b86c-b0e50a571477",
"eventId": "90d0a597-af62-4672-8941-ac4bdfb4cc50",
"eventType": "example_event_type",
"eventTime": "2025-08-29T16:50:16+10:00",
"payload": {
"projectReport": {
"id": "396ee1c7-d100-4619-b5ba-596ad7ea031f",
"projectId": "42bd392d-d624-4629-a455-8fc5278d8cb7",
"quarter": "2025-1",
"submitterId": "16e9edf6-1ffe-4137-88ee-0539576ec4af",
"submitterName": "John Smith"
}
}
}
Validating Webhook Requests
When you receive a webhook delivery, you should validate that it came from Rev79 by verifying the JWT signature in the Authorization header.
The JWT token is signed using EdDSA (Ed25519) and contains the following standard claims:
iss(issuer) - Always"https://api.rev79.app"- identifies Rev79 as the token issueraud(audience) - Your webhook endpoint URL - ensures the token is intended for your endpointjti(JWT ID) - Unique identifier for this event (same aseventIdin the body) - prevents replay attacksiat(issued at) - Timestamp when the token was createdexp(expiration) - Expiration timestamp (tokens are short-lived, valid for 5 minutes)
Important Security Notes:
- Verify
audmatches your endpoint URL - This prevents tokens from being reused at different endpoints - Track used
jtivalues - To prevent replay attacks, maintain a list of processedjtivalues and reject duplicates - Check expiration - Tokens expire after 5 minutes, providing a short window for delivery
To verify the JWT signature, you need to:
- Fetch the public keys from Rev79's JWK endpoint:
https://api.rev79.app/.well-known/jwks.json - Use a JWT library to verify the token signature using the public key matching the
kidin the JWT header - Verify the token hasn't expired (
expclaim) - Verify the
audclaim matches your webhook endpoint URL - Verify the
issclaim is"https://api.rev79.app" - Check that you haven't already processed this
jti(to prevent replay attacks)
import jwt
import requests
from jwt import PyJWKClient
# Fetch JWK keys from Rev79
jwks_url = "https://api.rev79.app/.well-known/jwks.json"
jwks_client = PyJWKClient(jwks_url)
# Get the token from the Authorization header
auth_header = request.headers.get('Authorization')
token = auth_header.replace('Bearer ', '')
# Verify the token
try:
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["EdDSA"],
options={"verify_exp": True},
audience=YOUR_WEBHOOK_ENDPOINT_URL, # Verify audience
issuer="https://api.rev79.app" # Verify issuer
)
# Verify this is not a replay attack
jti = payload['jti']
if has_processed_jti(jti): # Check your database/cache
return 401 # Duplicate request
mark_jti_as_processed(jti)
# Token is valid, process the webhook
event_id = payload['jti'] # Same as eventId in body
except jwt.ExpiredSignatureError:
# Token has expired
return 401
except jwt.InvalidTokenError:
# Token is invalid (including aud/iss mismatch)
return 401
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
// Create JWKS client
const client = jwksClient({
jwksUri: 'https://api.rev79.app/.well-known/jwks.json'
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
// Get token from Authorization header
const authHeader = req.headers.authorization;
const token = authHeader.replace('Bearer ', '');
// Verify token
jwt.verify(token, getKey, {
algorithms: ['EdDSA'],
audience: YOUR_WEBHOOK_ENDPOINT_URL, // Verify audience
issuer: 'https://api.rev79.app' // Verify issuer
}, async (err, decoded) => {
if (err) {
// Token is invalid, expired, or aud/iss mismatch
return res.status(401).send('Unauthorized');
}
// Verify this is not a replay attack
const jti = decoded.jti;
if (await hasProcessedJti(jti)) { // Check your database/cache
return res.status(401).send('Duplicate request');
}
await markJtiAsProcessed(jti);
// Token is valid, process the webhook
const eventId = decoded.jti; // Same as eventId in body
});
Events
Webhooks support notification of a number of event types. If you want to be notified about something that is not yet included in this list, reach out the Rev79 development team.
ping
There will be an empty payload for this event.
This event is used during webhook registration to verify the endpoint is working. Your server must return 200 OK for the webhook to be registered. This event can also be sent on demand via a GraphQL mutation, e.g. for testing your server's processing of JWT tokens.
project_report_quarterly_submitted
{
"projectReport": {
"id": "{{uuid}}",
"projectId": "{{uuid}}",
"quarter": "{{quarter}}",
"submitterId": "{{uuid}}",
"submitterName": "{{user name}}"
}
}
project_report_quarterly_approved
{
"projectReport": {
"id": "{{uuid}}",
"projectId": "{{uuid}}",
"quarter": "{{quarter}}",
"approverId": "{{uuid}}",
"approverName": "{{user name}}",
"approvedDate": "YYYY-MM-DD"
}
}
project_report_quarterly_overdue
{
"projectReport": {
"id": "{{uuid}}",
"projectId": "{{uuid}}",
"quarter": "{{quarter}}"
}
}
token_expired
{
"accessTokenId": "{{uuid}}"
}
Registering a webhook
Warning
Webhooks can only be created through the Rev79 GraphQL API. There is currently no user-facing interface for managing webhooks.
Webhooks can be registered using the createWebhook mutation. The endpoint URL will be pinged and a 200 OK HTTP response is required for a prospective webhook to be registered.
createWebhook(
url: "https://example.org/webhook/endpoint",
events: [], # omit for "all events"
organisationIds: ["{{uuid}}"],
projectIds: ["{{uuid}}"],
)
Acknowledging Events
We will handle status codes from your endpoint:
- 2XX
This is a successful delivery, we consider that the event payload was acknowledged and will no longer process it.
- 429
Set this webhook to be inactive, unless the Retry-After header is set. If set, the event will be redelivered after the specified number of seconds or after the specified time, and the webhook will remain active.
- other 4XX
This is your endpoint rejecting our request as invalid. We will not retry to send this payload.
- 5XX
This response code communicates there was an issue on your server. We will retry to call your endpoint again with this payload with an exponential backoff strategy. We will retry after 5 seconds then 10s, 20s, 40s, 80s, 160s, 320s, 640s, 1280s and finally 2560s. If the last delivery fails, we will no longer try to send this payload.
Developing webhooks
If you have a public server that is serving and can handle webhook requests, point the webhooks you create to that.
Developing webhooks invert the standard control expected when developing, tools have been created to help. Github recommends smee-client, but there are many alternatives. Choosing one of these is out of the scope of this document.
The important thing you will need from your proxy is the endpoint URL.
Diagnosing your Rev79 webhook
- Query your webhook through our GraphQL API
- Check that you can receive a ping event from your webhook
- Ensure the events you are expecting belong to the payload organisation or project
Furthermore, you can disable a webhook without deleting it by setting the status enum to disabled when you call the upsert mutation.