{"openapi":"3.1.0","info":{"title":"POST /api/v1/credit/sandbox/sync-config","version":"1.0.0","description":"Copy program configuration to the sandbox again"},"servers":[{"url":"https://api.ondayzero.com","description":"Production"}],"paths":{"/api/v1/credit/sandbox/sync-config":{"post":{"tags":["credit"],"summary":"Copy program configuration to the sandbox again","description":"Console operators only, on the live business. Copies every program's configuration onto the paired sandbox's copy (creating a copy for a program added since), and copies any scorecard version the live business has activated since, making it active on the sandbox. Makes the sandbox's memberships, firm links and client access mirror the live business (`access`), clears alert destinations a copy still shares with live, and lists sandbox tokens held by DayZero staff who are not members (`superuser_tokens`). Reports what changed per program. Never touches lines, loans or cards. 400 `CREDIT_581` when there is no paired sandbox.","operationId":"credit_sync_sandbox_config","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-business-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Business-Id"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope_SandboxSyncResponse_"}}}},"400":{"description":"Bad Request - Invalid input","content":{"application/json":{"example":{"error":"validation_error","message":"Validation failed: due_date: Input should be a valid date","errors":{"due_date":"Input should be a valid date"},"code":"GEN_002","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized - Missing or expired token, an MCP OAuth token used against the REST API, or no access to this business","content":{"application/json":{"example":{"error":"unauthorized","message":"Authentication required","code":"AUTH_001","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden - Insufficient permissions","content":{"application/json":{"example":{"error":"forbidden","message":"You don't have permission to access this resource","code":"AUTH_002","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found - Resource does not exist","content":{"application/json":{"example":{"error":"not_found","message":"Resource not found","code":"NOT_FOUND_001","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"validation_error","message":"Validation failed: amount: Input should be a valid integer","errors":{"amount":"Input should be a valid integer"},"code":"GEN_002","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"}}}}},"x-fastapi-operation-id":"credit_sync_sandbox_config_api_v1_credit_sandbox_sync_config_post"}}},"components":{"schemas":{"SandboxAccessSync":{"properties":{"members_added":{"type":"integer","title":"Members Added","description":"People given the live business's role on the sandbox.","default":0},"members_removed":{"type":"integer","title":"Members Removed","description":"People removed: no longer members of the live business.","default":0},"members_updated":{"type":"integer","title":"Members Updated","description":"People whose role or menu changed to match live.","default":0},"hidden_members_removed":{"type":"integer","title":"Hidden Members Removed","description":"Hidden staff (support) memberships on the sandbox with no counterpart on the live business, removed.","default":0},"firm_links_added":{"type":"integer","title":"Firm Links Added","description":"Firm links added.","default":0},"firm_links_removed":{"type":"integer","title":"Firm Links Removed","description":"Firm links removed.","default":0},"firm_links_updated":{"type":"integer","title":"Firm Links Updated","description":"Firm links changed.","default":0},"client_access_added":{"type":"integer","title":"Client Access Added","description":"Rows added that give someone the sandbox: client access, firm staff assignments and firm teams.","default":0},"client_access_removed":{"type":"integer","title":"Client Access Removed","description":"Such rows removed: gone from the live business.","default":0},"client_access_updated":{"type":"integer","title":"Client Access Updated","description":"Such rows changed.","default":0}},"type":"object","title":"SandboxAccessSync","description":"How the sandbox's access was made to mirror the live business's."},"SandboxProgramSync":{"properties":{"live_program_id":{"type":"string","title":"Live Program Id","description":"The program on the live business."},"sandbox_program_id":{"type":"string","title":"Sandbox Program Id","description":"Its copy on the sandbox."},"slug":{"type":"string","title":"Slug","description":"The sandbox program's slug."},"action":{"type":"string","enum":["created","updated","unchanged"],"title":"Action","description":"`created`: the live program had no sandbox copy yet. `updated`: a configuration field or a scorecard changed. `unchanged`: the copy already matched."},"changed_fields":{"items":{"type":"string"},"type":"array","title":"Changed Fields","description":"The program fields whose value was copied over."},"alert_destinations_cleared":{"items":{"type":"string"},"type":"array","title":"Alert Destinations Cleared","description":"`alert_emails` and `alert_slack_channel` when the sandbox copy still held the live program's own destinations (copied by provisioning before it stopped copying them) and this sync cleared them, so rehearsal alerts stop reaching the live inbox. Destinations set on the sandbox by hand are kept."},"scorecards":{"items":{"$ref":"#/components/schemas/SandboxScorecardSync"},"type":"array","title":"Scorecards","description":"Each live scorecard and its copy."}},"type":"object","required":["live_program_id","sandbox_program_id","slug","action"],"title":"SandboxProgramSync"},"SandboxScorecardSync":{"properties":{"live_scorecard_id":{"type":"string","title":"Live Scorecard Id","description":"The scorecard on the live program."},"sandbox_scorecard_id":{"type":"string","title":"Sandbox Scorecard Id","description":"Its copy on the sandbox program."},"name":{"type":"string","title":"Name","description":"The scorecard's name."},"action":{"type":"string","enum":["created","version_activated","updated","unchanged"],"title":"Action","description":"`created`: the scorecard was new to the sandbox. `version_activated`: the live business activated a version that was never copied, so it was copied and made active on the sandbox (the sandbox's active version retired). `updated`: only the scorecard's settings changed. `unchanged`: nothing to copy. A version activated on the sandbox itself stays active until the live business activates a new one."},"live_version_id":{"type":"string","title":"Live Version Id","description":"The live scorecard's active version."},"sandbox_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sandbox Version Id","description":"The sandbox scorecard's active version after the sync."},"changed_fields":{"items":{"type":"string"},"type":"array","title":"Changed Fields","description":"Scorecard settings that changed: name, description, is_default, required_approvals."}},"type":"object","required":["live_scorecard_id","sandbox_scorecard_id","name","action","live_version_id","sandbox_version_id"],"title":"SandboxScorecardSync"},"SandboxSyncResponse":{"properties":{"live_business_id":{"type":"string","title":"Live Business Id","description":"The business the call was made on."},"sandbox_business_id":{"type":"string","title":"Sandbox Business Id","description":"The paired sandbox business."},"sandbox_business_name":{"type":"string","title":"Sandbox Business Name","description":"The sandbox's name."},"programs":{"items":{"$ref":"#/components/schemas/SandboxProgramSync"},"type":"array","title":"Programs","description":"Each live program and what the sync did to its sandbox copy."},"programs_created":{"type":"integer","title":"Programs Created","description":"Programs copied for the first time."},"programs_updated":{"type":"integer","title":"Programs Updated","description":"Programs with a changed field or scorecard."},"programs_unchanged":{"type":"integer","title":"Programs Unchanged","description":"Programs that already matched."},"access":{"$ref":"#/components/schemas/SandboxAccessSync","description":"Memberships, firm links and client access on the sandbox, made to mirror the live business: added, removed and updated."},"superuser_tokens":{"items":{"$ref":"#/components/schemas/SandboxTokenFlag"},"type":"array","title":"Superuser Tokens","description":"Live tokens limited to the sandbox and owned by a DayZero superuser who is not a member of the live business. They are listed, not revoked: staff revoke each deliberately."},"superuser_tokens_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Superuser Tokens Note","description":"How to revoke the listed tokens. Null when there are none."}},"type":"object","required":["live_business_id","sandbox_business_id","sandbox_business_name","programs_created","programs_updated","programs_unchanged"],"title":"SandboxSyncResponse"},"SandboxTokenFlag":{"properties":{"token_id":{"type":"string","title":"Token Id","description":"The token, to revoke it."},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"The token's name."},"owner_user_id":{"type":"string","title":"Owner User Id","description":"The superuser who owns it."},"owner_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Email","description":"Their email."},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At","description":"When it was minted."},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","description":"When it expires."}},"type":"object","required":["token_id","owner_user_id"],"title":"SandboxTokenFlag","description":"A sandbox token owned by DayZero staff who are not members of the business."},"SuccessEnvelope_SandboxSyncResponse_":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"},"data":{"anyOf":[{"$ref":"#/components/schemas/SandboxSyncResponse"},{"type":"null"}]}},"additionalProperties":true,"type":"object","title":"SuccessEnvelope[SandboxSyncResponse]"},"ErrorResponse":{"title":"ErrorResponse","type":"object","description":"Envelope returned by every non-2xx response. Branch on `code` (stable) rather than `message` (human-readable, may change).","required":["error","message","code"],"properties":{"error":{"type":"string","description":"Error category: `validation_error`, `unauthorized`, `forbidden`, `not_found`, `conflict`, `rate_limited`, `server_error`, or `service_unavailable`."},"message":{"type":"string","description":"Human-readable explanation, safe to show to end users."},"code":{"type":"string","description":"Stable machine-readable code in `CATEGORY_NNN` form (e.g. `NOT_FOUND_006`, `AUTH_010`, `GEN_002`)."},"request_id":{"type":"string","description":"Correlation id for support requests. Echoes the `x-request-id` request header when one was supplied."},"errors":{"type":"object","additionalProperties":{"type":"string"},"description":"Field-level validation messages keyed by field name. Present on 400/422 validation failures only."}}}},"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Token","description":"API token authentication. Format: `Bearer dz_...`"}}},"security":[{"BearerAuth":[]}]}