Automating Dispute Management with Agents SDK and Stripe API (part 1)

July 18, 2025
tl;dr: I started exploring Dan Bell's guide on automating Stripe dispute management with OpenAI Agents SDK. After some Stripe API doc-diving, everything became clear. Cool first session!

You're reading part 1 of my exploration of the OpenAI Agents SDK, inspired by Dan Bell's post: Automating Dispute Management with Agents SDK and Stripe API:

  • Check out Part 2 where I experimented with different triage agents,

  • Check out Part 3 where I looked at the JSON data of the requests and responses that the OpenAI Agents SDK sends to the Responses API,

  • Check out Part 4 where I decided to try removing all the agents from the original code.

Yesterday, I started reading and experimenting with Dan Bell's post: Automating Dispute Management with Agents SDK and Stripe API. It's an interesting article to get started using AI Agents to automate real business cases. In this case, it's about managing Stripe disputes.

The goal of the program is simple. If the company hasn't shipped the product, it should automatically accept the dispute. Otherwise, it should generate a detailed report on the dispute so the company knows how to proceed.

The main logic uses an agent for triage. That agent then hands things off, either to another agent that accepts the dispute, or to one that creates the report.

Agent orchestration is handled using the Python OpenAI Agents SDK.

For instance, the triage agent is defined like this:

triage_agent = Agent(
    name="Triage Agent",
    instructions=(
        "Please do the following:\n"
        "1. Find the order ID from the payment intent's metadata.\n"
        "2. Retrieve detailed information about the order (e.g., shipping status).\n"
        "3. If the order has shipped, escalate this dispute to the investigator agent.\n"
        "4. If the order has not shipped, accept the dispute.\n"
    ),
    model="gpt-4o",
    tools=[retrieve_payment_intent, get_order],
    handoffs=[accept_dispute_agent, investigator_agent],
)

All the agents use tools to retrieve relevant information or to trigger actions, like accepting the dispute:

@function_tool
async def close_dispute(dispute_id: str) -> dict:
    """
    Close a Stripe dispute by ID.
    Returns the dispute object on success or an empty dictionary on failure.
    """
    try:
        return stripe.Dispute.close(dispute_id)
    except stripe.error.StripeError as e:
        logger.error(f"Stripe error occurred while closing dispute: {e}")
        return {}

Stripe API and testing

