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

  1. Reconcile first: retrieve the Stripe charge (you have stripe.chargeID) and inspect its refunds collection to see whether the refund executed.
  2. If it executed, record the real refund ID into 'stripe.refundID' metadata and complete the local refund bookkeeping.
  3. If it did not execute, retry the refund once reconciled. Never retry before checking — a double refund loses money.
  4. 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

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


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/b78a94a86cfed032. Report an issue: GitHub.