Subscription renewal webhook triggers twice for same contract creating billing errors

We’re experiencing critical billing errors because the Subscription API webhook fires two identical renewal events for the same contract within minutes of each other. This causes our billing system to generate duplicate invoices, creating significant reconciliation issues.

Here’s the duplicate payload we receive:

{
  "eventId": "evt_12345",
  "contractId": "CNT-2025-001",
  "eventType": "subscription_renewed"
}

Both webhooks have the same eventId and contractId but arrive 3-5 minutes apart. The webhook idempotency design seems flawed - shouldn’t the eventId prevent duplicate processing? Our event payload design doesn’t include enough metadata to detect duplicates reliably, and the contract renewal workflow appears to be triggering the webhook multiple times. How do we ensure webhook idempotency for subscription events?

Let me provide a comprehensive solution addressing all three aspects of webhook reliability:

Webhook Idempotency: The core issue is that eventId alone isn’t sufficient for idempotency. Implement proper deduplication in your webhook handler:

  1. Create a webhook events table:
CREATE TABLE webhook_events (
  event_id VARCHAR(100) PRIMARY KEY,
  contract_id VARCHAR(50),
  event_type VARCHAR(50),
  processed_at TIMESTAMP,
  business_key VARCHAR(200)
);
  1. Use composite business key for deduplication:
business_key = f"{contract_id}_{event_type}_{renewal_date}"
  1. Check before processing:
if webhook_events.exists(business_key):
    return 200  # Already processed

The eventId is a delivery identifier, not a business event identifier. Multiple deliveries of the same business event will have different eventIds, so you must deduplicate using business context (contract + event + date).

Event Payload Design: Request enhanced payload from Infor OS to include proper deduplication metadata:

  1. Modify webhook subscription to include extended payload:
    • Go to Infor OS Portal > Integration > Webhooks
    • Edit subscription: “Subscription Renewal Events”
    • Enable “Extended Payload” option
    • Add custom fields:
{
  "eventId": "{{event.id}}",
  "eventTimestamp": "{{event.timestamp}}",
  "contractId": "{{contract.id}}",
  "eventType": "{{event.type}}",
  "renewalDate": "{{contract.renewal_date}}",
  "renewalAmount": "{{contract.renewal_amount}}",
  "contractVersion": "{{contract.version}}"
}

The contractVersion field is crucial - it changes only when the contract actually updates, providing a reliable deduplication signal.

  1. Use this enhanced payload for idempotency:
business_key = f"{contractId}_{renewalDate}_{contractVersion}"

Contract Renewal Workflow: Configure the workflow to prevent multiple webhook triggers:

  1. In CloudSuite, navigate to Subscription Management > Workflow Configuration

  2. Edit “Contract Renewal Workflow”

  3. Review webhook trigger points:

    • Renewal Initiated (intermediate state)
    • Renewal Approved (intermediate state)
    • Renewal Completed (final state) ✓
    • Renewal Activated (post-completion state)
  4. Configure webhook to fire ONLY on “Renewal Completed” state

  5. Set webhook event filter:

{
  "event_type": "subscription_renewed",
  "filter": {
    "status": "COMPLETED",
    "exclude_intermediate_states": true
  }
}

Additional Safeguards:

  1. Implement webhook response timeout:
@app.route('/webhook/subscription', methods=['POST'])
def handle_subscription_webhook():
    # Acknowledge immediately
    response = jsonify({"status": "received"})

    # Process asynchronously
    queue.enqueue(process_renewal, request.json)

    return response, 200

Responding within 2-3 seconds prevents retry logic from triggering.

  1. Add webhook signature verification:
def verify_webhook_signature(payload, signature):
    expected = hmac.new(webhook_secret, payload, sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
  1. Implement exponential backoff for processing:
if is_duplicate(business_key):
    logger.info(f"Duplicate webhook ignored: {business_key}")
    return 200  # Still return success to prevent retries

Monitoring and Alerting: Set up monitoring to detect duplicate webhook issues:

  1. Track duplicate rate:
SELECT business_key, COUNT(*) as duplicates
FROM webhook_events
WHERE created_at > NOW() - INTERVAL '1 day'
GROUP BY business_key
HAVING COUNT(*) > 1;
  1. Alert if duplicate rate exceeds threshold (>5% is concerning)

  2. Log all webhook deliveries with eventId and business_key for debugging

Testing Idempotency: Verify your implementation handles duplicates correctly:

  1. Send test webhook twice with same business context
  2. Verify only one invoice is created
  3. Check that second webhook returns 200 but doesn’t process
  4. Confirm webhook_events table shows single processed record

This comprehensive approach ensures reliable webhook processing with proper idempotency, preventing duplicate billing while maintaining compatibility with Infor OS webhook delivery guarantees.


This draft is based on general Infor CloudSuite knowledge. It has not been verified against your specific version and environment. Practitioners: verify the steps and share your experience below.

This is a known issue with webhook delivery guarantees. Most webhook systems use at-least-once delivery, which means duplicates are possible. You need to implement idempotency handling on your receiving end using the eventId as a deduplication key.

I’ve dealt with this exact problem. The issue is that Infor OS webhook delivery has retry logic that sometimes results in duplicate sends, especially during network issues or if your endpoint takes too long to respond. You need to store processed eventIds in your system and check before processing each webhook. Also, respond with 200 OK quickly to prevent retries.

Check the contract renewal workflow configuration in CloudSuite. If the renewal process has multiple steps or approval stages, each stage might be triggering the webhook. You may need to configure the webhook to only fire on the final renewal state, not intermediate states. Also verify that you don’t have multiple webhook subscriptions configured for the same event type - that’s a common cause of duplicates.

We’re already checking eventId for duplicates, but we still occasionally see different eventIds for what appears to be the same renewal. Is there additional metadata in the event payload that we should be using for more reliable deduplication?

Different eventIds for the same renewal suggests multiple webhook triggers from the workflow. You need to use a combination of contractId and renewal_date as your deduplication key, not just eventId. The eventId is unique per webhook delivery attempt, not per business event.

For subscription webhooks in ICS 2022, you should also check the webhook configuration’s event filters. Make sure you’re only subscribed to the final renewal event, not intermediate status changes. The contract renewal workflow has multiple stages, and by default webhooks fire for each stage transition.

Tested this on Infor CloudSuite with PostgreSQL webhook_events table and composite business key deduplication, eliminating duplicate subscription renewal billing errors across all contract IDs.