As you can see above, we're using the Stripe API to close the dispute. If you're like me and unfamiliar with the Stripe API, you may appreciate the following references and examples:

  1. Stripe lets you simulate payments to test your integration. See the docs.

  2. https://docs.stripe.com/api/payment_intents

  3. https://docs.stripe.com/api/disputes

  4. Create a test PaymentIntent to simulate a dispute for a product not received:

    payment = stripe.PaymentIntent.create(
      amount=2000,
      currency="usd",
      payment_method = "pm_card_createDisputeProductNotReceived",
      confirm=True,
      metadata={"order_id": "1234"},
      off_session=True,
      automatic_payment_methods={"enabled": True},
    )
    
    payment
    # <PaymentIntent payment_intent id=pi_3RlnqDQuMzbiv2kf0ArxCteD at 0x76a8e759f340> JSON: {
    #   "amount": 2000,
    #   "amount_capturable": 0,
    #   "amount_details": {
    #     "tip": {}
    #   },
    #   "amount_received": 2000,
    #   "application": null,
    #   "application_fee_amount": null,
    #   "automatic_payment_methods": {
    #     "allow_redirects": "always",
    #     "enabled": true
    #   },
    #   "canceled_at": null,
    #   "cancellation_reason": null,
    #   "capture_method": "automatic_async",
    #   "client_secret": "pi_3RlnqDQuMzbiv2kf0ArxCteD_secret_UnrGOgU6YAeL8pG7tpYkAVdcO",
    #   "confirmation_method": "automatic",
    #   "created": 1752743461,
    #   "currency": "usd",
    #   "customer": null,
    #   "description": null,
    #   "id": "pi_3RlnqDQuMzbiv2kf0ArxCteD",
    #   "last_payment_error": null,
    #   "latest_charge": "ch_3RlnqDQuMzbiv2kf0yimYSQE",
    #   "livemode": false,
    #   "metadata": {
    #     "order_id": "1234"
    #   },
    #   "next_action": null,
    #   "object": "payment_intent",
    #   "on_behalf_of": null,
    #   "payment_method": "pm_1RlnqDQuMzbiv2kfWoEqVnxE",
    #   "payment_method_configuration_details": {
    #     "id": "pmc_1RlmnJQuMzbiv2kfOw1kPhMl",
    #     "parent": null
    #   },
    #   "payment_method_options": {
    #     "amazon_pay": {
    #       "express_checkout_element_session_id": null
    #     },
    #     "card": {
    #       "installments": null,
    #       "mandate_options": null,
    #       "network": null,
    #       "request_three_d_secure": "automatic"
    #     },
    #     "link": {
    #       "persistent_token": null
    #     }
    #   },
    #   "payment_method_types": [
    #     "card",
    #     "link",
    #     "amazon_pay"
    #   ],
    #   "processing": null,
    #   "receipt_email": null,
    #   "review": null,
    #   "setup_future_usage": null,
    #   "shipping": null,
    #   "source": null,
    #   "statement_descriptor": null,
    #   "statement_descriptor_suffix": null,
    #   "status": "succeeded",
    #   "transfer_data": null,
    #   "transfer_group": null
    # }

    Once created we can monitor it in the Stripe Test dashboard:

    Monitoring and managing Stripe payment disputes in Stripe dashboard for automation

  5. We can retrieve a PaymentIntent given its id:

    payment.id # 'pi_3RlnqDQuMzbiv2kf0ArxCteD'
    stripe.PaymentIntent.retrieve(payment.id)
  6. We can close a dispute like this:

    stripe.Dispute.close('dp_1RlnqEQuMzbiv2kfEb8g1kGv')
    # <Dispute dispute id=dp_1RlnqEQuMzbiv2kfEb8g1kGv at 0x76a8e42f7660> JSON: {
    #   "amount": 2000,
    #   "balance_transaction": "txn_1RlnqEQuMzbiv2kf7fL8zROt",
    #   "balance_transactions": [
    #     {
    #       "amount": -2000,
    #       "available_on": 1752743462,
    #       "balance_type": "payments",
    #       "created": 1752743462,
    #       "currency": "eur",
    #       "description": "Chargeback withdrawal for ch_3RlnqDQuMzbiv2kf0yimYSQE",
    #       "exchange_rate": null,
    #       "fee": 2000,
    #       "fee_details": [
    #         {
    #           "amount": 2000,
    #           "application": null,
    #           "currency": "eur",
    #           "description": "Dispute fee",
    #           "type": "stripe_fee"
    #         }
    #       ],
    #       "id": "txn_1RlnqEQuMzbiv2kf7fL8zROt",
    #       "net": -4000,
    #       "object": "balance_transaction",
    #       "reporting_category": "dispute",
    #       "source": "dp_1RlnqEQuMzbiv2kfEb8g1kGv",
    #       "status": "available",
    #       "type": "adjustment"
    #     }
    #   ],
    #   "charge": "ch_3RlnqDQuMzbiv2kf0yimYSQE",
    #   "created": 1752743462,
    #   "currency": "usd",
    #   "enhanced_eligibility_types": [],
    #   "evidence": {
    #     "access_activity_log": null,
    #     "billing_address": null,
    #     "cancellation_policy": null,
    #     "cancellation_policy_disclosure": null,
    #     "cancellation_rebuttal": null,
    #     "customer_communication": null,
    #     "customer_email_address": null,
    #     "customer_name": null,
    #     "customer_purchase_ip": null,
    #     "customer_signature": null,
    #     "duplicate_charge_documentation": null,
    #     "duplicate_charge_explanation": null,
    #     "duplicate_charge_id": null,
    #     "enhanced_evidence": {},
    #     "product_description": null,
    #     "receipt": null,
    #     "refund_policy": null,
    #     "refund_policy_disclosure": null,
    #     "refund_refusal_explanation": null,
    #     "service_date": null,
    #     "service_documentation": null,
    #     "shipping_address": null,
    #     "shipping_carrier": null,
    #     "shipping_date": null,
    #     "shipping_documentation": null,
    #     "shipping_tracking_number": null,
    #     "uncategorized_file": null,
    #     "uncategorized_text": null
    #   },
    #   "evidence_details": {
    #     "due_by": 1753574399,
    #     "enhanced_eligibility": {},
    #     "has_evidence": false,
    #     "past_due": false,
    #     "submission_count": 0
    #   },
    #   "id": "dp_1RlnqEQuMzbiv2kfEb8g1kGv",
    #   "is_charge_refundable": false,
    #   "livemode": false,
    #   "metadata": {},
    #   "object": "dispute",
    #   "payment_intent": "pi_3RlnqDQuMzbiv2kf0ArxCteD",
    #   "payment_method_details": {
    #     "card": {
    #       "brand": "visa",
    #       "case_type": "chargeback",
    #       "network_reason_code": "13.1"
    #     },
    #     "type": "card"
    #   },
    #   "reason": "product_not_received",
    #   "status": "lost"
    # }

