Automating Dispute Management with Agents SDK and Stripe API (part 1)
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:
-
Stripe lets you simulate payments to test your integration. See the docs.
-
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:
-
We can retrieve a PaymentIntent given its id:
payment.id # 'pi_3RlnqDQuMzbiv2kf0ArxCteD' stripe.PaymentIntent.retrieve(payment.id)
-
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.