{"openapi":"3.1.0","info":{"title":"Spectrum Cloud External API","description":"Client-facing API for Spectrum Cloud.","version":"1.0.0"},"servers":[{"url":"https://spectrum.photon.codes"}],"tags":[{"name":"billing","description":"Billing and subscription operations"},{"name":"imessage","description":"iMessage platform operations"},{"name":"lines","description":"Phone line enumeration across platforms"},{"name":"platforms","description":"Platform management operations"},{"name":"users","description":"User operations"},{"name":"voice","description":"Voice platform operations"},{"name":"webhooks","description":"Webhook registration for receiving Spectrum events"},{"name":"whatsapp-business","description":"WhatsApp Business platform operations"},{"name":"slack","description":"Slack platform operations"}],"components":{"schemas":{"ErrorResponse":{"type":"object","properties":{"succeed":{"type":"boolean","enum":[false]},"data":{"nullable":true},"code":{"type":"string"},"message":{"type":"string"}},"required":["succeed","data","code","message"]}}},"paths":{"/users/{userId}/redirect":{"get":{"summary":"Redirect user to messaging platform","description":"Public endpoint that redirects to the appropriate messaging platform for a shared user. Currently supports iMessage via SMS deep link. Pass an optional `msg` query parameter to override the default message body.","tags":["users"],"responses":{"302":{"description":"Redirect to the messaging platform deep link (e.g. sms: URL).","headers":{"Location":{"description":"Messaging platform deep link URL.","schema":{"type":"string"}}}},"403":{"description":"User is not a shared user, or the messaging platform is not enabled for the project."},"404":{"description":"User not found."},"422":{"description":"User does not have an assigned phone number."}},"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"msg","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":1000}}],"operationId":"getUsersByUserIdRedirect"}},"/projects/{projectId}/":{"get":{"summary":"Get project","description":"Returns the project's display name plus, when set, its profile (firstName, lastName, avatarUrl). When a profile is set, the response also includes `imessageSynced`: true iff the project is Business-tier and every active dedicated iMessage Line has its current Profile successfully applied. Free/Pro projects ride a shared line and always report false. Zero active dedicated Lines on Business is considered synced. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["projects"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"},"profile":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"avatarUrl":{"anyOf":[{"type":"string"},{"type":"null"}]},"imessageSynced":{"type":"boolean"}},"required":["firstName","lastName","avatarUrl","imessageSynced"],"additionalProperties":false}},"required":["name","slug","profile"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectId"}},"/projects/{projectId}/profile":{"get":{"summary":"Get project profile","description":"Returns the Project Profile through the Project Basic Auth boundary.","tags":["projects"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"projectId":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"avatarUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["projectId","firstName","lastName","avatarUrl"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdProfile"},"patch":{"summary":"Update project profile","description":"Updates the supplied Project name fields and conditionally propagates the complete Profile to Lines that still match the old Project Profile.","tags":["projects"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"}},"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"}},"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"}},"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"projectId":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"avatarUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["projectId","firstName","lastName","avatarUrl"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"patchProjectsByProjectIdProfile"}},"/projects/{projectId}/profile/avatar/upload":{"post":{"summary":"Get project avatar upload URL","description":"Returns a presigned PUT URL and the Project-bound key to commit after uploading the avatar.","tags":["projects"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"default":{},"type":"object","properties":{"contentType":{"type":"string","pattern":"^image\\/"}},"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"default":{},"type":"object","properties":{"contentType":{"type":"string","pattern":"^image\\/"}},"additionalProperties":false}},"multipart/form-data":{"schema":{"default":{},"type":"object","properties":{"contentType":{"type":"string","pattern":"^image\\/"}},"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"uploadUrl":{"type":"string"},"key":{"type":"string"}},"required":["uploadUrl","key"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdProfileAvatarUpload"}},"/projects/{projectId}/profile/avatar/commit":{"post":{"summary":"Commit project avatar","description":"Validates the uploaded image and conditionally propagates the complete Project Profile.","tags":["projects"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string"}},"required":["key"],"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"key":{"type":"string"}},"required":["key"],"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"key":{"type":"string"}},"required":["key"],"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"avatarUrl":{"type":"string"}},"required":["avatarUrl"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdProfileAvatarCommit"}},"/projects/{projectId}/profile/sync":{"post":{"summary":"Sync project profile to lines","description":"Idempotently aligns every active dedicated iMessage Line Profile to the Project Profile and returns after the database transaction commits.","tags":["projects"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"projectId":{"type":"string"},"targetedLineCount":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["projectId","targetedLineCount"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdProfileSync"},"get":{"summary":"Get project profile sync status","description":"Returns current aggregate Line convergence with no sync instance ID or history.","tags":["projects"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"projectId":{"type":"string"},"status":{"type":"string","enum":["in_progress","completed","partial_failed","failed"]},"total":{"type":"integer","minimum":0,"maximum":9007199254740991},"pending":{"type":"integer","minimum":0,"maximum":9007199254740991},"synced":{"type":"integer","minimum":0,"maximum":9007199254740991},"failed":{"type":"integer","minimum":0,"maximum":9007199254740991},"errors":{"type":"array","items":{"type":"object","properties":{"lineId":{"type":"string"},"reason":{"type":"string"}},"required":["lineId","reason"],"additionalProperties":false}}},"required":["projectId","status","total","pending","synced","failed","errors"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdProfileSync"}},"/projects/{projectId}/billing/subscription":{"get":{"summary":"Get subscription details","description":"Returns the current plan tier and subscription status for a project. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["billing"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"tier":{"type":"string"},"status":{"anyOf":[{"type":"string","enum":["active","canceled","past_due"]},{"type":"null"}]},"cancel_at_period_end":{"type":"boolean"},"subscription_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"customer_id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["tier","status","cancel_at_period_end","subscription_id","customer_id"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdBillingSubscription"}},"/projects/{projectId}/billing/status":{"get":{"summary":"Get billing sync status","description":"Returns the project's async-billing sync state. After POST/DELETE /lines, poll this endpoint until `syncStatus !== 'syncing'` to read the final proration. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["billing"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"syncStatus":{"type":"string","enum":["in_sync","syncing","failed"]},"lastSyncedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"error":{"anyOf":[{"type":"string"},{"type":"null"}]},"quantity":{"type":"number"},"lastProrationAmount":{"anyOf":[{"type":"number"},{"type":"null"}]}},"required":["syncStatus","lastSyncedAt","error","quantity","lastProrationAmount"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdBillingStatus"}},"/projects/{projectId}/fusor/token":{"post":{"summary":"Issue Fusor token","description":"Issues a short-lived LightAuth JWT bound to the Photon Fusor service (`codes.photon.spectrum.fusor`) and the requesting project. Use Authorization: Basic base64(projectId:projectSecret). Returns `{ token, expiresIn }` where `expiresIn` is the TTL in seconds. The token's `sub` is the project id; downstream Fusor services should treat it as the project-scoped capability.","tags":["fusor"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"token":{"type":"string"},"expiresIn":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["token","expiresIn"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdFusorToken"}},"/projects/{projectId}/imessage/tokens":{"post":{"summary":"Issue iMessage tokens","description":"Issues iMessage tokens for a project. Use Authorization: Basic base64(projectId:projectSecret). Dedicated projects return per-instance LightAuth tokens keyed by instance ID in `auth`, along with a parallel `numbers` map of instance ID to phone number. Shared projects return a single LightAuth token. All returned tokens include their TTL in seconds.","tags":["imessage"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"dedicated"},"auth":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"numbers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"expiresIn":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["type","auth","numbers","expiresIn"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"shared"},"token":{"type":"string"},"expiresIn":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["type","token","expiresIn"],"additionalProperties":false}]}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdImessageTokens"}},"/projects/{projectId}/imessage/":{"get":{"summary":"Get iMessage info","description":"Returns the iMessage service type (shared or dedicated) for the project. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["imessage"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"type":{"type":"string","enum":["shared","dedicated"]}},"required":["type"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdImessage"}},"/projects/{projectId}/imessage/shared/availability":{"get":{"summary":"Check shared phone number availability","description":"Checks whether a new shared iMessage number can be assigned to the given phone number under this project. Mirrors the allocation rules used by `POST /projects/:projectId/users`, including reuse of a soft-deleted user's previously assigned number within the same project. Returns `false` only when the project has no soft-deleted slot to reuse and every Cosmos shared number is already assigned globally for this phone — except that numbers assigned by a since-deleted project this phone owned (`meta.project_owner`) are released back and count as assignable. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["imessage"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"phoneNumber","in":"query","required":true,"schema":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"available":{"type":"boolean"}},"required":["available"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdImessageSharedAvailability"}},"/projects/{projectId}/lines/":{"get":{"summary":"List project lines","description":"Returns only **dedicated** phone lines the project owns across all platforms. Use the optional `platform` query parameter to filter to a single platform. On iMessage Free or Pro plans, lines are assigned per-user rather than dedicated to the project — for those cases, redirect users via `GET /users/{userId}/redirect` instead. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["lines"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"platform","in":"query","required":false,"schema":{"type":"string","enum":["imessage","whatsapp_business","voice","slack"]}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"lines":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"platform":{"type":"string","const":"imessage"},"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"phoneNumber":{"type":"string"},"profile":{"type":"object","properties":{"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"avatarUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["firstName","lastName","avatarUrl"],"additionalProperties":false},"status":{"type":"string","enum":["available","unavailable","unknown"]},"createdAt":{"type":"string"}},"required":["platform","id","phoneNumber","profile","status","createdAt"],"additionalProperties":false},{"oneOf":[{"type":"object","properties":{"platform":{"type":"string","const":"whatsapp_business"},"state":{"type":"string","const":"registered"},"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"phoneNumberId":{"type":"string"},"displayPhoneNumber":{"anyOf":[{"type":"string"},{"type":"null"}]},"verifiedName":{"anyOf":[{"type":"string"},{"type":"null"}]},"qualityRating":{"anyOf":[{"type":"string"},{"type":"null"}]},"status":{"anyOf":[{"type":"string"},{"type":"null"}]},"codeVerificationStatus":{"anyOf":[{"type":"string"},{"type":"null"}]},"wabaId":{"type":"string"},"businessName":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"}},"required":["platform","state","id","phoneNumberId","displayPhoneNumber","verifiedName","qualityRating","status","codeVerificationStatus","wabaId","businessName","createdAt"],"additionalProperties":false},{"type":"object","properties":{"platform":{"type":"string","const":"whatsapp_business"},"state":{"type":"string","const":"pending"},"phoneNumberId":{"type":"string"},"displayPhoneNumber":{"anyOf":[{"type":"string"},{"type":"null"}]},"registrationState":{"type":"string","enum":["registering","failed"]},"errorCode":{"anyOf":[{"type":"string"},{"type":"null"}]},"errorMessage":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"}},"required":["platform","state","phoneNumberId","displayPhoneNumber","registrationState","errorCode","errorMessage","createdAt"],"additionalProperties":false}]}]}}},"required":["lines"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdLines"},"post":{"summary":"Add a dedicated iMessage line","description":"Allocates a new dedicated iMessage phone number for the project and updates the Stripe subscription quantity with pro-rated billing. Business plan only. iMessage only for now; WhatsApp Business onboarding flows through the Meta registration endpoints. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["lines"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"platform":{"type":"string","const":"imessage"}},"required":["platform"],"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"platform":{"type":"string","const":"imessage"}},"required":["platform"],"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"platform":{"type":"string","const":"imessage"}},"required":["platform"],"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"line":{"type":"object","properties":{"platform":{"type":"string","const":"imessage"},"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"phoneNumber":{"type":"string"},"profile":{"type":"object","properties":{"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"avatarUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["firstName","lastName","avatarUrl"],"additionalProperties":false},"status":{"type":"string","enum":["available","unavailable","unknown"]},"createdAt":{"type":"string"}},"required":["platform","id","phoneNumber","profile","status","createdAt"],"additionalProperties":false},"billing":{"type":"object","properties":{"quantity":{"anyOf":[{"type":"number"},{"type":"null"}]},"prorationAmount":{"anyOf":[{"type":"number"},{"type":"null"}]},"syncStatus":{"type":"string","enum":["in_sync","syncing","failed"]}},"required":["quantity","prorationAmount","syncStatus"],"additionalProperties":false}},"required":["line","billing"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdLines"}},"/projects/{projectId}/lines/route":{"get":{"summary":"Route a new user to the best dedicated iMessage line","description":"Returns the single best dedicated iMessage line to assign a new user to, load-balancing by active user count and recent growth. Lines provisioned less than 15 minutes ago are skipped while an older line exists: running Spectrum SDK instances discover new lines at token renewal, so routing users to a brand-new line could strand their messages on replicas that cannot see it yet. `isBestAvailable` is `false` when the returned line is the least-bad fallback — it holds more than 400 users or grew by more than 2 users in the last minute, meaning no genuinely healthy line was available. Returns 404 if the project owns no dedicated iMessage lines. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["lines"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"line":{"type":"object","properties":{"platform":{"type":"string","const":"imessage"},"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"phoneNumber":{"type":"string"},"profile":{"type":"object","properties":{"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"avatarUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["firstName","lastName","avatarUrl"],"additionalProperties":false},"status":{"type":"string","enum":["available","unavailable","unknown"]},"createdAt":{"type":"string"}},"required":["platform","id","phoneNumber","profile","status","createdAt"],"additionalProperties":false},"isBestAvailable":{"type":"boolean"}},"required":["line","isBestAvailable"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdLinesRoute"}},"/projects/{projectId}/lines/{lineId}/profile":{"get":{"summary":"Get a dedicated iMessage line profile","description":"Returns the stored name and avatar for this dedicated iMessage line. Requires Project Basic Auth.","tags":["lines"],"parameters":[{"name":"lineId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"lineId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"phoneNumber":{"type":"string"},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"avatarUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["projectId","lineId","phoneNumber","firstName","lastName","avatarUrl"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdLinesByLineIdProfile"},"patch":{"summary":"Update a dedicated iMessage line name","description":"Merges firstName and/or lastName into this line's stored Profile while preserving its other name and avatar. Requires Project Basic Auth.","tags":["lines"],"parameters":[{"name":"lineId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]}},"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]}},"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]}},"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"lineId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"phoneNumber":{"type":"string"},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"avatarUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["projectId","lineId","phoneNumber","firstName","lastName","avatarUrl"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"patchProjectsByProjectIdLinesByLineIdProfile"}},"/projects/{projectId}/lines/{lineId}/profile/avatar/upload":{"post":{"summary":"Get a line avatar upload URL","description":"Returns a presigned upload URL and a storage key bound to this project and dedicated iMessage line. Requires Project Basic Auth.","tags":["lines"],"parameters":[{"name":"lineId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"default":{},"type":"object","properties":{"contentType":{"type":"string","pattern":"^image\\/"}},"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"default":{},"type":"object","properties":{"contentType":{"type":"string","pattern":"^image\\/"}},"additionalProperties":false}},"multipart/form-data":{"schema":{"default":{},"type":"object","properties":{"contentType":{"type":"string","pattern":"^image\\/"}},"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"lineId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"uploadUrl":{"type":"string"},"key":{"type":"string"}},"required":["projectId","lineId","uploadUrl","key"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdLinesByLineIdProfileAvatarUpload"}},"/projects/{projectId}/lines/{lineId}/profile/avatar/commit":{"post":{"summary":"Commit a line avatar","description":"Verifies an uploaded image key bound to this line and updates its stored Profile while preserving its name. Requires Project Basic Auth.","tags":["lines"],"parameters":[{"name":"lineId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string","minLength":1}},"required":["key"],"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"key":{"type":"string","minLength":1}},"required":["key"],"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"key":{"type":"string","minLength":1}},"required":["key"],"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"lineId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"phoneNumber":{"type":"string"},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"avatarUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["projectId","lineId","phoneNumber","firstName","lastName","avatarUrl"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdLinesByLineIdProfileAvatarCommit"}},"/projects/{projectId}/lines/{lineId}":{"delete":{"summary":"Remove a dedicated line","description":"Deallocates a dedicated line by id. For iMessage lines this decrements the Stripe subscription quantity with pro-rated credit (business plan only), updating the quantity before the Cosmos deallocation so the customer is never billed for a line they can no longer use. WhatsApp Business lines are removed without a billing change and return `billing: null`. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["lines"],"parameters":[{"name":"lineId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"billing":{"anyOf":[{"type":"object","properties":{"quantity":{"anyOf":[{"type":"number"},{"type":"null"}]},"prorationAmount":{"anyOf":[{"type":"number"},{"type":"null"}]},"syncStatus":{"type":"string","enum":["in_sync","syncing","failed"]}},"required":["quantity","prorationAmount","syncStatus"],"additionalProperties":false},{"type":"null"}]}},"required":["billing"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"deleteProjectsByProjectIdLinesByLineId"}},"/projects/{projectId}/platforms/":{"get":{"summary":"Get platforms","description":"Returns the project's platform configuration, including disabled entries (metadata is preserved across toggles). Requires Authorization: Basic base64(projectId:projectSecret).","tags":["platforms"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"imessage":{"type":"object","properties":{"autoScale":{"type":"boolean"},"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false},"whatsapp_business":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false},"voice":{"type":"object","properties":{"imessage_enabled":{"type":"boolean"},"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false},"slack":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false}},"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdPlatforms"},"patch":{"summary":"Toggle platform","description":"Enables or disables a platform for the project. Preserves any previously-stored metadata across toggles. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["platforms"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"platform":{"type":"string","enum":["imessage","whatsapp_business","voice","slack"]},"enabled":{"type":"boolean"}},"required":["platform","enabled"],"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"platform":{"type":"string","enum":["imessage","whatsapp_business","voice","slack"]},"enabled":{"type":"boolean"}},"required":["platform","enabled"],"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"platform":{"type":"string","enum":["imessage","whatsapp_business","voice","slack"]},"enabled":{"type":"boolean"}},"required":["platform","enabled"],"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"imessage":{"type":"object","properties":{"autoScale":{"type":"boolean"},"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false},"whatsapp_business":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false},"voice":{"type":"object","properties":{"imessage_enabled":{"type":"boolean"},"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false},"slack":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false}},"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"patchProjectsByProjectIdPlatforms"}},"/projects/{projectId}/platforms/{platform}":{"patch":{"summary":"Update platform metadata","description":"Updates platform-specific metadata. Can only be called after the platform is enabled (returns 409 otherwise). Use `PATCH /platforms` to toggle `enabled`. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["platforms"],"parameters":[{"name":"platform","in":"path","required":true,"schema":{"type":"string","enum":["imessage","whatsapp_business","voice","slack"]}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"autoScale":{"type":"boolean"}},"additionalProperties":false},{"type":"object","properties":{},"additionalProperties":false},{"type":"object","properties":{"imessage_enabled":{"type":"boolean"}},"additionalProperties":false}]}},"application/x-www-form-urlencoded":{"schema":{"anyOf":[{"type":"object","properties":{"autoScale":{"type":"boolean"}},"additionalProperties":false},{"type":"object","properties":{},"additionalProperties":false},{"type":"object","properties":{"imessage_enabled":{"type":"boolean"}},"additionalProperties":false}]}},"multipart/form-data":{"schema":{"anyOf":[{"type":"object","properties":{"autoScale":{"type":"boolean"}},"additionalProperties":false},{"type":"object","properties":{},"additionalProperties":false},{"type":"object","properties":{"imessage_enabled":{"type":"boolean"}},"additionalProperties":false}]}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"imessage":{"type":"object","properties":{"autoScale":{"type":"boolean"},"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false},"whatsapp_business":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false},"voice":{"type":"object","properties":{"imessage_enabled":{"type":"boolean"},"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false},"slack":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false}},"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"patchProjectsByProjectIdPlatformsByPlatform"}},"/projects/{projectId}/slug/":{"patch":{"summary":"Update project slug","description":"Replaces the project's slug with the supplied value.\n\nThe slug must match `^[a-z0-9](?:[a-z0-9-]{0,8}[a-z0-9])?$` — 1–10 characters of lowercase letters, digits, and hyphens, with no leading or trailing hyphen. Invalid formats return `422`.\n\nReturns `409` if another active project already owns the slug. Slugs from soft-deleted projects are reusable.\n\nRequires `Authorization: Basic base64(projectId:projectSecret)`.","tags":["projects"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"projectSlug":{"type":"string"}},"required":["projectSlug"],"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"projectSlug":{"type":"string"}},"required":["projectSlug"],"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"projectSlug":{"type":"string"}},"required":["projectSlug"],"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"projectId":{"type":"string"},"projectSlug":{"type":"string"}},"required":["projectId","projectSlug"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"patchProjectsByProjectIdSlug"}},"/projects/{projectId}/users/":{"post":{"summary":"Create user","description":"Creates a new user for the project. Pass `type: \"shared\"` for a shared user (server assigns a phone number from the Cosmos pool and enforces `maxSharedUsers`). A shared user is identified by `phoneNumber` within the project: re-creating with an existing active `phoneNumber` returns that same user and updates its `firstName`, `lastName`, and `email` from any values supplied (omitted/null fields are left unchanged; `maxSharedUsers` is not re-checked on an update). Pass `type: \"dedicated\"` for a dedicated user, which requires `assignedPhoneNumber` to match an iMessage line owned by the project; the operation is idempotent on the full `(phoneNumber, assignedPhoneNumber)` tuple — calling it again with the same tuple returns the existing row, while calling it with the same `phoneNumber` but a different `assignedPhoneNumber` creates a new row (the same user phone may be assigned to multiple lines). `firstName`, `lastName`, and `email` are optional in both cases. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["users"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"shared"},"phoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]}},"required":["type","phoneNumber"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"dedicated"},"phoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"assignedPhoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]}},"required":["type","phoneNumber","assignedPhoneNumber"],"additionalProperties":false}]}},"application/x-www-form-urlencoded":{"schema":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"shared"},"phoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]}},"required":["type","phoneNumber"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"dedicated"},"phoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"assignedPhoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]}},"required":["type","phoneNumber","assignedPhoneNumber"],"additionalProperties":false}]}},"multipart/form-data":{"schema":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"shared"},"phoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]}},"required":["type","phoneNumber"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"dedicated"},"phoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"assignedPhoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]}},"required":["type","phoneNumber","assignedPhoneNumber"],"additionalProperties":false}]}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"type":{"type":"string","enum":["shared","dedicated"]},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]},"phoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"assignedPhoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"meta":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"createdAt":{"type":"string"}},"required":["id","projectId","type","firstName","lastName","email","phoneNumber","assignedPhoneNumber","meta","createdAt"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdUsers"},"get":{"summary":"List users","description":"Returns active users for the project. Filter with `?type=shared|dedicated`, batch-fetch by id with repeated `?id=uuid&id=uuid` params, or `?search=` for a partial case-insensitive match on first/last name, phone number, and email. Pagination is opt-in: omit `limit`/`offset` to return all matches (default), or pass `limit` (max 500) and `offset` to page. `total` is the count of all matches ignoring pagination. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["users"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["shared","dedicated"]}},{"name":"id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}]}},{"name":"search","in":"query","required":false,"schema":{"type":"string","minLength":1}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"maximum":1000000}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"users":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"type":{"type":"string","enum":["shared","dedicated"]},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]},"phoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"assignedPhoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"meta":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"createdAt":{"type":"string"}},"required":["id","projectId","type","firstName","lastName","email","phoneNumber","assignedPhoneNumber","meta","createdAt"],"additionalProperties":false}},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["users","total"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdUsers"}},"/projects/{projectId}/users/{userId}/":{"get":{"summary":"Get user","description":"Returns a single user by ID. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["users"],"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"type":{"type":"string","enum":["shared","dedicated"]},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]},"phoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"assignedPhoneNumber":{"type":"string","format":"e164","pattern":"^\\+[1-9]\\d{6,14}$"},"meta":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"createdAt":{"type":"string"}},"required":["id","projectId","type","firstName","lastName","email","phoneNumber","assignedPhoneNumber","meta","createdAt"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdUsersByUserId"},"delete":{"summary":"Delete user","description":"Soft-deletes a user. The user will no longer appear in listings. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["users"],"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"userId":{"type":"string"}},"required":["userId"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"deleteProjectsByProjectIdUsersByUserId"}},"/projects/{projectId}/voice/sip-inbound/":{"get":{"summary":"Get SIP inbound config","description":"Returns the project's active SIP inbound configuration, or `data: null` if none is configured. The password is never returned; `hasPassword` indicates whether one is set. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["voice"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"anyOf":[{"type":"object","properties":{"configId":{"type":"string"},"projectId":{"type":"string"},"sipUri":{"type":"string"},"username":{"anyOf":[{"type":"string"},{"type":"null"}]},"hasPassword":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["configId","projectId","sipUri","username","hasPassword","createdAt","updatedAt"],"additionalProperties":false},{"type":"null"}]}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdVoiceSip-inbound"},"patch":{"summary":"Set or update SIP inbound config","description":"Upsert the project's SIP inbound configuration. On first call (no active config), `sipUri` is required and the project's voice platform must be enabled. On subsequent calls, any non-empty subset patches the existing config — omitted fields are preserved, explicit `null` for `username`/`password` clears them. Resulting state must satisfy: `username` and `password` are either both set or both null (half-credentials are rejected). Requires Authorization: Basic base64(projectId:projectSecret).","tags":["voice"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"sipUri":{"type":"string","pattern":"^sips?:"},"username":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"password":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]}},"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"sipUri":{"type":"string","pattern":"^sips?:"},"username":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"password":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]}},"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"sipUri":{"type":"string","pattern":"^sips?:"},"username":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"password":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]}},"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"configId":{"type":"string"},"projectId":{"type":"string"},"sipUri":{"type":"string"},"username":{"anyOf":[{"type":"string"},{"type":"null"}]},"hasPassword":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["configId","projectId","sipUri","username","hasPassword","createdAt","updatedAt"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"patchProjectsByProjectIdVoiceSip-inbound"},"delete":{"summary":"Delete SIP inbound config","description":"Soft-deletes the project's active SIP inbound configuration. Returns 404 if no active config exists. After deletion, a fresh config can be created via PATCH. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["voice"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"projectId":{"type":"string"}},"required":["projectId"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"deleteProjectsByProjectIdVoiceSip-inbound"}},"/projects/{projectId}/voice/tokens":{"post":{"summary":"Issue voice token","description":"Issues a single voice LightAuth token for the project. Use Authorization: Basic base64(projectId:projectSecret). Always returns one token regardless of dedicated/shared provisioning. The token's TTL (seconds) is included as `expiresIn`.","tags":["voice"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"token":{"type":"string"},"expiresIn":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["token","expiresIn"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdVoiceTokens"}},"/projects/{projectId}/webhooks/":{"get":{"summary":"List webhooks","description":"Returns the webhooks currently registered for the authenticated project, ordered by creation time (oldest first).\nEach row includes `schemaVersion`, the pinned payload contract for that endpoint. `normalized-events.v1` is the original normalized Spectrum event shape; `raw-inbound.v1` preserves the inbound provider request.\n\nSecrets are not included in list responses. The Standard secret is returned at registration and can be replaced with the rotation endpoint.\n\nRequires `Authorization: Basic base64(projectId:projectSecret)`.","tags":["webhooks"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"webhookUrl":{"type":"string","format":"uri"},"schemaVersion":{"type":"string","enum":["normalized-events.v1","raw-inbound.v1"]},"eventTypes":{"type":"array","items":{"type":"string","enum":["message.received"]}},"enabled":{"type":"boolean"},"status":{"type":"string","enum":["active","disabled"]},"failureNotificationEmail":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"disabledAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},{"type":"null"}]},"disabledReason":{"anyOf":[{"type":"string","enum":["manual","receiver_gone","delivery_failures"]},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}},"required":["id","webhookUrl","schemaVersion","eventTypes","enabled","status","failureNotificationEmail","disabledAt","disabledReason","createdAt","updatedAt"],"additionalProperties":false}}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdWebhooks"},"post":{"summary":"Register webhook","description":"Registers a destination URL for the authenticated project. `schemaVersion` defaults to `normalized-events.v1`, the normalized Spectrum event shape; choose `raw-inbound.v1` only if your project delivers on a platform Fusor already serves. Save both one-time secrets from the response: `standardSigningSecret` is the `whsec_` secret for Standard Webhooks libraries, while `signingSecret` verifies the legacy Spectrum header.\n\n### Delivery format\nEach event is delivered as an HTTP `POST` to your URL with these headers:\n\n| Header | Meaning |\n| --- | --- |\n| `webhook-id` | Stable event id. |\n| `webhook-timestamp` | UNIX epoch seconds at signing time. |\n| `webhook-signature` | One or more space-delimited `v1,<base64>` signatures. |\n| `X-Spectrum-Webhook-Id` | Legacy header. The id of this webhook, useful when you have multiple registered. |\n| `X-Spectrum-Timestamp` | Legacy header. UNIX epoch seconds at signing time. |\n| `X-Spectrum-Signature` | Legacy header. `v0=<hex>`, verified with `signingSecret`. |\n\n### Verifying the signature\nUse a Standard Webhooks consumer library with `standardSigningSecret`. The signed content is `webhook-id.webhook-timestamp.rawBody`; always verify the exact raw request bytes before parsing JSON. During secret rotation the signature header contains signatures for both the new and overlapping old secret.\n\n```\nverify(rawBody, headers, standardSigningSecret)\n```\n\nReject timestamps outside your replay-protection window and deduplicate by `webhook-id`.\n\n### Errors\nReturns `409` if an active webhook with the same URL is already registered for this project. Registering a URL that was previously deleted is allowed and yields a new webhook with a new signing secret. Returns `503` when an explicitly legacy webhook cannot be registered with spectrum-webhook; the database insert is rolled back so the request can be retried.\n\nRequires `Authorization: Basic base64(projectId:projectSecret)`.","tags":["webhooks"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"webhookUrl":{"type":"string","format":"uri"},"schemaVersion":{"default":"normalized-events.v1","type":"string","enum":["normalized-events.v1","raw-inbound.v1"]},"eventTypes":{"default":["message.received"],"minItems":1,"type":"array","items":{"type":"string","enum":["message.received"]}},"failureNotificationEmail":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]}},"required":["webhookUrl","schemaVersion","eventTypes"],"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"webhookUrl":{"type":"string","format":"uri"},"schemaVersion":{"default":"normalized-events.v1","type":"string","enum":["normalized-events.v1","raw-inbound.v1"]},"eventTypes":{"default":["message.received"],"minItems":1,"type":"array","items":{"type":"string","enum":["message.received"]}},"failureNotificationEmail":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]}},"required":["webhookUrl","schemaVersion","eventTypes"],"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"webhookUrl":{"type":"string","format":"uri"},"schemaVersion":{"default":"normalized-events.v1","type":"string","enum":["normalized-events.v1","raw-inbound.v1"]},"eventTypes":{"default":["message.received"],"minItems":1,"type":"array","items":{"type":"string","enum":["message.received"]}},"failureNotificationEmail":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]}},"required":["webhookUrl","schemaVersion","eventTypes"],"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"webhookUrl":{"type":"string","format":"uri"},"schemaVersion":{"type":"string","enum":["normalized-events.v1","raw-inbound.v1"]},"eventTypes":{"type":"array","items":{"type":"string","enum":["message.received"]}},"enabled":{"type":"boolean"},"status":{"type":"string","enum":["active","disabled"]},"failureNotificationEmail":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"disabledAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},{"type":"null"}]},"disabledReason":{"anyOf":[{"type":"string","enum":["manual","receiver_gone","delivery_failures"]},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"signingSecret":{"type":"string"},"standardSigningSecret":{"type":"string","pattern":"^whsec_.*"}},"required":["id","webhookUrl","schemaVersion","eventTypes","enabled","status","failureNotificationEmail","disabledAt","disabledReason","createdAt","updatedAt","signingSecret","standardSigningSecret"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdWebhooks"}},"/projects/{projectId}/webhooks/egress-ips":{"get":{"summary":"List webhook egress IPs","description":"Returns the stable public IPv4 addresses used by Fusor webhook delivery. Allowlist every returned address; the set can change with advance notice.","tags":["webhooks"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"addresses":{"type":"array","items":{"type":"string","format":"ipv4","pattern":"^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$"}}},"required":["addresses"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdWebhooksEgress-ips"}},"/projects/{projectId}/webhooks/{webhookId}":{"patch":{"summary":"Update webhook","description":"Updates event subscriptions, failure-notification email, enabled state, or permanently upgrades an endpoint from `normalized-events.v1` to `raw-inbound.v1`. `message.received` is currently the supported event type.\n\nSend a schema-version change separately from settings updates. The request body only accepts `schemaVersion: \"raw-inbound.v1\"`; downgrading to `normalized-events.v1` is not supported.\n\nBefore upgrading, deploy the `standardSigningSecret` returned at registration. The update response does not reveal that one-time secret again.\n\nThe handoff enables the Fusor destination before disabling the legacy source. Delivery can therefore briefly overlap while the legacy source converges, but avoids a delivery gap.\n\nReturns `404` if the webhook does not belong to the authenticated project and `409` if a concurrent migration supersedes this request.\nReturns `409` if `schemaVersion` is combined with another setting, or if `eventTypes` or `enabled` is sent for an endpoint still pinned to `normalized-events.v1`.\nRequests to downgrade an endpoint to `normalized-events.v1` fail schema validation with `422`.\n\nRequires `Authorization: Basic base64(projectId:projectSecret)`.","tags":["webhooks"],"parameters":[{"name":"webhookId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"schemaVersion":{"type":"string","const":"raw-inbound.v1"},"eventTypes":{"minItems":1,"type":"array","items":{"type":"string","enum":["message.received"]}},"enabled":{"type":"boolean"},"failureNotificationEmail":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]}},"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"schemaVersion":{"type":"string","const":"raw-inbound.v1"},"eventTypes":{"minItems":1,"type":"array","items":{"type":"string","enum":["message.received"]}},"enabled":{"type":"boolean"},"failureNotificationEmail":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]}},"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"schemaVersion":{"type":"string","const":"raw-inbound.v1"},"eventTypes":{"minItems":1,"type":"array","items":{"type":"string","enum":["message.received"]}},"enabled":{"type":"boolean"},"failureNotificationEmail":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]}},"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"webhookUrl":{"type":"string","format":"uri"},"schemaVersion":{"type":"string","enum":["normalized-events.v1","raw-inbound.v1"]},"eventTypes":{"type":"array","items":{"type":"string","enum":["message.received"]}},"enabled":{"type":"boolean"},"status":{"type":"string","enum":["active","disabled"]},"failureNotificationEmail":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"disabledAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},{"type":"null"}]},"disabledReason":{"anyOf":[{"type":"string","enum":["manual","receiver_gone","delivery_failures"]},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}},"required":["id","webhookUrl","schemaVersion","eventTypes","enabled","status","failureNotificationEmail","disabledAt","disabledReason","createdAt","updatedAt"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"patchProjectsByProjectIdWebhooksByWebhookId"},"delete":{"summary":"Delete webhook","description":"Stops delivery to this webhook. Once this call returns `200`, no further events will be sent to its URL. A delivery already in flight at the moment of deletion may still complete.\n\nThe `signingSecret` issued for this webhook is invalidated by deletion — register the URL again to obtain a new one.\n\nReturns `404` if the webhook id does not exist or has already been deleted.\nReturns `503` if spectrum-webhook does not acknowledge deregistration of a legacy webhook; the webhook remains active so deletion can be retried safely. Fusor-owned webhook deletion does not depend on spectrum-webhook.\n\nRequires `Authorization: Basic base64(projectId:projectSecret)`.","tags":["webhooks"],"parameters":[{"name":"webhookId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},"required":["id"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"deleteProjectsByProjectIdWebhooksByWebhookId"}},"/projects/{projectId}/webhooks/{webhookId}/secret/rotate":{"post":{"summary":"Rotate Standard Webhooks secret","description":"Creates a new `whsec_` secret. For the overlap period, deliveries contain signatures for both the new and previous secret so consumers can switch without downtime. The legacy Spectrum secret is unchanged.","tags":["webhooks"],"parameters":[{"name":"webhookId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"overlapSeconds":{"default":86400,"type":"integer","minimum":0,"maximum":604800}},"required":["overlapSeconds"],"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"overlapSeconds":{"default":86400,"type":"integer","minimum":0,"maximum":604800}},"required":["overlapSeconds"],"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"overlapSeconds":{"default":86400,"type":"integer","minimum":0,"maximum":604800}},"required":["overlapSeconds"],"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"standardSigningSecret":{"type":"string","pattern":"^whsec_.*"},"previousValidUntil":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},{"type":"null"}]}},"required":["id","standardSigningSecret","previousValidUntil"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdWebhooksByWebhookIdSecretRotate"}},"/projects/{projectId}/whatsapp-business/tokens":{"post":{"summary":"Issue WhatsApp Business tokens","description":"Issues per-line WhatsApp Business LightAuth tokens for a project. Use Authorization: Basic base64(projectId:projectSecret). Returns `auth` (Meta phone_number_id -> LightAuth token) and `numbers` (Meta phone_number_id -> display phone number, nullable). All returned tokens share the same TTL (seconds) in `expiresIn`.","tags":["whatsapp-business"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"auth":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"numbers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]}},"expiresIn":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["auth","numbers","expiresIn"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdWhatsapp-businessTokens"}},"/projects/{projectId}/whatsapp-business/accounts":{"get":{"summary":"List WhatsApp Business accounts","description":"Lists the project's onboarded WhatsApp Business accounts (WABAs), newest first. Each entry returns the internal `accountId` (used to scope template endpoints under `/accounts/:accountId/templates`), Meta's `wabaId`, the `businessName` Meta returned at onboarding (nullable), and `createdAt`. Returns an empty array if the project has none. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["whatsapp-business"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"accounts":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"wabaId":{"type":"string"},"businessName":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"}},"required":["accountId","wabaId","businessName","createdAt"],"additionalProperties":false}}},"required":["accounts"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdWhatsapp-businessAccounts"}},"/projects/{projectId}/whatsapp-business/accounts/{accountId}/templates/":{"get":{"summary":"List WhatsApp Business message templates","description":"Lists message templates for the given WhatsApp Business account. Forwards filters to Meta Graph (https://developers.facebook.com/docs/whatsapp/business-management-api/message-templates) and forwards opaque cursors via `paging.nextCursor` / `paging.prevCursor`. Component JSON is preserved in Meta's snake_case shape — see Meta's components reference. Requires Authorization: Basic base64(projectId:projectSecret).","tags":["whatsapp-business"],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"name","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":512}},{"name":"nameOrContent","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":512}},{"name":"status","in":"query","required":false,"schema":{"type":"string"}},{"name":"language","in":"query","required":false,"schema":{"type":"string"}},{"name":"category","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":true,"schema":{"default":25,"type":"integer","minimum":1,"maximum":100}},{"name":"after","in":"query","required":false,"schema":{"type":"string"}},{"name":"before","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"templates":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","enum":["APPROVED","PENDING","REJECTED","PAUSED","IN_APPEAL","PENDING_DELETION","DELETED","DISABLED","LIMIT_EXCEEDED"]},"category":{"type":"string","enum":["MARKETING","UTILITY","AUTHENTICATION"]},"language":{"type":"string"},"components":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"}},"required":["type"],"additionalProperties":{}}},"parameterFormat":{"type":"string","enum":["POSITIONAL","NAMED"]},"qualityScore":{"type":"object","properties":{"score":{"type":"string"}},"additionalProperties":{}},"rejectedReason":{"type":"string"}},"required":["id","name","status","category","language","components"],"additionalProperties":false}},"paging":{"type":"object","properties":{"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}]},"prevCursor":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["nextCursor","prevCursor"],"additionalProperties":false}},"required":["templates","paging"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdWhatsapp-businessAccountsByAccountIdTemplates"},"post":{"summary":"Create a WhatsApp Business message template","description":"Creates a message template under the given WhatsApp Business account. Body fields (`name`, `language`, `category`, `components`, `parameterFormat`, `allowCategoryChange`) map to Meta's `POST /{waba_id}/message_templates` payload; component JSON should follow Meta's components reference and is forwarded unchanged. Returns Meta's `{ id, status, category }` — newly-created templates start in `PENDING` and require Meta approval before they can be sent.","tags":["whatsapp-business"],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":512},"language":{"type":"string","minLength":2,"maxLength":20},"category":{"type":"string","enum":["MARKETING","UTILITY","AUTHENTICATION"]},"components":{"minItems":1,"type":"array","items":{"type":"object","properties":{"type":{"type":"string"}},"required":["type"],"additionalProperties":{}}},"parameterFormat":{"type":"string","enum":["POSITIONAL","NAMED"]},"allowCategoryChange":{"type":"boolean"}},"required":["name","language","category","components"],"additionalProperties":{}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":512},"language":{"type":"string","minLength":2,"maxLength":20},"category":{"type":"string","enum":["MARKETING","UTILITY","AUTHENTICATION"]},"components":{"minItems":1,"type":"array","items":{"type":"object","properties":{"type":{"type":"string"}},"required":["type"],"additionalProperties":{}}},"parameterFormat":{"type":"string","enum":["POSITIONAL","NAMED"]},"allowCategoryChange":{"type":"boolean"}},"required":["name","language","category","components"],"additionalProperties":{}}},"multipart/form-data":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":512},"language":{"type":"string","minLength":2,"maxLength":20},"category":{"type":"string","enum":["MARKETING","UTILITY","AUTHENTICATION"]},"components":{"minItems":1,"type":"array","items":{"type":"object","properties":{"type":{"type":"string"}},"required":["type"],"additionalProperties":{}}},"parameterFormat":{"type":"string","enum":["POSITIONAL","NAMED"]},"allowCategoryChange":{"type":"boolean"}},"required":["name","language","category","components"],"additionalProperties":{}}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["APPROVED","PENDING","REJECTED","PAUSED","IN_APPEAL","PENDING_DELETION","DELETED","DISABLED","LIMIT_EXCEEDED"]},"category":{"type":"string","enum":["MARKETING","UTILITY","AUTHENTICATION"]}},"required":["id","status","category"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdWhatsapp-businessAccountsByAccountIdTemplates"}},"/projects/{projectId}/whatsapp-business/accounts/{accountId}/templates/{templateId}":{"patch":{"summary":"Edit a WhatsApp Business message template","description":"Edits an existing message template. Only `components`, `category`, and `messageSendTtlSeconds` are editable — Meta forbids changing `name` and `language`. Empty body returns 400. Editing is subject to Meta's template lifecycle: APPROVED templates can edit `components`/`category` (category changes require re-approval), REJECTED templates can edit any field, PAUSED templates have limited edits.","tags":["whatsapp-business"],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"templateId","in":"path","required":true,"schema":{"type":"string","minLength":1}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"components":{"minItems":1,"type":"array","items":{"type":"object","properties":{"type":{"type":"string"}},"required":["type"],"additionalProperties":{}}},"category":{"type":"string","enum":["MARKETING","UTILITY","AUTHENTICATION"]},"messageSendTtlSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":{}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"components":{"minItems":1,"type":"array","items":{"type":"object","properties":{"type":{"type":"string"}},"required":["type"],"additionalProperties":{}}},"category":{"type":"string","enum":["MARKETING","UTILITY","AUTHENTICATION"]},"messageSendTtlSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":{}}},"multipart/form-data":{"schema":{"type":"object","properties":{"components":{"minItems":1,"type":"array","items":{"type":"object","properties":{"type":{"type":"string"}},"required":["type"],"additionalProperties":{}}},"category":{"type":"string","enum":["MARKETING","UTILITY","AUTHENTICATION"]},"messageSendTtlSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":{}}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"templateId":{"type":"string"},"success":{"type":"boolean","const":true}},"required":["templateId","success"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"patchProjectsByProjectIdWhatsapp-businessAccountsByAccountIdTemplatesByTemplateId"},"delete":{"summary":"Delete a WhatsApp Business message template","description":"Deletes a single language version of a message template. The path `:templateId` is forwarded to Meta as `hsm_id` so the delete is scoped to that exact template row — other language versions sharing the same `name` are untouched. The `name` query parameter is required because Meta's `DELETE /{waba_id}/message_templates` endpoint requires it alongside `hsm_id`. Returns `{ templateId, success: true }` on confirmation; Meta acknowledging `success=false` surfaces as 502.","tags":["whatsapp-business"],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"templateId","in":"path","required":true,"schema":{"type":"string","minLength":1}},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"name","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":512}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"templateId":{"type":"string"},"success":{"type":"boolean","const":true}},"required":["templateId","success"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"deleteProjectsByProjectIdWhatsapp-businessAccountsByAccountIdTemplatesByTemplateId"}},"/projects/{projectId}/slack/tokens":{"post":{"summary":"Issue Slack tokens","description":"Issues per-installation Slack LightAuth tokens for a project. Use Authorization: Basic base64(projectId:projectSecret). Returns `auth` (Slack team_id -> LightAuth token) and `teams` (Slack team_id -> { teamName, botUserId, appId, grantedScopes }). All returned tokens share the same TTL (seconds) in `expiresIn`. The map is the unified-API contract for 'every workspace this deployment can act on' — the SDK uses it to fan outbound calls and route inbound events without dev code touching team_id directly. Returns `403` if the project does not have the Slack platform enabled. Returns empty `auth`/`teams` maps when there are no active installations.","tags":["slack"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"auth":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"teams":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"teamName":{"type":"string"},"botUserId":{"type":"string"},"appId":{"type":"string"},"grantedScopes":{"type":"array","items":{"type":"string"}}},"required":["teamName","botUserId","appId","grantedScopes"],"additionalProperties":false}},"expiresIn":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["auth","teams","expiresIn"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdSlackTokens"}},"/projects/{projectId}/slack/":{"get":{"summary":"Get Slack app config","description":"Returns the project's active Slack app configuration plus the count of active installations. Returns 404 if no active config exists. Requires Authorization: Basic base64(projectId:projectSecret). RETURNS PLAINTEXT CREDENTIALS — only call from trusted environments.","tags":["slack"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"appConfigId":{"type":"string"},"projectId":{"type":"string"},"enabledFeatures":{"type":"array","items":{"type":"string"}},"clientId":{"anyOf":[{"type":"string"},{"type":"null"}]},"clientSecret":{"anyOf":[{"type":"string"},{"type":"null"}]},"signingSecret":{"anyOf":[{"type":"string"},{"type":"null"}]},"appId":{"anyOf":[{"type":"string"},{"type":"null"}]},"installationCount":{"type":"integer","minimum":0,"maximum":9007199254740991},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["appConfigId","projectId","enabledFeatures","clientId","clientSecret","signingSecret","appId","installationCount","createdAt","updatedAt"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdSlack"},"put":{"summary":"Upsert Slack app config","description":"Creates or updates the project's Slack app configuration. Partial — omitted fields are preserved on update. Validates `enabledFeatures` against the FEATURE_CATALOG. The OAuth proxy calls this after `apps.manifest.create` returns credentials, and may call it again to rotate credentials. NEVER pass a Slack config token (xoxe.xoxp-…) here — that's a workspace-admin credential and must not be persisted.","tags":["slack"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enabledFeatures":{"type":"array","items":{"type":"string"}},"clientId":{"type":"string","minLength":1},"clientSecret":{"type":"string","minLength":1},"signingSecret":{"type":"string","minLength":1},"appId":{"type":"string","minLength":1}},"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"enabledFeatures":{"type":"array","items":{"type":"string"}},"clientId":{"type":"string","minLength":1},"clientSecret":{"type":"string","minLength":1},"signingSecret":{"type":"string","minLength":1},"appId":{"type":"string","minLength":1}},"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"enabledFeatures":{"type":"array","items":{"type":"string"}},"clientId":{"type":"string","minLength":1},"clientSecret":{"type":"string","minLength":1},"signingSecret":{"type":"string","minLength":1},"appId":{"type":"string","minLength":1}},"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"appConfigId":{"type":"string"},"projectId":{"type":"string"},"enabledFeatures":{"type":"array","items":{"type":"string"}},"clientId":{"anyOf":[{"type":"string"},{"type":"null"}]},"clientSecret":{"anyOf":[{"type":"string"},{"type":"null"}]},"signingSecret":{"anyOf":[{"type":"string"},{"type":"null"}]},"appId":{"anyOf":[{"type":"string"},{"type":"null"}]},"installationCount":{"type":"integer","minimum":0,"maximum":9007199254740991},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["appConfigId","projectId","enabledFeatures","clientId","clientSecret","signingSecret","appId","installationCount","createdAt","updatedAt"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"putProjectsByProjectIdSlack"},"delete":{"summary":"Delete Slack app config","description":"Soft-deletes the project's active Slack app configuration. Existing installations are NOT deleted by this endpoint — they keep referencing the soft-deleted config until the project is deleted or the installations are explicitly removed.","tags":["slack"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"projectId":{"type":"string"}},"required":["projectId"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"deleteProjectsByProjectIdSlack"}},"/projects/{projectId}/slack/installations":{"get":{"summary":"List Slack installations for project","description":"Returns every active installation owned by the project's active Slack app config. Requires Authorization: Basic base64(projectId:projectSecret). RETURNS PLAINTEXT BOT TOKENS — only call from trusted environments.","tags":["slack"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"installations":{"type":"array","items":{"type":"object","properties":{"installationId":{"type":"string"},"appConfigId":{"type":"string"},"projectId":{"type":"string"},"teamId":{"type":"string"},"teamName":{"type":"string"},"appId":{"type":"string"},"botToken":{"type":"string"},"botRefreshToken":{"anyOf":[{"type":"string"},{"type":"null"}]},"botTokenExpiresAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"botUserId":{"type":"string"},"grantedScopes":{"type":"array","items":{"type":"string"}},"installedAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["installationId","appConfigId","projectId","teamId","teamName","appId","botToken","botRefreshToken","botTokenExpiresAt","botUserId","grantedScopes","installedAt","updatedAt"],"additionalProperties":false}}},"required":["installations"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"getProjectsByProjectIdSlackInstallations"}},"/projects/{projectId}/slack/installations/{teamId}":{"put":{"summary":"Upsert Slack installation","description":"Creates or updates an installation row for a workspace. Called by the OAuth proxy after `oauth.v2.access` succeeds. Returns 409 if the project has no active Slack app config (caller must `PUT /projects/:projectId/slack` first).","tags":["slack"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"teamId","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"teamName":{"type":"string","minLength":1},"appId":{"type":"string","minLength":1},"botToken":{"type":"string","minLength":1},"botRefreshToken":{"type":"string","minLength":1},"botTokenExpiresInSec":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"botUserId":{"type":"string","minLength":1},"grantedScopes":{"type":"array","items":{"type":"string"}}},"required":["teamName","appId","botToken","botUserId","grantedScopes"],"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"teamName":{"type":"string","minLength":1},"appId":{"type":"string","minLength":1},"botToken":{"type":"string","minLength":1},"botRefreshToken":{"type":"string","minLength":1},"botTokenExpiresInSec":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"botUserId":{"type":"string","minLength":1},"grantedScopes":{"type":"array","items":{"type":"string"}}},"required":["teamName","appId","botToken","botUserId","grantedScopes"],"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"teamName":{"type":"string","minLength":1},"appId":{"type":"string","minLength":1},"botToken":{"type":"string","minLength":1},"botRefreshToken":{"type":"string","minLength":1},"botTokenExpiresInSec":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"botUserId":{"type":"string","minLength":1},"grantedScopes":{"type":"array","items":{"type":"string"}}},"required":["teamName","appId","botToken","botUserId","grantedScopes"],"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"installationId":{"type":"string"},"appConfigId":{"type":"string"},"projectId":{"type":"string"},"teamId":{"type":"string"},"teamName":{"type":"string"},"appId":{"type":"string"},"botToken":{"type":"string"},"botRefreshToken":{"anyOf":[{"type":"string"},{"type":"null"}]},"botTokenExpiresAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"botUserId":{"type":"string"},"grantedScopes":{"type":"array","items":{"type":"string"}},"installedAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["installationId","appConfigId","projectId","teamId","teamName","appId","botToken","botRefreshToken","botTokenExpiresAt","botUserId","grantedScopes","installedAt","updatedAt"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"putProjectsByProjectIdSlackInstallationsByTeamId"},"delete":{"summary":"Delete Slack installation","description":"Soft-deletes a workspace installation. Frees the (slack_app, team) slot so the same workspace can re-install via OAuth.","tags":["slack"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"teamId","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"projectId":{"type":"string"},"teamId":{"type":"string"}},"required":["projectId","teamId"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"deleteProjectsByProjectIdSlackInstallationsByTeamId"}},"/projects/{projectId}/slack/setup":{"post":{"summary":"Set up a project's Slack app via workspace-admin tokens","description":"Creates (or looks up) the project's Slack app by forwarding to spectrum-slack's `POST /install/manifest`. Body carries a Slack workspace-admin `configToken` (`xoxe.xoxp-...`) — required on first install — and an optional paired `refreshToken` (`xoxe-...`). When supplied, the refresh token is persisted (plaintext today, TODO(kms)) on spectrum-cloud's `slack_apps.refresh_token` so the manifest can be auto-updated later via `tooling.tokens.rotate` + `apps.manifest.update`.\n\nUse this when the dashboard wants a single API surface for everything Slack-related. The functionally equivalent path is to hit spectrum-slack's `/install/manifest` directly with the same body plus `projectId`.\n\nReturns the new `appId` (or `null` on the re-install short-circuit before any workspace has completed OAuth) and the `startUrl` to continue the install flow. Upstream errors are surfaced as 4xx/5xx with the Slack error code in `message`.","tags":["slack"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"appName":{"type":"string","minLength":1,"maxLength":80},"enabledFeatures":{"type":"array","items":{"type":"string"}},"configToken":{"type":"string","minLength":8},"refreshToken":{"type":"string","minLength":8}},"required":["appName","enabledFeatures"],"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"appName":{"type":"string","minLength":1,"maxLength":80},"enabledFeatures":{"type":"array","items":{"type":"string"}},"configToken":{"type":"string","minLength":8},"refreshToken":{"type":"string","minLength":8}},"required":["appName","enabledFeatures"],"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"appName":{"type":"string","minLength":1,"maxLength":80},"enabledFeatures":{"type":"array","items":{"type":"string"}},"configToken":{"type":"string","minLength":8},"refreshToken":{"type":"string","minLength":8}},"required":["appName","enabledFeatures"],"additionalProperties":false}}}},"responses":{"200":{"description":"Response for status 200","content":{"application/json":{"schema":{"type":"object","properties":{"succeed":{"type":"boolean","const":true},"data":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"appId":{"anyOf":[{"type":"string"},{"type":"null"}]},"startUrl":{"type":"string"}},"required":["ok","appId","startUrl"],"additionalProperties":false}},"required":["succeed","data"],"additionalProperties":false}}}}},"operationId":"postProjectsByProjectIdSlackSetup"}}}}