How to get the Stripe dispute id given its PaymentIntent id

At first, I missed how to get the dispute ID from a PaymentIntent. Note that the close_dispute tool needs this ID.

I ended up on the Stripe docs, where I found their AI assistant. I asked:

how to get the dispute id from the payment intent id?

It replied with JS code. So, I clarified "in Python". This gave me this piece of code which didn't work for me:

def foo_get_dispute_id_from_payment_intent(payment_intent_id):
    try:
        # Retrieve the PaymentIntent with expanded charges
        payment_intent = stripe.PaymentIntent.retrieve(
            payment_intent_id,
            expand=['charges']
        )

        # Check if there are any charges
        if payment_intent.charges and payment_intent.charges.data:
            # Loop through the charges to find any with disputes
            for charge in payment_intent.charges.data:
                if charge.dispute:
                    return charge.dispute  # This is the dispute ID

        return None  # No dispute found
    except stripe.error.StripeError as e:
        print(f"An error occurred: {str(e)}")
        return None

This led to that error:

AttributeError: charges

After that, I tried a slightly different question:

Using stripe API (python sdk) how to retrieve the dispute id given a payment id?

This time, I asked Perplexity which gave me that snippet which worked for me:

payment_intent_id = "pi_3MtwBwLkdIwHu7ix28a3tqPa"
disputes = stripe.Dispute.list(payment_intent=payment_intent_id)
dispute_ids = [dispute.id for dispute in disputes.data]
print(dispute_ids)

Out of curiosity, I also tried that same question with Stripe's AI assistant. This time it gave me another snippet that did worked:

def foo_get_dispute_id(payment_intent_id):
    try:
        payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id)
        charge_id = payment_intent.latest_charge
        charge = stripe.Charge.retrieve(charge_id)
        if charge.dispute:
            return charge.dispute
        else:
            return "No dispute found for this payment"
    except stripe.error.StripeError as e:
        # Handle any errors
        return f"An error occurred: {str(e)}"

By the way, if I'd just read the body of process_dispute more carefully, I would have seen how to do it:

async def process_dispute(payment_intent_id, triage_agent):
    """Retrieve and process dispute data for a given PaymentIntent."""
    disputes_list = stripe.Dispute.list(payment_intent=payment_intent_id)
    if not disputes_list.data:
        logger.warning("No dispute data found for PaymentIntent: %s", payment_intent_id)
        return None

    dispute_data = disputes_list.data[0]

    relevant_data = {
        "dispute_id": dispute_data.get("id"),
        # ...
    }
    # ...

For this first look at the post, most of my time went into discovering how the Stripe API works and exploring its docs. Next I'll dive more into the agent orchestration.

That's all I have for today! Talk to you soon ;)

Built with one.el.