phacility/phabricator · critical · Exception
Stripe refund call did not return an ID!
Error message
Stripe refund call did not return an ID!
What it means
Exception thrown by PhortuneStripePaymentProvider::executeRefund() when the refund object returned by $stripe_charge->refunds->create($params) has a falsy ->id. The refund ID is what Phortune stores as 'stripe.refundID' metadata; without it a possibly-executed refund at Stripe has no local trace, leaving the books unbalanced (customer money returned, Phortune unaware). Mirrors the charge-side check: the legacy SDK path can yield objects without ids instead of throwing.
Source
Thrown at src/applications/phortune/provider/PhortuneStripePaymentProvider.php:182
pht('Unable to refund charge; no Stripe chargeID!'));
}
$refund_cents = $refund
->getAmountAsCurrency()
->negate()
->getValueInUSDCents();
$secret_key = $this->getSecretKey();
$params = array(
'amount' => $refund_cents,
);
$stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
$stripe_refund = $stripe_charge->refunds->create($params);
$id = $stripe_refund->id;
if (!$id) {
throw new Exception(pht('Stripe refund call did not return an ID!'));
}
$charge->setMetadataValue('stripe.refundID', $id);
$charge->save();
}
public function updateCharge(PhortuneCharge $charge) {
$this->loadStripeAPILibraries();
$charge_id = $charge->getMetadataValue('stripe.chargeID');
if (!$charge_id) {
throw new Exception(
pht('Unable to update charge; no Stripe chargeID!'));
}
$secret_key = $this->getSecretKey();
$stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
View on GitHub (pinned to 5720a38cfe)
Solutions
- Reconcile first: retrieve the Stripe charge (you have stripe.chargeID) and inspect its refunds collection to see whether the refund executed.
- If it executed, record the real refund ID into 'stripe.refundID' metadata and complete the local refund bookkeeping.
- If it did not execute, retry the refund once reconciled. Never retry before checking — a double refund loses money.
- Upgrade the vendored stripe-php library to the API version in use and re-test refunds in Stripe test mode.
Example fix
// before
$stripe_refund = $stripe_charge->refunds->create($params);
$id = $stripe_refund->id;
if (!$id) {
throw new Exception(pht('Stripe refund call did not return an ID!'));
}
// after: reconcile the unknown refund state
$stripe_refund = $stripe_charge->refunds->create($params);
$id = $stripe_refund->id;
if (!$id) {
$fresh = Stripe_Charge::retrieve($charge_id, $secret_key);
phlog(pht('Refund for %s returned no ID; %d refunds exist at Stripe. Reconcile before retrying.', $charge->getPHID(), count($fresh->refunds->data)));
throw new Exception(
pht('Stripe refund state unknown for %s; reconcile in Stripe dashboard.',
$charge->getPHID()));
}
$charge->setMetadataValue('stripe.refundID', $id);
$charge->save(); Defensive patterns
Strategy: try-catch
Validate before calling
// Shape-check the refund result before recording it.
function stripe_refund_has_id($stripe_refund) {
return is_object($stripe_refund) && !empty($stripe_refund->id);
}
$stripe_refund = $stripe_charge->refunds->create($params);
if (!stripe_refund_has_id($stripe_refund)) {
// Unknown refund state: retrieve the charge and inspect ->refunds->data
// to see whether money actually moved before doing anything else.
} Type guard
function stripe_refund_has_id($stripe_refund) {
return is_object($stripe_refund) && !empty($stripe_refund->id);
} Try / catch
try {
$provider->executeRefund($charge, $refund);
} catch (Exception $ex) {
if (strpos($ex->getMessage(), 'refund call did not return an ID') !== false) {
// Retrieve the charge (we hold stripe.chargeID) and reconcile:
$fresh = Stripe_Charge::retrieve($charge_id, $secret_key);
phlog(pht(
'UNRESOLVED STRIPE REFUND for %s: %d refunds at Stripe. Reconcile before retry.',
$charge->getPHID(),
count($fresh->refunds->data)));
}
throw $ex;
} Prevention
- Never blind-retry a refund whose result was malformed — verify the charge's refunds list at Stripe first (a duplicate refund is lost money).
- Keep the stripe-php library current with the API version you call; malformed-object errors are typically version drift or mocks.
- Alert on this exception class: an untracked refund unbalances the books silently.
When it happens
Trigger: Stripe_Charge::retrieve() succeeds, refunds->create() returns a malformed object (old stripe-php versions, API shape drift, mocks) — ->id empty. The Stripe-side refund may or may not actually have been issued, so the state is unknown and must be reconciled, not retried blindly.
Common situations: Vendored Stripe library out of sync with the live API version; sandbox test doubles returning stdClass; network middleboxes mangling the JSON response; partial refunds already issued making subsequent responses unexpected to old SDK parsers.
Related errors
- Stripe charge call did not return an ID!
- Unable to refund charge; no Stripe chargeID!
- Charge has no transaction ID!
- Paypal checkout does not match Phortune charge!
- Unable to update charge; no Stripe chargeID!
AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21).
Data as JSON: /api/errors/b78a94a86cfed032.
Report an issue: GitHub.