Skip to content

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

  1. Add new columns to tiqets.product table
  2. Create webhook_subscription table
  3. Create webhook_notification_log table (optional)

Phase 2: Core Webhook Handler

  1. Create WebhookPayload.java and ProductChange.java DTOs
  2. Create TiqetsWebhookService.java with change handlers
  3. Update ProductEntity.java with new fields
  4. Create TiqetsWebhookIpFilter.java

Phase 3: REST Endpoint

  1. Create TiqetsWebhookApi.java with /notify endpoint
  2. Configure IP filtering
  3. Update api-roles.properties

Phase 4: Subscription Management

  1. Create WebhookSubscriptionEntity.java
  2. Create WebhookSubscriptionService.java
  3. Add PUT/DELETE methods to TiqetsHttpClient.java
  4. Add admin endpoints for subscription management

Phase 5: Integration

  1. Update TiqetsDBSession.java to register new entities
  2. Update TiqetsCatalogFacade.java with refreshProduct() method
  3. Add configuration properties

Phase 6: Testing

  1. Unit tests for webhook payload parsing
  2. Integration tests for database updates
  3. End-to-end test with mock webhook calls

Verification Steps

1. Database Migration

psql -d tlinq -f config/db/tiqets-schema.sql

2. Build

./gradlew :tqtiqets:build :tqapi: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

  1. IP Whitelisting: Only accept requests from Tiqets IPs (primary security)
  2. Hard-to-guess URL: Callback URL should not be easily guessable
  3. URL Rotation: Consider periodic rotation of callback URL
  4. Logging: Log all incoming webhooks for audit trail
  5. Rate Limiting: Protect against potential abuse even from valid IPs

Benefits

  1. Reduced API Calls: No need for frequent full catalog refreshes
  2. Real-time Updates: Products disabled/enabled immediately
  3. Better UX: Users won't see unavailable products
  4. Cost Savings: Fewer API calls to Tiqets
  5. Data Freshness: Product details always current

Risk Considerations

  1. Webhook Delivery Failure: Tiqets retries, but may eventually cancel subscription
  2. Processing Errors: Log failures, implement retry mechanism for DB updates
  3. IP Changes: Monitor Tiqets documentation for IP updates
  4. Endpoint Availability: Ensure webhook endpoint is highly available