post_admin_stripe_backfill

Changes data · you confirm first

Calls POST /api/admin/stripe-backfill as the signed-in person · REST reference

Import historical Stripe charges the ledger never saw

Walks the configured Stripe account's PaymentIntents and records each eligible one in the ledger. A PaymentIntent that did not succeed, was fully or partly refunded, is disputed or received nothing is reported as skipped with a reason. Platform staff only (requirePlatformStaff); there is no dedicated backfill permission. Unlike the factory reset it needs no emailed token: a committed run is additive and idempotent (the DAL MERGEs on the PaymentIntent id), so a repeat committed run reports payments the ledger already holds as alreadyPresent. ENVIRONMENT-GATED to local and staging by the same allowlist the factory reset uses, and the tier is the FIRST thing checked - a production tier answers 403 before the Stripe key is looked at. DEFAULTS TO A DRY RUN. dryRun is true unless the body sets it to exactly false, so omitting the flag - or sending any other value - reports what WOULD be imported and writes nothing. A dry run does not consult the ledger, so every eligible PaymentIntent is reported as imported, including ones the ledger already holds. Committing credits the voucher pool.

Arguments

ArgumentTypeDescription
bodyJSONobjectThe request body, as a JSON object. Its fields are listed below.

Fields of body

FieldTypeDescription
dryRunbooleanOnly the literal false commits. Absent, null or any other value is a dry run, and this field is NOT range-validated the way the three below are.
limitintegerCap on PaymentIntents scanned. Non-negative finite number when supplied.
sinceintegerLower bound on the charge date, in epoch MILLISECONDS. Must be a non-negative finite number when supplied.
untilintegerUpper bound on the charge date, in epoch MILLISECONDS. Must be a non-negative finite number when supplied.

Example call

A client sends this over the MCP session; Claude and other agents build it for you. The values are placeholders — the shape is real.

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "post_admin_stripe_backfill",
    "arguments": {
      "body": {
        "dryRun": true,
        "limit": 1,
        "since": 1,
        "until": 1
      }
    }
  }
}
Confirmation
The client shows the person the exact request and waits. Nothing runs until they tick Make this change. See Reading and changing.

Result

On success (HTTP 200) the tool returns one text item holding the API's JSON, pretty-printed.

The run finished. A dry run and a committed run return the same shape.

json
{
  "alreadyPresent": 1,
  "dryRun": true,
  "failed": 1,
  "imported": 1,
  "importedAmount": 1,
  "outcomes": [
    {
      "amount": 1,
      "currency": "string",
      "occurredAt": 1,
      "paymentIntentId": "string",
      "reason": "string",
      "status": "imported"
    }
  ],
  "scanned": 1,
  "skipped": 1
}

Fields

FieldTypeDescription
alreadyPresentintegerAlready in the ledger, so nothing was written for them. Always 0 on a dry run, which does not consult the ledger.
dryRunbooleanTrue for a dry run, which writes nothing; false for a committed run.
failedinteger
importedinteger
importedAmountintegerCENTS actually added to the ledger by this run. Always 0 on a dry run.
outcomesarray of objectOne entry per PaymentIntent considered.
outcomes[].amountintegerCENTS.
outcomes[].currencystring
outcomes[].occurredAtintegerEpoch milliseconds.
outcomes[].paymentIntentIdstring
outcomes[].reasonstringPresent on skipped and failed outcomes, and on every imported outcome of a dry run ("dry run — nothing written").
outcomes[].status"imported" | "alreadyPresent" | "skipped" | "failed"
scannedinteger
skippedinteger

When it fails

A failed call returns isError: true with the API's message as text. A change the person declines, or lets time out, also comes back this way and nothing changes.

400

since, until or limit was supplied and is not a non-negative finite number.

403

Either the caller is not platform staff, or the environment forbids the backfill entirely (production always does, and so does any tier string outside the local/staging allowlist). Both answer 403 and the body is the only way to tell them apart, exactly as on the factory-reset routes.

What the model is shown

This is the tool exactly as the server lists it in tools/list. The description starts with the REST call it makes.

json
{
  "name": "post_admin_stripe_backfill",
  "description": "POST /api/admin/stripe-backfill — Walks the configured Stripe account's PaymentIntents and records each eligible one in the ledger. A PaymentIntent that did not succeed, was fully or partly refunded, is disputed or received nothing is reported as `skipped` with a `reason`. Platform staff only (`requirePlatformStaff`); there is no dedicated backfill permission. Unlike the factory reset it needs no emailed token: a committed run is additive and idempotent (the DAL MERGEs on the PaymentIntent id), so a repeat committed run reports payments the ledger already holds as `alreadyPresent`. ENVIRONMENT-GATED to local and staging by the same allowlist the factory reset uses, and the tier is the FIRST thing checked - a production tier answers 403 before the Stripe key is looked at. DEFAULTS TO A DRY RUN. `dryRun` is true unless the body sets it to exactly `false`, so omitting the flag - or sending any other value - reports what WOULD be imported and writes nothing. A dry run does not consult the ledger, so every eligible PaymentIntent is reported as `imported`, including ones the ledger already holds. Committing credits the voucher pool.\n\nThis changes data. The person is asked to confirm it before it runs, and nothing happens if they decline.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "body": {
        "type": "string",
        "description": "The JSON request body: { dryRun: boolean (Only the literal `false` commits.); limit: integer (Cap on PaymentIntents scanned.); since: integer (Lower bound on the charge date, in epoch MILLISECONDS.); until: integer (Upper bound on the charge date, in epoch MILLISECONDS.) }"
      }
    }
  },
  "annotations": {
    "readOnlyHint": false,
    "destructiveHint": true,
    "idempotentHint": false,
    "openWorldHint": false
  }
}