{"components":{"schemas":{"CreateFirmApiKeyRequest":{"properties":{"name":{"description":"Optional label. Blank and we'll name it for you.","type":"string"},"submission_updated_webhook_url":{"description":"Optional. When set, we POST a signed `submission.updated` event here on every status change.","type":"string"}},"type":"object"},"CreateFirmRequest":{"properties":{"auto_accept":{"default":true,"description":"Sandbox only. A sandbox firm is accepted on creation by default; send `false` to have it start `pending` instead, so it behaves like a real one and issuing its API key returns 409 `firm_not_accepted` until acceptance. Use that to rehearse your onboarding flow end to end \u2014 pair it with `POST /v1/partner/firms/{org_id}/sandbox/accept`, which performs the acceptance and fires the real `firm.management_accepted` webhook. Sending it at all with `sandbox: false` is rejected with 400 \u2014 including `false`, which is merely redundant there: a real firm always starts pending, and only its own admin can accept your management.","type":"boolean"},"firm_admin_email":{"description":"Who to invite as the firm's admin. Required for a real firm: one with no admin can never accept your management and would sit pending forever. Optional when `sandbox` is true \u2014 an auto-accepted sandbox firm has nothing to accept, and no email is sent. Supplying one alongside `auto_accept: false` does send a real invitation, if you want a human to accept it through the app instead.","format":"email","type":"string"},"name":{"description":"The firm's name, as it will appear to them.","example":"Acme Tax LLC","type":"string"},"sandbox":{"default":false,"description":"Create a throwaway SANDBOX firm instead of a real one. A sandbox firm is auto-accepted, needs no admin, and its submissions never reach a preparer \u2014 so you can exercise onboarding without leaving real organizations behind. Limit 5 per partner (`sandbox_limit_reached`); delete one to free a slot.","type":"boolean"}},"required":["name"],"type":"object"},"DeleteResult":{"properties":{"deleted":{"type":"boolean"},"id":{"type":"integer"}},"type":"object"},"Error":{"properties":{"code":{"description":"Stable, machine-readable error code \u2014 branch on this rather than string-matching `message`. Generic per-status codes plus specific ones that disambiguate same-status cases (e.g. `active_submission_exists` / `documents_still_uploading` are both 409). This is the full set across the whole API; a given endpoint only returns the subset that applies to it \u2014 see each endpoint's error responses and the Errors & limits guide.","enum":["bad_request","unauthorized","forbidden","not_found","conflict","payload_too_large","unprocessable_entity","rate_limited","internal_error","missing_authorization","missing_api_key","malformed_api_key","invalid_api_key","insufficient_scope","partner_paused","partner_unlinked","sandbox_only","no_input_documents","invalid_cursor","invalid_limit","invalid_content_md5","invalid_idempotency_key","active_submission_exists","documents_still_uploading","document_attached","cancel_window_passed","submission_already_terminal","firm_not_accepted","invitation_role_conflict","firm_already_accepted","firm_revoked","default_sandbox_protected","sandbox_limit_reached","idempotency_key_reuse","idempotency_request_in_progress"],"type":"string"},"message":{"type":"string"}},"required":["code","message"],"type":"object"},"Firm":{"properties":{"accepted_at":{"format":"date-time","nullable":true,"type":"string"},"created_at":{"format":"date-time","type":"string"},"declined_at":{"description":"When the firm declined your request. The status stays `pending` \u2014 declining is 'not now', not 'never', and you may ask again \u2014 so this is the only way to tell a firm that has not answered from one that said no. Null if they have never declined.","format":"date-time","nullable":true,"type":"string"},"is_sandbox":{"description":"True for your sandbox firm, which exists to build against.","type":"boolean"},"management_status":{"description":"You can only issue keys for a firm that has `accepted` your management. `revoked` means the firm ended the arrangement: it stays listed so you can reconcile, but every operation on it now fails and its keys no longer work.","enum":["pending","accepted","revoked"],"type":"string"},"name":{"nullable":true,"type":"string"},"onboarding_url":{"description":"Where to send the firm admin to complete onboarding. Returned on create, and on the single-firm read while the firm is still `pending` and your key holds `partner.firms.write`. **Treat it as a credential** \u2014 whoever opens it creates the firm's admin account. Null once they have accepted, or if the invitation is no longer outstanding.","nullable":true,"type":"string"},"organization_id":{"description":"The firm's identifier \u2014 use it in every path below.","type":"string"}},"type":"object"},"FirmApiKey":{"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"type":"integer"},"name":{"type":"string"},"partner_name":{"type":"string"},"sandbox":{"description":"Whether THIS KEY is a sandbox key \u2014 its submissions never reach a real preparer. Distinct from a firm's `is_sandbox`, which describes the firm: a live key on a sandbox firm reads `false` here, which is correct and not a contradiction.","type":"boolean"},"scopes":{"items":{"type":"string"},"type":"array"},"submission_updated_webhook_url":{"nullable":true,"type":"string"}},"type":"object"},"FirmApiKeyCreated":{"allOf":[{"$ref":"#/components/schemas/FirmApiKey"},{"properties":{"api_key":{"description":"The firm's key (`mag_...`). Shown once, never retrievable again.","type":"string"},"webhook_signing_secret":{"description":"Present only when a webhook URL was set. Verify `X-Magnetic-Signature` with it. Shown once.","nullable":true,"type":"string"}},"type":"object"}]},"FirmApiKeyList":{"properties":{"api_keys":{"items":{"$ref":"#/components/schemas/FirmApiKey"},"type":"array"}},"type":"object"},"FirmApiKeyUpdated":{"allOf":[{"$ref":"#/components/schemas/FirmApiKey"},{"properties":{"webhook_signing_secret":{"description":"Present ONLY when this request set or changed a non-empty webhook URL, which rotates the secret. Shown once \u2014 capture it before the response is discarded. Absent when the URL was cleared or only the name changed.","nullable":true,"type":"string"}},"type":"object"}]},"FirmDeleteResult":{"properties":{"deleted":{"type":"boolean"},"organization_id":{"type":"string"}},"required":["deleted","organization_id"],"type":"object"},"FirmInvitation":{"properties":{"email":{"description":"The address the invitation was sent to.","type":"string"},"invited":{"description":"Always true on a 201 \u2014 present so the shape reads unambiguously.","type":"boolean"},"role":{"description":"Firm role granted on acceptance, e.g. `org:admin`.","type":"string"}},"required":["email","role","invited"],"type":"object"},"FirmList":{"properties":{"firms":{"items":{"$ref":"#/components/schemas/Firm"},"type":"array"}},"type":"object"},"InviteFirmUserRequest":{"properties":{"as_admin":{"default":true,"description":"Invite as a firm admin. Set false for an ordinary member.","type":"boolean"},"email":{"format":"email","type":"string"}},"required":["email"],"type":"object"},"Partner":{"properties":{"name":{"type":"string"},"scopes":{"description":"The scopes THIS key holds \u2014 check here when a call returns `insufficient_scope`.","items":{"enum":["partner.read","partner.keys.write","partner.firms.write"],"type":"string"},"type":"array"},"status":{"description":"While `paused`, issuing keys is blocked; reads keep working.","enum":["active","paused"],"type":"string"}},"type":"object"},"PartnerOnboarding":{"properties":{"return_url":{"description":"Where a firm admin is sent once they answer. Null means they stay in the Magnetic portal.","nullable":true,"type":"string"}},"type":"object"},"PartnerWebhook":{"properties":{"webhook_signing_secret":{"description":"Verify `X-Magnetic-Signature` on onboarding deliveries with this.","nullable":true,"type":"string"},"webhook_url":{"nullable":true,"type":"string"}},"type":"object"},"SandboxAcceptResult":{"properties":{"already_accepted":{"description":"Whether this call found the firm already accepted, rather than causing the acceptance. `false` \u2014 this call performed the transition and `firm.management_accepted` was dispatched, so expect the delivery. `true` \u2014 nothing changed and NO webhook was sent; you get this on a retry, or if a concurrent call won the race. Both are 200, since the call is idempotent. Unrelated to the `firm_already_accepted` error code, which is a 409 on deleting a firm that accepted your management.","type":"boolean"},"management_status":{"description":"Always `accepted` \u2014 the firm's state after the call.","enum":["accepted"],"type":"string"}},"required":["management_status","already_accepted"],"type":"object"},"UpdateFirmApiKeyRequest":{"properties":{"name":{"type":"string"},"submission_updated_webhook_url":{"description":"Empty string clears it. A non-empty change rotates the signing secret.","nullable":true,"type":"string"}},"type":"object"},"UpdatePartnerOnboardingRequest":{"properties":{"return_url":{"description":"https only, and no credentials in the URL. Null or an empty string clears it.","nullable":true,"type":"string"}},"required":["return_url"],"type":"object"},"UpdatePartnerWebhookRequest":{"properties":{"webhook_url":{"description":"https only. Empty string disables.","type":"string"}},"required":["webhook_url"],"type":"object"}},"securitySchemes":{"bearerAuth":{"description":"Your partner key (`magp_...`), issued from partner settings in the console.","scheme":"bearer","type":"http"}}},"info":{"description":"**Part of the [Integrations API](/v1/docs)** \u2014 the subset for managing the firms you serve and their API keys programmatically, instead of through the partner console. Same `/v1` surface, same conventions, same version; documented separately because it has a different reader and a different credential.\n\n**That credential is not your firm key.** Authenticate with a partner key (`Authorization: Bearer magp_...`), issued from partner settings in the console. A partner key manages firms and keys; it can never read or write a firm's client data. To do that, use the firm API key you issue here against the [rest of the API](/v1/docs).\n\n**Tenancy:** a partner key is not tied to one organization. Every endpoint takes the firm's `organization_id` in the path and checks that you manage it \u2014 a firm you don't manage returns `404`, never `403`, so org ids can't be probed.\n\n**Conventions** are the API's own throughout: enveloped list responses, the same stable error `code` vocabulary, and `Retry-After` on rate limits.","title":"Magnetic Partner API","version":"1.0.8"},"openapi":"3.0.3","paths":{"/v1/partner":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Partner"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing/invalid partner key"}},"summary":"Who am I \u2014 the partner this key acts for, and its scopes","tags":["Partner"]}},"/v1/partner/firms":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirmList"}}},"description":"OK"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The key is missing the required scope (`insufficient_scope`)"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited \u2014 retry after the `Retry-After` delay"}},"summary":"List the firms you manage","tags":["Firms"]},"post":{"description":"Creates the firm's organization and invites `firm_admin_email` as its admin \u2014 the person who accepts your management and owns the firm's settings. The firm starts as `pending` and becomes `accepted` once they accept; you can't issue its API keys until then.\n\n**Send an `Idempotency-Key`.** It is optional, as on every write, but this provisions a real organization \u2014 a retried timeout without one can leave a duplicate firm for someone to remove by hand. Use a unique key per firm you onboard and reuse it for every retry of that onboarding.","parameters":[{"description":"Optional. Retry a write with the same key and you get the ORIGINAL response back instead of the work happening twice \u2014 so a request that timed out is safe to repeat. Use one key per logical operation (a UUID), and reuse it for every retry of that operation. Keys are scoped to your API key and honoured for 24 hours. A replayed response carries the `Idempotent-Replayed: true` header \u2014 the body is byte-identical to the original, so that is the only way to tell stored work from fresh work. Reusing a key with a different request body returns 409 `idempotency_key_reuse`; replaying while the first request is still running returns 409 `idempotency_request_in_progress`. An empty header, or one over 255 characters, is rejected with 400 `invalid_idempotency_key` \u2014 omit it entirely to opt out.","in":"header","name":"Idempotency-Key","required":false,"schema":{"maxLength":255,"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFirmRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Firm"}}},"description":"Created \u2014 a real firm is `pending` until its admin accepts; a sandbox firm is `accepted` immediately"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid name / admin email, a non-boolean `sandbox` or `auto_accept`, or `auto_accept` sent at all on a non-sandbox firm"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The key is missing the required scope (`insufficient_scope`)"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sandbox firm limit reached (`sandbox_limit_reached`). Also returned for an Idempotency-Key conflict \u2014 reused for a different request (`idempotency_key_reuse`), or the original request is still running (`idempotency_request_in_progress`)"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited \u2014 retry after the `Retry-After` delay"}},"summary":"Onboard a firm","tags":["Firms"]}},"/v1/partner/firms/{org_id}":{"delete":{"description":"Removes a firm you created \u2014 the Clerk organization as well as your record of it, so nothing is left orphaned.\n\nOnly while it is still yours to remove: once a real firm has **accepted** your management the organization is theirs, and this returns `409 firm_already_accepted`. Sandbox firms can always be deleted, which is how you free a slot against the 5-sandbox limit. A firm you did not onboard returns `404`.","parameters":[{"description":"Optional. Retry a write with the same key and you get the ORIGINAL response back instead of the work happening twice \u2014 so a request that timed out is safe to repeat. Use one key per logical operation (a UUID), and reuse it for every retry of that operation. Keys are scoped to your API key and honoured for 24 hours. A replayed response carries the `Idempotent-Replayed: true` header \u2014 the body is byte-identical to the original, so that is the only way to tell stored work from fresh work. Reusing a key with a different request body returns 409 `idempotency_key_reuse`; replaying while the first request is still running returns 409 `idempotency_request_in_progress`. An empty header, or one over 255 characters, is rejected with 400 `invalid_idempotency_key` \u2014 omit it entirely to opt out.","in":"header","name":"Idempotency-Key","required":false,"schema":{"maxLength":255,"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirmDeleteResult"}}},"description":"Deleted"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The key is missing the required scope (`insufficient_scope`)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found \u2014 or not a firm this partner manages"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The firm already accepted your management (`firm_already_accepted`), or it is your default sandbox (`default_sandbox_protected`). Also returned for an Idempotency-Key conflict \u2014 reused for a different request (`idempotency_key_reuse`), or the original request is still running (`idempotency_request_in_progress`)"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited \u2014 retry after the `Retry-After` delay"}},"summary":"Delete a firm you onboarded","tags":["Firms"]},"get":{"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Firm"}}},"description":"OK"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The key is missing the required scope (`insufficient_scope`)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found \u2014 or not a firm this partner manages"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited \u2014 retry after the `Retry-After` delay"}},"summary":"Get one managed firm","tags":["Firms"]},"parameters":[{"description":"The firm's `organization_id`, as returned by `GET /v1/partner/firms`.","example":"org_3Gpc0EoJuPJxgIiTCm6v2K6htvr","in":"path","name":"org_id","required":true,"schema":{"type":"string"}}]},"/v1/partner/firms/{org_id}/api_keys":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirmApiKeyList"}}},"description":"OK"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The key is missing the required scope (`insufficient_scope`)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found \u2014 or not a firm this partner manages"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited \u2014 retry after the `Retry-After` delay"}},"summary":"List the keys you issued for this firm","tags":["Firm API keys"]},"parameters":[{"description":"The firm's `organization_id`, as returned by `GET /v1/partner/firms`.","example":"org_3Gpc0EoJuPJxgIiTCm6v2K6htvr","in":"path","name":"org_id","required":true,"schema":{"type":"string"}}],"post":{"description":"Returns the key exactly once \u2014 store it immediately, it cannot be retrieved again. Supplying a `submission_updated_webhook_url` also mints that key's webhook signing secret, likewise returned once.\n\n**Send an `Idempotency-Key`.** Without one a retried request issues a SECOND key; with one the retry returns the original response and no extra key exists.","parameters":[{"description":"Optional. Retry a write with the same key and you get the ORIGINAL response back instead of the work happening twice \u2014 so a request that timed out is safe to repeat. Use one key per logical operation (a UUID), and reuse it for every retry of that operation. Keys are scoped to your API key and honoured for 24 hours. A replayed response carries the `Idempotent-Replayed: true` header \u2014 the body is byte-identical to the original, so that is the only way to tell stored work from fresh work. Reusing a key with a different request body returns 409 `idempotency_key_reuse`; replaying while the first request is still running returns 409 `idempotency_request_in_progress`. An empty header, or one over 255 characters, is rejected with 400 `invalid_idempotency_key` \u2014 omit it entirely to opt out.","in":"header","name":"Idempotency-Key","required":false,"schema":{"maxLength":255,"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFirmApiKeyRequest"}}},"required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirmApiKeyCreated"}}},"description":"Created \u2014 the only time the key is shown"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The key is missing the required scope (`insufficient_scope`)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found \u2014 or not a firm this partner manages"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The firm hasn't accepted your management yet (`firm_not_accepted`). Also returned for an Idempotency-Key conflict \u2014 reused for a different request (`idempotency_key_reuse`), or the original request is still running (`idempotency_request_in_progress`)"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited \u2014 retry after the `Retry-After` delay"}},"summary":"Issue an API key for this firm","tags":["Firm API keys"]}},"/v1/partner/firms/{org_id}/api_keys/{api_key_id}":{"delete":{"description":"The key stops working immediately. Any integration using it breaks; this can't be undone.","parameters":[{"description":"Optional. Retry a write with the same key and you get the ORIGINAL response back instead of the work happening twice \u2014 so a request that timed out is safe to repeat. Use one key per logical operation (a UUID), and reuse it for every retry of that operation. Keys are scoped to your API key and honoured for 24 hours. A replayed response carries the `Idempotent-Replayed: true` header \u2014 the body is byte-identical to the original, so that is the only way to tell stored work from fresh work. Reusing a key with a different request body returns 409 `idempotency_key_reuse`; replaying while the first request is still running returns 409 `idempotency_request_in_progress`. An empty header, or one over 255 characters, is rejected with 400 `invalid_idempotency_key` \u2014 omit it entirely to opt out.","in":"header","name":"Idempotency-Key","required":false,"schema":{"maxLength":255,"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResult"}}},"description":"OK"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The key is missing the required scope (`insufficient_scope`)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found \u2014 or not a firm this partner manages"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Idempotency-Key conflict \u2014 reused for a different request (`idempotency_key_reuse`), or the original request is still running (`idempotency_request_in_progress`)"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited \u2014 retry after the `Retry-After` delay"}},"summary":"Revoke a key","tags":["Firm API keys"]},"parameters":[{"description":"The firm's `organization_id`, as returned by `GET /v1/partner/firms`.","example":"org_3Gpc0EoJuPJxgIiTCm6v2K6htvr","in":"path","name":"org_id","required":true,"schema":{"type":"string"}},{"example":116,"in":"path","name":"api_key_id","required":true,"schema":{"type":"integer"}}],"patch":{"description":"Send `name` to rename, or `submission_updated_webhook_url` to re-point the webhook (empty string clears it).\n\n**Setting or changing a non-empty URL rotates that key's signing secret**, and the new secret is returned once in `webhook_signing_secret`. The previous secret stops verifying immediately, so update your handler before you switch traffic over.","parameters":[{"description":"Optional. Retry a write with the same key and you get the ORIGINAL response back instead of the work happening twice \u2014 so a request that timed out is safe to repeat. Use one key per logical operation (a UUID), and reuse it for every retry of that operation. Keys are scoped to your API key and honoured for 24 hours. A replayed response carries the `Idempotent-Replayed: true` header \u2014 the body is byte-identical to the original, so that is the only way to tell stored work from fresh work. Reusing a key with a different request body returns 409 `idempotency_key_reuse`; replaying while the first request is still running returns 409 `idempotency_request_in_progress`. An empty header, or one over 255 characters, is rejected with 400 `invalid_idempotency_key` \u2014 omit it entirely to opt out.","in":"header","name":"Idempotency-Key","required":false,"schema":{"maxLength":255,"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFirmApiKeyRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirmApiKeyUpdated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Nothing to update \u2014 send `name` and/or `submission_updated_webhook_url`"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The key is missing the required scope (`insufficient_scope`)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found \u2014 or not a firm this partner manages"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Idempotency-Key conflict \u2014 reused for a different request (`idempotency_key_reuse`), or the original request is still running (`idempotency_request_in_progress`)"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited \u2014 retry after the `Retry-After` delay"}},"summary":"Rename a key, or set/clear its webhook URL","tags":["Firm API keys"]}},"/v1/partner/firms/{org_id}/invitations":{"parameters":[{"description":"The firm's `organization_id`, as returned by `GET /v1/partner/firms`.","example":"org_3Gpc0EoJuPJxgIiTCm6v2K6htvr","in":"path","name":"org_id","required":true,"schema":{"type":"string"}}],"post":{"description":"Invites a person to the firm \u2014 an admin by default, or a member with `as_admin: false`. The firm must already have **accepted** your management.\n\n**Send an `Idempotency-Key`.** Optional, as on every write, but an invitation sends email, so a retry without one means a second message to a real person. Inviting an address that already has a PENDING invitation returns `200` and sends nothing, so a repeated call is safe as well as a retried one. Re-inviting at a DIFFERENT role withdraws the pending invitation and sends the new one (`201`), so correcting a mistaken admin invite down to member takes effect.","parameters":[{"description":"Optional. Retry a write with the same key and you get the ORIGINAL response back instead of the work happening twice \u2014 so a request that timed out is safe to repeat. Use one key per logical operation (a UUID), and reuse it for every retry of that operation. Keys are scoped to your API key and honoured for 24 hours. A replayed response carries the `Idempotent-Replayed: true` header \u2014 the body is byte-identical to the original, so that is the only way to tell stored work from fresh work. Reusing a key with a different request body returns 409 `idempotency_key_reuse`; replaying while the first request is still running returns 409 `idempotency_request_in_progress`. An empty header, or one over 255 characters, is rejected with 400 `invalid_idempotency_key` \u2014 omit it entirely to opt out.","in":"header","name":"Idempotency-Key","required":false,"schema":{"maxLength":255,"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteFirmUserRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirmInvitation"}}},"description":"Already invited \u2014 this address had a pending invitation, so nothing was sent again"},"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirmInvitation"}}},"description":"Invitation sent"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid email, or a non-boolean `as_admin`"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The key is missing the required scope (`insufficient_scope`)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found \u2014 or not a firm this partner manages"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The firm hasn't accepted your management yet (`firm_not_accepted`), or a pending invitation at a different role could not be withdrawn (`invitation_role_conflict`). Also returned for an Idempotency-Key conflict \u2014 reused for a different request (`idempotency_key_reuse`), or the original request is still running (`idempotency_request_in_progress`)"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited \u2014 retry after the `Retry-After` delay"}},"summary":"Invite someone to a managed firm","tags":["Firms"]}},"/v1/partner/firms/{org_id}/sandbox/accept":{"parameters":[{"description":"The firm's `organization_id`, as returned by `GET /v1/partner/firms`.","example":"org_3Gpc0EoJuPJxgIiTCm6v2K6htvr","in":"path","name":"org_id","required":true,"schema":{"type":"string"}}],"post":{"description":"Performs the acceptance a firm admin would perform in the app, on a **sandbox** firm \u2014 so you can rehearse onboarding without a human, an inbox, or a phone number.\n\nThis runs the same transition as the real thing and fires the same `firm.management_accepted` webhook, so what you receive is identical to production. Pair it with `auto_accept: false` on `POST /v1/partner/firms`: create the firm pending, confirm issuing its key returns 409 `firm_not_accepted`, call this, handle the webhook, then issue the key.\n\nRefused with 403 `sandbox_only` on a real firm \u2014 only a firm's own admin can accept its management. Calling it on a firm that has already accepted returns 200 with `already_accepted: true` and sends no second webhook.","parameters":[{"description":"Optional. Retry a write with the same key and you get the ORIGINAL response back instead of the work happening twice \u2014 so a request that timed out is safe to repeat. Use one key per logical operation (a UUID), and reuse it for every retry of that operation. Keys are scoped to your API key and honoured for 24 hours. A replayed response carries the `Idempotent-Replayed: true` header \u2014 the body is byte-identical to the original, so that is the only way to tell stored work from fresh work. Reusing a key with a different request body returns 409 `idempotency_key_reuse`; replaying while the first request is still running returns 409 `idempotency_request_in_progress`. An empty header, or one over 255 characters, is rejected with 400 `invalid_idempotency_key` \u2014 omit it entirely to opt out.","in":"header","name":"Idempotency-Key","required":false,"schema":{"maxLength":255,"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxAcceptResult"}}},"description":"Accepted \u2014 `already_accepted` distinguishes a fresh transition from a repeat call"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a sandbox firm (`sandbox_only`), or your key lacks `partner.firms.write`"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found \u2014 or not a firm this partner manages"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"This firm's management was revoked (`firm_revoked`). Also returned for an Idempotency-Key conflict \u2014 reused for a different request (`idempotency_key_reuse`), or the original request is still running (`idempotency_request_in_progress`)"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited \u2014 retry after the `Retry-After` delay"}},"summary":"Accept management on a sandbox firm (sandbox only)","tags":["Firms"]}},"/v1/partner/onboarding":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerOnboarding"}}},"description":"OK"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The key is missing the required scope (`insufficient_scope`)"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited \u2014 retry after the `Retry-After` delay"}},"summary":"Get the onboarding return URL","tags":["Onboarding"]},"patch":{"description":"Where we send a firm admin once they answer your management request, so a flow that STARTED on your platform ends there too. We append `status` (`accepted`, `declined` or `revoked`) and `organization_id` to it.\n\nThe redirect is a courtesy, not the record: a user can close the tab, and the answer still stands. Treat the signed `firm.management_*` webhook as the source of truth.\n\nhttps only, and no credentials in the URL \u2014 it is a destination for a real person's browser. Send `null` to clear it, after which a firm that answers simply stays in the Magnetic portal.","parameters":[{"description":"Optional. Retry a write with the same key and you get the ORIGINAL response back instead of the work happening twice \u2014 so a request that timed out is safe to repeat. Use one key per logical operation (a UUID), and reuse it for every retry of that operation. Keys are scoped to your API key and honoured for 24 hours. A replayed response carries the `Idempotent-Replayed: true` header \u2014 the body is byte-identical to the original, so that is the only way to tell stored work from fresh work. Reusing a key with a different request body returns 409 `idempotency_key_reuse`; replaying while the first request is still running returns 409 `idempotency_request_in_progress`. An empty header, or one over 255 characters, is rejected with 400 `invalid_idempotency_key` \u2014 omit it entirely to opt out.","in":"header","name":"Idempotency-Key","required":false,"schema":{"maxLength":255,"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePartnerOnboardingRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerOnboarding"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing `return_url`, or a URL that is not https / carries credentials"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The key is missing the required scope (`insufficient_scope`)"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Idempotency-Key conflict \u2014 reused for a different request (`idempotency_key_reuse`), or the original request is still running (`idempotency_request_in_progress`)"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited \u2014 retry after the `Retry-After` delay"}},"summary":"Set or clear the onboarding return URL","tags":["Onboarding"]}},"/v1/partner/webhook":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerWebhook"}}},"description":"OK"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The key is missing the required scope (`insufficient_scope`)"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited \u2014 retry after the `Retry-After` delay"}},"summary":"Get the partner-level webhook config","tags":["Webhook"]},"patch":{"description":"We POST a signed event here whenever a managed firm answers, so onboarding can react without polling: `firm.management_accepted`, `firm.management_declined` (they said not now \u2014 it stays pending and you may ask again), and `firm.management_revoked` (a firm that had accepted ended it; its keys stop working). Handle all three \u2014 listening only for `accepted` waits forever on a firm that declined. Send an empty string to disable.","parameters":[{"description":"Optional. Retry a write with the same key and you get the ORIGINAL response back instead of the work happening twice \u2014 so a request that timed out is safe to repeat. Use one key per logical operation (a UUID), and reuse it for every retry of that operation. Keys are scoped to your API key and honoured for 24 hours. A replayed response carries the `Idempotent-Replayed: true` header \u2014 the body is byte-identical to the original, so that is the only way to tell stored work from fresh work. Reusing a key with a different request body returns 409 `idempotency_key_reuse`; replaying while the first request is still running returns 409 `idempotency_request_in_progress`. An empty header, or one over 255 characters, is rejected with 400 `invalid_idempotency_key` \u2014 omit it entirely to opt out.","in":"header","name":"Idempotency-Key","required":false,"schema":{"maxLength":255,"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePartnerWebhookRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerWebhook"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid URL (https required)"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The key is missing the required scope (`insufficient_scope`)"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Idempotency-Key conflict \u2014 reused for a different request (`idempotency_key_reuse`), or the original request is still running (`idempotency_request_in_progress`)"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited \u2014 retry after the `Retry-After` delay"}},"summary":"Set or clear the partner-level webhook URL","tags":["Webhook"]}}},"security":[{"bearerAuth":[]}],"servers":[{"url":"/"}],"tags":[{"description":"Who the calling key acts for.","name":"Partner"},{"description":"The firms this partner manages.","name":"Firms"},{"description":"Issue, rename, re-point and revoke the keys a firm's integration uses.","name":"Firm API keys"},{"description":"The partner-level onboarding webhook (`firm.management_*`).","name":"Webhook"},{"description":"Where a firm admin returns to after answering your management request.","name":"Onboarding"}]}
