actualbudget/actual · error

SimpleFIN claim failed with HTTP ${response.status}

Error message

SimpleFIN claim failed with HTTP ${response.status}

What it means

claimAccessKey POSTs the temporary setup token to SimpleFIN's claim URL and returns the resulting access key text. A 403 is treated as an expected 'not claimable/already claimed' response, but any other non-OK HTTP status causes it to throw 'SimpleFIN claim failed with HTTP <status>'. This signals an upstream problem with the token exchange.

Source

Thrown at packages/sync-server/src/app-simplefin/app-simplefin.js:365

  }

  return decoded;
}

async function claimAccessKey(claimUrl) {
  // Self-hosters may run their own SimpleFIN bridge on the local network, so
  // private addresses are allowed here; cloud metadata and other always-blocked
  // ranges are still rejected.
  await assertUrlAllowed(claimUrl, { allowPrivateNetwork: true });

  // don't auto-follow redirects for SSRF safety
  const response = await fetch(claimUrl, {
    method: 'POST',
    redirect: 'manual',
  });

  if (!response.ok && response.status !== 403) {
    throw new Error(`SimpleFIN claim failed with HTTP ${response.status}`);
  }

  return (await response.text()).trim();
}

function isForbidden(value) {
  return typeof value === 'string' && value.startsWith('Forbidden');
}

function isInvalidAccessKey(accessKey) {
  return (
    typeof accessKey !== 'string' ||
    isForbidden(accessKey) ||
    !ACCESS_KEY_FORMAT.test(accessKey)
  );
}

async function getTransactions(accessKey, accounts, startDate, endDate) {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Generate a new setup token at bridge.simplefin.org and retry the claim — tokens expire quickly.
  2. Check the HTTP status in the message: 429 means back off and retry later; 5xx means wait for SimpleFIN to recover.
  3. Retry after a short delay for transient 5xx errors (the message includes the exact status to branch on).
  4. Verify network/proxy settings if a non-SimpleFIN status appears (e.g. 502 from a corporate proxy).

Example fix

// before
const key = await claimAccessKey(token); // throws on 500

// after
let key;
try {
  key = await claimAccessKey(token);
} catch (e) {
  if (e.message.includes('HTTP 5')) key = await claimAccessKey(token); // retry transient
  else throw e;
}
Defensive patterns

Strategy: retry

Try / catch

try {
  key = await claimAccessKey(token);
} catch (e) {
  const m = /HTTP (\d{3})/.exec(e.message);
  if (m && +m[1] >= 500) {
    await sleep(2000);
    key = await claimAccessKey(token); // retry transient 5xx
  } else {
    throw e; // 4xx: token problem, get a fresh token
  }
}

Prevention

When it happens

Trigger: POSTing to the SimpleFIN claim endpoint and receiving e.g. 400, 404, 429, or 5xx; expired setup token returning an unexpected status; SimpleFIN outage; network intermediary returning an error status.

Common situations: Setup token older than ~30 minutes (expired) or already claimed with unexpected status handling; rate limiting after repeated setup attempts; transient SimpleFIN server errors during bank setup; a proxy stripping the redirect-manual POST.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/7daa4c2b635c1b70. Report an issue: GitHub.