← all posts

Made an auto-dispute response system for Interior AI to see how easy it'd be

6 April, 2026

โœ… Done

๐Ÿ’ณ Made an auto-dispute response system for Interior AI to see how easy it'd be

It syncs old disputes but also catches new disputes via Stripe webhook and then auto submits evidence to try win them, it even includes the interior designs they generated in the evidence PDF to prove they used it!

Here's the prompt/skill I made:

Build an auto-dispute-response system for Stripe that:

  1. Shared evidence collection (app/dispute_evidence.php)

Create a shared file with functions used by both the webhook and sync worker. This avoids duplicating evidence logic.

Key functions:

Important: pull total_amount_paid from Stripe charges API (sum of succeeded, non-refunded charges) instead of trusting the local DB which can be null/stale.

  1. Webhook handler (in stripe_webhook.php)

Catch charge.dispute.created events. When a dispute comes in:

Also catch charge.dispute.updated and charge.dispute.closed events to track dispute outcomes (won/lost) in the database and send Telegram notifications with the result (with emoji: checkmark for won, x for lost, warning for other).

  1. Evidence fields submitted to Stripe

TEXT fields (write strings directly):

FILE UPLOAD fields (upload file to Stripe first via $stripe->files->create(['purpose'=>'dispute_evidence', 'file'=>fopen($path,'r')]), then pass the returned file_xxxxx ID):

Also save both PDFs to your file storage (e.g. Cloudflare R2, S3) with hashed filenames so they're not guessable but viewable from the admin dashboard.

Store the storage URLs in the evidence_json as _receipt_r2_url and _service_doc_r2_url (underscore prefix so they're easy to identify as internal fields).

DO NOT use these fields for text โ€” they expect file upload IDs only:
service_documentation, cancellation_policy, refund_policy, customer_communication, customer_signature, receipt, shipping_documentation, duplicate_charge_documentation, uncategorized_file

  1. CLI sync worker (workers/syncDisputes.php)

A script that pulls ALL existing disputes from Stripe's API (paginated with $stripe->disputes->all(['limit' => 100]) and starting_after for pagination), saves them to the local disputes database, and for any that still have needs_response or warning_needs_response status and haven't had evidence submitted yet โ€” auto-submits evidence using the shared collectDisputeEvidence() function. This is needed because the webhook only catches future disputes, not existing ones. Too heavy to run on frontend โ€” run via CLI only (php workers/syncDisputes.php). Saves a JSON cache file with sync results so the dashboard can show last sync time.

  1. Mini dashboard (disputes.php with ?key= auth)

A simple HTML page protected by ?key= query parameter that shows:

Detail view (action=view&id=dispute_id):

Regenerate Evidence (action=regen&id=dispute_id):

Add an nginx rewrite for the page (e.g. rewrite ^/disputes/?$ /disputes.php). Make sure it's in the correct nginx config file (check which one the symlink in sites-enabled actually points to).

  1. Telegram notifications
  1. Make sure these Stripe webhook events are enabled in the Stripe dashboard:

    • charge.dispute.created
    • charge.dispute.updated
    • charge.dispute.closed
  2. Database permissions

The disputes.db file must be writable by the web server user (e.g. www-data). If you create it from CLI as root, fix ownership to match your other DB files. PHP-FPM runs as a different user than root.

  1. Dependencies

Originally posted on X

P.S. I'm on 𝕏 too if you'd like to follow more of my stories. And I wrote a book called MAKE about building startups without funding. See a list of my stories or contact me. To get an alert when I write a new blog post, you can join 13,144 subscribers below:

Subscribing you...
Subscribed! Check your inbox to confirm your email.