{"record":{"id":"b24261430220dda6","repo":"actualbudget/actual","slug":"since-required","errorCode":"since-required","errorMessage":"since-required","messagePattern":"since-required","errorType":"validation","errorClass":null,"httpStatus":422,"severity":"error","filePath":"packages/sync-server/src/app-sync.ts","lineNumber":156,"sourceCode":"app.post('/sync', async (req, res): Promise<void> => {\n  let requestPb;\n  try {\n    requestPb = fromBinary(SyncRequestSchema, req.body);\n  } catch (e) {\n    console.log('Error parsing sync request', e);\n    res.status(500);\n    res.send({ status: 'error', reason: 'internal-error' });\n    return;\n  }\n\n  const fileId = requestPb.fileId || null;\n  const groupId = requestPb.groupId || null;\n  const keyId = requestPb.keyId || null;\n  const since = requestPb.since || null;\n  const messages = requestPb.messages;\n\n  if (!since) {\n    res.status(422).send({\n      details: 'since-required',\n      reason: 'unprocessable-entity',\n      status: 'error',\n    });\n    return;\n  }\n\n  const filesService = new FilesService(getAccountDb());\n\n  const currentFile = verifyFileExists(\n    fileId,\n    filesService,\n    res,\n    'file-not-found',\n  );\n\n  if (!currentFile) {\n    return;","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-sync.ts#L138-L174","documentation":"The sync protocol requires a 'since' timestamp cursor to know from which point to return changes. If the decoded SyncRequest has no since value, the server responds 422 with details 'since-required' and reason 'unprocessable-entity'.","triggerScenarios":"POSTing a well-formed SyncRequest protobuf whose since field is 0/null/absent — e.g. hand-built sync requests or clients that omitted the field after a protocol change.","commonSituations":"Custom API integrations constructing SyncRequest manually; clients upgraded against an older schema where since was optional; resetting local state and forgetting to seed the cursor (use '0' or the epoch value for a full initial sync).","solutions":["Set the since field on the SyncRequest to the last received sync timestamp (use '0' for an initial full sync).","Re-generate the client protobuf code from the current schema so since is populated.","If using the official client/API, upgrade it — it sets since automatically."],"exampleFix":"// before\nconst request = { fileId, groupId, messages };\n// after\nconst request = { fileId, groupId, messages, since: lastSyncedTimestamp || '0' };","handlingStrategy":"validation","validationCode":"if (!request.since || request.since === '0' && !allowFullSync) throw new Error('since is required; pass the last sync timestamp or 0 for initial sync');","typeGuard":"const hasSince = (r) => typeof r.since === 'string' && r.since.length > 0;","tryCatchPattern":"try {\n  const res = await postSync(request);\n  if (res.status === 422 && (await res.json()).details === 'since-required') {\n    throw new Error('add a since cursor to the SyncRequest');\n  }\n  return res;\n} catch (e) { throw e; }","preventionTips":["Persist the last sync timestamp after every successful sync","Use since='0' deliberately for full initial syncs","Regenerate protobuf types after protocol upgrades"],"tags":["validation","http-422","sync","missing-field"],"backgroundTag":"missing-required-field","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}