Documentation Index
Fetch the complete documentation index at: https://atp.hypertext.studio/llms.txt
Use this file to discover all available pages before exploring further.
Overview
When a user submits a response to a notification, the ATP server delivers it to the originating service via a webhook callback to the URL specified during service registration. This page explains how to properly receive and handle these webhook callbacks.
HTTP Method: POST
URL: Service’s registered callback URL
Headers:
Content-Type: application/json
X-ATP-Signature: t=1622145123,v1=5257a869e7ecebb... (HMAC signature for verification)
X-ATP-Request-ID: req_abc123def456 (Unique request identifier)
Webhook Payload
{
"notification_id": "550e8400-e29b-41d4-a716-446655440000",
"action_id": "approve",
"response_data": null,
"responded_at": "2025-05-25T10:35:12Z",
"responder": {
"id": "user_123",
"type": "human"
}
}
| Field | Type | Description |
|---|
notification_id | string | UUID of the notification being answered |
action_id | string | Selected action identifier from the notification |
response_data | any | User input data, type determined by action’s response_type |
responded_at | datetime | ISO 8601 timestamp of response submission |
responder | object | Object containing responder identification |
The responder object contains:
| Field | Type | Description |
|---|
id | string | Unique identifier of responding entity |
type | string | Either “human” or “agent” to indicate responder type |
Signature Verification
The ATP server signs each webhook payload using the webhook_secret provided during service registration. You must verify this signature before processing the webhook to ensure it came from the ATP server.
The signature is included in the X-ATP-Signature header with this format:
X-ATP-Signature: t=1622145123,v1=5257a869e7ecebb07c954661a7577d13c99705955e814274785a3174a535d5c7
Where:
t is the timestamp when the signature was generated
v1 indicates the signature algorithm version
- The value after
v1= is the hexadecimal representation of the HMAC-SHA256 signature
Signature Verification Example (Node.js)
const crypto = require('crypto');
function verifySignature(body, signature, secret) {
// Extract timestamp and signature from header
const [timeComponent, signatureComponent] = signature.split(',');
const timestamp = timeComponent.split('=')[1];
const signatureValue = signatureComponent.split('=')[1];
// Check if timestamp is within acceptable range (5 minutes)
const currentTime = Math.floor(Date.now() / 1000);
if (currentTime - parseInt(timestamp) > 300) {
return false; // Signature too old, possible replay attack
}
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
// Compare signatures using constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(signatureValue, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
Signature Verification Example (Python)
import hmac
import hashlib
import time
def verify_signature(payload_body, signature_header, secret):
# Parse the signature header
header_parts = signature_header.split(',')
timestamp_str = header_parts[0].split('=')[1]
signature = header_parts[1].split('=')[1]
# Check if signature is recent (within 5 minutes)
timestamp = int(timestamp_str)
current_time = int(time.time())
if current_time - timestamp > 300:
return False # Signature too old
# Compute expected signature
expected_signature = hmac.new(
secret.encode('utf-8'),
payload_body.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Compare signatures using constant-time comparison
return hmac.compare_digest(signature, expected_signature)
Webhook Response
Services must respond to webhook callbacks with an appropriate HTTP status code:
- 200 OK: The webhook was processed successfully
- 400 Bad Request: The webhook payload was invalid
- 401 Unauthorized: Signature verification failed
- 500 Internal Server Error: Service encountered an error processing the webhook
If you return any status code other than 200, the ATP server will retry the delivery with exponential backoff according to this schedule:
- Initial retry after 30 seconds
- Second retry after 2 minutes
- Third retry after 10 minutes
- Fourth retry after 30 minutes
- Fifth retry after 1 hour
After the fifth retry, the ATP server will mark the webhook delivery as failed.
Custom Error Responses
If your service encounters an error processing the webhook, you can return a structured error response to provide more information:
{
"code": "RESOURCE_LOCKED",
"message": "Cannot apply changes because resource is currently locked",
"user_message": "The system is currently processing another change. Please try again in a few moments.",
"retriable": true
}
| Field | Type | Description |
|---|
code | string | Service-specific error identifier |
message | string | Technical error description for logging |
user_message | string | Human-readable message for potential user display |
retriable | boolean | Indicates whether retry attempts may succeed |
Webhook Handler Implementation
Express.js Example
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = process.env.ATP_WEBHOOK_SECRET;
// Use raw body parser to get the raw request body for signature verification
app.use(bodyParser.json({
verify: (req, res, buf) => {
req.rawBody = buf;
}
}));
app.post('/atp/webhook', async (req, res) => {
// Verify signature
const signature = req.headers['x-atp-signature'];
if (!signature) {
return res.status(401).json({ error: 'Missing signature header' });
}
if (!verifySignature(req.rawBody, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { notification_id, action_id, response_data, responder } = req.body;
try {
// Process the response based on action type
switch (action_id) {
case 'approve_deployment':
await triggerDeployment(notification_id);
break;
case 'reject_deployment':
await cancelDeployment(notification_id, response_data);
break;
default:
console.log(`Unknown action_id: ${action_id}`);
}
// Respond with success
res.status(200).json({ status: 'processed' });
} catch (error) {
console.error('Error processing webhook:', error);
// Determine if this error is retriable
const retriable = error.code !== 'PERMANENT_FAILURE';
// Respond with error
res.status(500).json({
code: error.code || 'INTERNAL_ERROR',
message: error.message,
user_message: 'The system encountered an error processing your response.',
retriable
});
}
});
app.listen(3000, () => console.log('Webhook server running on port 3000'));
Flask Example
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import os
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('ATP_WEBHOOK_SECRET')
def verify_signature(payload_body, signature_header):
# Parse the signature header
header_parts = signature_header.split(',')
timestamp_str = header_parts[0].split('=')[1]
signature = header_parts[1].split('=')[1]
# Compute expected signature
expected_signature = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
payload_body,
hashlib.sha256
).hexdigest()
# Compare signatures using constant-time comparison
return hmac.compare_digest(signature, expected_signature)
@app.route('/atp/webhook', methods=['POST'])
def handle_webhook():
# Get the raw request body for signature verification
payload_body = request.data
# Verify signature
signature = request.headers.get('X-ATP-Signature')
if not signature:
return jsonify({"error": "Missing signature header"}), 401
if not verify_signature(payload_body, signature):
return jsonify({"error": "Invalid signature"}), 401
# Parse the payload
data = request.json
notification_id = data.get('notification_id')
action_id = data.get('action_id')
response_data = data.get('response_data')
responder = data.get('responder')
try:
# Process the response based on action type
if action_id == 'approve_deployment':
# Trigger deployment
trigger_deployment(notification_id)
elif action_id == 'reject_deployment':
# Cancel deployment with reason
cancel_deployment(notification_id, response_data)
else:
print(f"Unknown action_id: {action_id}")
# Respond with success
return jsonify({"status": "processed"}), 200
except Exception as e:
# Determine if this error is retriable
retriable = getattr(e, 'retriable', True)
# Respond with error
return jsonify({
"code": getattr(e, 'code', 'INTERNAL_ERROR'),
"message": str(e),
"user_message": "The system encountered an error processing your response.",
"retriable": retriable
}), 500
if __name__ == '__main__':
app.run(port=3000)
Best Practices
- Always verify signatures: Never process webhooks without verifying the signature
- Respond quickly: Process webhooks asynchronously and respond within 5 seconds
- Implement idempotency: Handle duplicate webhook deliveries gracefully
- Log everything: Keep detailed logs of webhook requests and responses
- Implement error handling: Return appropriate status codes and error messages
- Use HTTPS: Ensure your webhook endpoint uses HTTPS for secure communication
- Monitor webhook failures: Set up alerting for repeated webhook failures