Tiqets Product Change Notification Webhooks - Implementation Plan¶
Overview¶
Implement webhook support for Tiqets product change notifications to enable real-time product updates instead of periodic full catalog refreshes. This reduces API calls, improves data freshness, and enables immediate response to product availability changes.
Source Documentation: Tiqets Webhooks Guide
Webhook Event Types (from Tiqets API)¶
| Event Type | Description | Action Required |
|---|---|---|
checkout_disabled |
Product becomes unavailable | Mark product inactive, store reason & expected reopen |
checkout_enabled |
Product becomes available | Mark product active |
product_details_update |
Content/pricing changed | Refresh specific product from API |
Architecture¶
Tiqets Server
↓ POST (JSON payload)
TiqetsWebhookApi.java (JAX-RS endpoint in tqapi)
↓ IP validation
TiqetsWebhookService.java (tqtiqets)
↓ Parse & dispatch
WebhookNotificationHandler.java
↓ Handle each change type
ProductEntity / TiqetsCatalogFacade
↓ Database updates or API refresh
Part 1: Database Schema¶
1.1 Webhook Subscription Table¶
CREATE TABLE IF NOT EXISTS tiqets.webhook_subscription (
id INTEGER PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
subscription_id VARCHAR(50) NOT NULL UNIQUE,
subscription_type VARCHAR(30) NOT NULL, -- product_ids, experience_ids, products_i_sell
entity_ids TEXT, -- comma-separated IDs (null for products_i_sell)
change_types VARCHAR(255) NOT NULL, -- comma-separated: checkout_disabled,checkout_enabled,product_details_update
callback_url VARCHAR(500) NOT NULL,
status VARCHAR(20) DEFAULT 'active', -- active, degraded, cancelled
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
1.2 Product Status Fields (extend existing product table)¶
ALTER TABLE tiqets.product ADD COLUMN IF NOT EXISTS checkout_enabled BOOLEAN DEFAULT true;
ALTER TABLE tiqets.product ADD COLUMN IF NOT EXISTS checkout_disabled_reason VARCHAR(100);
ALTER TABLE tiqets.product ADD COLUMN IF NOT EXISTS expected_reopen_date TIMESTAMP;
ALTER TABLE tiqets.product ADD COLUMN IF NOT EXISTS alternative_product_id VARCHAR(30);
ALTER TABLE tiqets.product ADD COLUMN IF NOT EXISTS last_webhook_update TIMESTAMP;
1.3 Webhook Notification Log (optional, for debugging)¶
CREATE TABLE IF NOT EXISTS tiqets.webhook_notification_log (
id INTEGER PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
subscription_id VARCHAR(50),
received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
payload_json TEXT,
processing_status VARCHAR(20), -- processed, failed
error_message TEXT
);
CREATE INDEX IF NOT EXISTS idx_webhook_log_received ON tiqets.webhook_notification_log(received_at);
Part 2: New Files to Create¶
tqapi Module¶
| File | Purpose |
|---|---|
TiqetsWebhookApi.java |
REST endpoint to receive webhook callbacks |
TiqetsWebhookIpFilter.java |
IP whitelist filter for security |
tqtiqets Module¶
| File | Purpose |
|---|---|
WebhookSubscriptionEntity.java |
JPA entity for subscriptions |
WebhookNotificationLogEntity.java |
JPA entity for notification log |
TiqetsWebhookService.java |
Core webhook handling logic |
WebhookSubscriptionService.java |
Manage subscriptions via Tiqets API |
dto/WebhookPayload.java |
DTO for incoming webhook JSON |
dto/ProductChange.java |
DTO for individual product changes |
Part 3: Implementation Details¶
3.1 TiqetsWebhookApi.java (tqapi)¶
@Path("/tiqets/webhook")
public class TiqetsWebhookApi {
@POST
@Path("/notify")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response receiveNotification(String payload, @Context HttpServletRequest request) {
// 1. Validate source IP against whitelist
// 2. Parse webhook payload
// 3. Dispatch to TiqetsWebhookService
// 4. Return HTTP 200 (required by Tiqets)
return Response.ok().build();
}
}
3.2 IP Whitelist Filter¶
Sandbox IPs: 99.80.191.99, 63.35.71.28, 63.35.56.49
Production IPs: 99.81.16.132, 54.77.55.83, 34.246.210.158
public class TiqetsWebhookIpFilter {
private static final Set<String> ALLOWED_IPS = Set.of(
// Sandbox
"99.80.191.99", "63.35.71.28", "63.35.56.49",
// Production
"99.81.16.132", "54.77.55.83", "34.246.210.158"
);
public boolean isAllowed(String remoteIp) {
return ALLOWED_IPS.contains(remoteIp);
}
}
3.3 Webhook Payload DTO¶
public class WebhookPayload {
private String subscriptionId;
private List<ProductNotification> products;
public static class ProductNotification {
private Integer productId;
private List<ProductChange> changes;
}
public static class ProductChange {
private String changeType; // checkout_disabled, checkout_enabled, product_details_update
private String reasonCode; // For checkout_disabled
private String expectedReopening; // ISO date string
private Integer recommendedAlternativeProductId;
private String fieldName; // For product_details_update
private String type; // added, removed, changed
private String oldValue;
private String newValue;
}
}
3.4 TiqetsWebhookService.java¶
public class TiqetsWebhookService {
public void processNotification(WebhookPayload payload) {
for (ProductNotification product : payload.getProducts()) {
for (ProductChange change : product.getChanges()) {
switch (change.getChangeType()) {
case "checkout_disabled":
handleCheckoutDisabled(product.getProductId(), change);
break;
case "checkout_enabled":
handleCheckoutEnabled(product.getProductId());
break;
case "product_details_update":
handleProductUpdate(product.getProductId(), change);
break;
}
}
}
}
private void handleCheckoutDisabled(Integer productId, ProductChange change) {
// Update ProductEntity: set checkoutEnabled=false, store reason, expected reopen, alternative
try (Session session = TiqetsDBSession.getSession()) {
ProductEntity product = findBySupplierProductId(session, productId.toString());
if (product != null) {
product.setCheckoutEnabled(false);
product.setCheckoutDisabledReason(change.getReasonCode());
product.setExpectedReopenDate(parseDate(change.getExpectedReopening()));
product.setAlternativeProductId(
change.getRecommendedAlternativeProductId() != null
? change.getRecommendedAlternativeProductId().toString()
: null
);
product.setLastWebhookUpdate(new Date());
session.merge(product);
}
}
}
private void handleCheckoutEnabled(Integer productId) {
// Update ProductEntity: set checkoutEnabled=true, clear disabled fields
try (Session session = TiqetsDBSession.getSession()) {
ProductEntity product = findBySupplierProductId(session, productId.toString());
if (product != null) {
product.setCheckoutEnabled(true);
product.setCheckoutDisabledReason(null);
product.setExpectedReopenDate(null);
product.setAlternativeProductId(null);
product.setLastWebhookUpdate(new Date());
session.merge(product);
}
}
}
private void handleProductUpdate(Integer productId, ProductChange change) {
// Trigger targeted product refresh from Tiqets API
TiqetsCatalogFacade.instance().refreshProduct(productId.toString());
}
}
3.5 WebhookSubscriptionService.java¶
public class WebhookSubscriptionService {
private final TiqetsHttpClient httpClient;
/**
* Create or update a webhook subscription via Tiqets API
* PUT /v2/product-change-notification/{subscription_id}
*/
public void createSubscription(String subscriptionId, String subscriptionType,
List<String> entityIds, List<String> changeTypes,
String callbackUrl) {
Map<String, Object> request = new HashMap<>();
request.put("subscription_type", subscriptionType);
if (entityIds != null && !entityIds.isEmpty()) {
request.put("entity_ids", entityIds.stream().map(Integer::parseInt).toList());
}
request.put("change_types", changeTypes);
request.put("callback_url", callbackUrl);
httpClient.put("/v2/product-change-notification/" + subscriptionId, request);
// Save to local database
saveSubscription(subscriptionId, subscriptionType, entityIds, changeTypes, callbackUrl);
}
/**
* Get subscription status
* GET /v2/product-change-notification/{subscription_id}
*/
public WebhookSubscriptionEntity getSubscription(String subscriptionId) {
// Fetch from Tiqets API and sync with local DB
}
/**
* Delete subscription
* DELETE /v2/product-change-notification/{subscription_id}
*/
public void deleteSubscription(String subscriptionId) {
httpClient.delete("/v2/product-change-notification/" + subscriptionId);
// Remove from local database
}
}
Part 4: Configuration Updates¶
4.1 tiqets-client.xml¶
<!-- Webhook configuration -->
<property name="tiqets.webhook.callback.url"
value="https://api.yourdomain.com/tlinq-api/tiqets/webhook/notify"/>
<property name="tiqets.webhook.environment" value="production"/> <!-- sandbox or production -->
4.2 api-roles.properties¶
# Webhook endpoint - allow from any source (IP filter handles security)
tiqets/webhook/notify=guest
# Subscription management endpoints - admin only
tiqets/webhook/subscription/create=admin
tiqets/webhook/subscription/list=admin
tiqets/webhook/subscription/delete=admin
4.3 TiqetsDBSession.java Update¶
// Add new entity classes
configuration.addAnnotatedClass(WebhookSubscriptionEntity.class);
configuration.addAnnotatedClass(WebhookNotificationLogEntity.class);
Part 5: Files to Modify¶
| File | Changes |
|---|---|
ProductEntity.java |
Add checkout status fields (checkoutEnabled, reason, expectedReopen, alternative) |
TqProduct.java |
Add corresponding native entity fields |
CProduct.java |
Add canonical fields for checkout status |
TiqetsCatalogFacade.java |
Add refreshProduct(productId) method for targeted refresh |
TiqetsDBSession.java |
Register new webhook entities |
TiqetsHttpClient.java |
Add PUT and DELETE methods for subscription management |
config/db/tiqets-schema.sql |
Add webhook tables and product columns |
config/api-roles.properties |
Add webhook endpoint permissions |
Part 6: Admin API Endpoints (Optional)¶
@Path("/tiqets/webhook")
public class TiqetsWebhookApi {
@POST
@Path("/subscription/create")
public Response createSubscription(Map<String, Object> reqData) {
// Admin endpoint to create new webhook subscription
}
@POST
@Path("/subscription/list")
public Response listSubscriptions(Map<String, Object> reqData) {
// List all active subscriptions
}
@POST
@Path("/subscription/delete")
public Response deleteSubscription(Map<String, Object> reqData) {
// Delete a subscription
}
}
Part 7: Implementation Order¶
Phase 1: Database Schema¶
- Add new columns to
tiqets.producttable - Create
webhook_subscriptiontable - Create
webhook_notification_logtable (optional)
Phase 2: Core Webhook Handler¶
- Create
WebhookPayload.javaandProductChange.javaDTOs - Create
TiqetsWebhookService.javawith change handlers - Update
ProductEntity.javawith new fields - Create
TiqetsWebhookIpFilter.java
Phase 3: REST Endpoint¶
- Create
TiqetsWebhookApi.javawith/notifyendpoint - Configure IP filtering
- Update
api-roles.properties
Phase 4: Subscription Management¶
- Create
WebhookSubscriptionEntity.java - Create
WebhookSubscriptionService.java - Add PUT/DELETE methods to
TiqetsHttpClient.java - Add admin endpoints for subscription management
Phase 5: Integration¶
- Update
TiqetsDBSession.javato register new entities - Update
TiqetsCatalogFacade.javawithrefreshProduct()method - Add configuration properties
Phase 6: Testing¶
- Unit tests for webhook payload parsing
- Integration tests for database updates
- End-to-end test with mock webhook calls
Verification Steps¶
1. Database Migration¶
2. Build¶
3. Test Webhook Endpoint¶
curl -X POST http://localhost:11080/tlinq-api/tiqets/webhook/notify \
-H "Content-Type: application/json" \
-d '{
"subscription_id": "test-sub-1",
"products": [{
"product_id": 12345,
"changes": [{
"change_type": "checkout_disabled",
"reason_code": "sold_out",
"expected_reopening": "2024-02-01"
}]
}]
}'
4. Verify Product Update¶
SELECT supplier_product_id, title, checkout_enabled,
checkout_disabled_reason, expected_reopen_date, last_webhook_update
FROM tiqets.product
WHERE supplier_product_id = '12345';
5. Create Subscription (after deployment)¶
curl -X POST http://localhost:11080/tlinq-api/tiqets/webhook/subscription/create \
-H "Content-Type: application/json" \
-d '{
"subscriptionId": "tqpro-uae-products",
"subscriptionType": "products_i_sell",
"changeTypes": ["checkout_disabled", "checkout_enabled", "product_details_update"],
"callbackUrl": "https://api.yourdomain.com/tlinq-api/tiqets/webhook/notify"
}'
Security Considerations¶
- IP Whitelisting: Only accept requests from Tiqets IPs (primary security)
- Hard-to-guess URL: Callback URL should not be easily guessable
- URL Rotation: Consider periodic rotation of callback URL
- Logging: Log all incoming webhooks for audit trail
- Rate Limiting: Protect against potential abuse even from valid IPs
Benefits¶
- Reduced API Calls: No need for frequent full catalog refreshes
- Real-time Updates: Products disabled/enabled immediately
- Better UX: Users won't see unavailable products
- Cost Savings: Fewer API calls to Tiqets
- Data Freshness: Product details always current
Risk Considerations¶
- Webhook Delivery Failure: Tiqets retries, but may eventually cancel subscription
- Processing Errors: Log failures, implement retry mechanism for DB updates
- IP Changes: Monitor Tiqets documentation for IP updates
- Endpoint Availability: Ensure webhook endpoint is highly available