From 4a716bb82d19dc63a08e59b1316b329f48daec48 Mon Sep 17 00:00:00 2001 From: Hein Date: Sat, 21 Feb 2026 00:04:16 +0200 Subject: [PATCH] feat(api): add phone number registration endpoint and update related logic --- pkg/api/server.go | 1 + pkg/config/config.go | 5 +- pkg/handlers/phone_numbers.go | 61 +++++++++++++++++++-- pkg/serverembed/dist/api.json | 46 +++++++++++++--- pkg/whatsapp/businessapi/client.go | 83 ++++++++++++++++++++++++----- pkg/whatsapp/businessapi/sending.go | 35 +++++++++--- pkg/whatsapp/businessapi/types.go | 9 ++++ pkg/whatsapp/manager.go | 31 ++++++++++- pkg/whatshooked/server.go | 1 + web/public/api.json | 46 +++++++++++++--- 10 files changed, 275 insertions(+), 43 deletions(-) diff --git a/pkg/api/server.go b/pkg/api/server.go index bf5cb7d..ddfd768 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -224,6 +224,7 @@ func SetupWhatsAppRoutes(router *mux.Router, wh WhatsHookedInterface, distFS fs. router.HandleFunc("/api/phone-numbers", h.Auth(h.ListPhoneNumbers)) router.HandleFunc("/api/phone-numbers/request-code", h.Auth(h.RequestVerificationCode)).Methods("POST") router.HandleFunc("/api/phone-numbers/verify-code", h.Auth(h.VerifyCode)).Methods("POST") + router.HandleFunc("/api/phone-numbers/register", h.Auth(h.RegisterPhoneNumber)).Methods("POST") // Media management (with auth) router.HandleFunc("/api/media/upload", h.Auth(h.UploadMedia)).Methods("POST") diff --git a/pkg/config/config.go b/pkg/config/config.go index 3b6f48a..29e3afe 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -62,8 +62,9 @@ type WhatsAppConfig struct { type BusinessAPIConfig struct { PhoneNumberID string `json:"phone_number_id"` AccessToken string `json:"access_token"` - BusinessAccountID string `json:"business_account_id,omitempty"` - APIVersion string `json:"api_version,omitempty"` // Default: v21.0 + WABAId string `json:"waba_id,omitempty"` // WhatsApp Business Account ID (resolved at connect time) + BusinessAccountID string `json:"business_account_id,omitempty"` // Facebook Business Manager ID (optional) + APIVersion string `json:"api_version,omitempty"` // Default: v21.0 WebhookPath string `json:"webhook_path,omitempty"` VerifyToken string `json:"verify_token,omitempty"` } diff --git a/pkg/handlers/phone_numbers.go b/pkg/handlers/phone_numbers.go index e6b170f..365c927 100644 --- a/pkg/handlers/phone_numbers.go +++ b/pkg/handlers/phone_numbers.go @@ -3,8 +3,12 @@ package handlers import ( "encoding/json" "net/http" + "regexp" ) +var validLanguageCode = regexp.MustCompile(`^[a-z]{2,3}(_[A-Z]{2})?$`) +var validOTPCode = regexp.MustCompile(`^\d{4,8}$`) + // ListPhoneNumbers returns all phone numbers for the account. // POST /api/phone-numbers {"account_id"} func (h *Handlers) ListPhoneNumbers(w http.ResponseWriter, r *http.Request) { @@ -47,7 +51,8 @@ func (h *Handlers) RequestVerificationCode(w http.ResponseWriter, r *http.Reques var req struct { AccountID string `json:"account_id"` PhoneNumberID string `json:"phone_number_id"` - Method string `json:"method"` // "SMS" or "VOICE" + Method string `json:"code_method"` // "SMS" or "VOICE" + Language string `json:"language"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -55,13 +60,16 @@ func (h *Handlers) RequestVerificationCode(w http.ResponseWriter, r *http.Reques } if req.PhoneNumberID == "" || req.Method == "" { - http.Error(w, "phone_number_id and method are required", http.StatusBadRequest) + http.Error(w, "phone_number_id and code_method are required", http.StatusBadRequest) return } if req.Method != "SMS" && req.Method != "VOICE" { - http.Error(w, "method must be SMS or VOICE", http.StatusBadRequest) + http.Error(w, "code_method must be SMS or VOICE", http.StatusBadRequest) return } + if req.Language == "" || !validLanguageCode.MatchString(req.Language) { + req.Language = "en_US" + } baClient, err := h.getBusinessAPIClient(req.AccountID) if err != nil { @@ -69,7 +77,7 @@ func (h *Handlers) RequestVerificationCode(w http.ResponseWriter, r *http.Reques return } - if err := baClient.RequestVerificationCode(r.Context(), req.PhoneNumberID, req.Method); err != nil { + if err := baClient.RequestVerificationCode(r.Context(), req.PhoneNumberID, req.Method, req.Language); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -99,6 +107,10 @@ func (h *Handlers) VerifyCode(w http.ResponseWriter, r *http.Request) { http.Error(w, "phone_number_id and code are required", http.StatusBadRequest) return } + if !validOTPCode.MatchString(req.Code) { + http.Error(w, "code must be 4-8 digits", http.StatusBadRequest) + return + } baClient, err := h.getBusinessAPIClient(req.AccountID) if err != nil { @@ -113,3 +125,44 @@ func (h *Handlers) VerifyCode(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]string{"status": "ok"}) } + +// RegisterPhoneNumber registers a phone number with the WhatsApp Cloud API. +// POST /api/phone-numbers/register {"account_id","phone_number_id","pin"} +func (h *Handlers) RegisterPhoneNumber(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req struct { + AccountID string `json:"account_id"` + PhoneNumberID string `json:"phone_number_id"` + Pin string `json:"pin"` // 6-digit two-step verification PIN + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if req.PhoneNumberID == "" { + http.Error(w, "phone_number_id is required", http.StatusBadRequest) + return + } + if !validOTPCode.MatchString(req.Pin) { + http.Error(w, "pin must be 4-8 digits", http.StatusBadRequest) + return + } + + baClient, err := h.getBusinessAPIClient(req.AccountID) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := baClient.RegisterPhoneNumber(r.Context(), req.PhoneNumberID, req.Pin); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + writeJSON(w, map[string]string{"status": "ok"}) +} diff --git a/pkg/serverembed/dist/api.json b/pkg/serverembed/dist/api.json index 1e09d2d..47d4ca5 100644 --- a/pkg/serverembed/dist/api.json +++ b/pkg/serverembed/dist/api.json @@ -76,7 +76,8 @@ "properties": { "phone_number_id": { "type": "string" }, "access_token": { "type": "string" }, - "business_account_id": { "type": "string" }, + "waba_id": { "type": "string", "description": "WhatsApp Business Account ID — resolved automatically at connect time from business_account_id if not set" }, + "business_account_id": { "type": "string", "description": "Facebook Business Manager ID — used to resolve waba_id on first connect" }, "api_version": { "type": "string", "default": "v21.0" }, "webhook_path": { "type": "string" }, "verify_token": { "type": "string" } @@ -1246,15 +1247,15 @@ "account_id": { "type": "string" }, "phone_number_id": { "type": "string" }, "code_method": { "type": "string", "enum": ["SMS", "VOICE"] }, - "language": { "type": "string" } + "language": { "type": "string", "pattern": "^[a-z]{2,3}(_[A-Z]{2})?$", "default": "en_US", "example": "en_US", "description": "BCP-47 locale e.g. en_US, pt_BR — defaults to en_US if omitted or invalid" } }, - "required": ["account_id", "phone_number_id", "code_method", "language"] + "required": ["account_id", "phone_number_id", "code_method"] } } } }, "responses": { - "200": { "description": "Verification code requested", "content": { "application/json": { "schema": { "type": "object" } } } }, + "200": { "description": "Verification code requested", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StatusOk" } } } }, "400": { "description": "Bad request" }, "401": { "description": "Unauthorized" }, "500": { "description": "Meta API error" } @@ -1263,7 +1264,7 @@ }, "/api/phone-numbers/verify-code": { "post": { - "summary": "Verify a phone number with the received code (Business API only)", + "summary": "Verify a phone number with the received OTP code (Business API only)", "operationId": "verifyCode", "tags": ["Phone Numbers"], "requestBody": { @@ -1275,7 +1276,7 @@ "properties": { "account_id": { "type": "string" }, "phone_number_id": { "type": "string" }, - "code": { "type": "string" } + "code": { "type": "string", "pattern": "^\\d{4,8}$", "example": "123456", "description": "4–8 digit OTP received via SMS or VOICE" } }, "required": ["account_id", "phone_number_id", "code"] } @@ -1283,8 +1284,37 @@ } }, "responses": { - "200": { "description": "Code verified", "content": { "application/json": { "schema": { "type": "object" } } } }, - "400": { "description": "Bad request" }, + "200": { "description": "Code verified", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StatusOk" } } } }, + "400": { "description": "Bad request / invalid code format" }, + "401": { "description": "Unauthorized" }, + "500": { "description": "Meta API error" } + } + } + }, + "/api/phone-numbers/register": { + "post": { + "summary": "Register a phone number with the WhatsApp Cloud API (Business API only)", + "operationId": "registerPhoneNumber", + "tags": ["Phone Numbers"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "account_id": { "type": "string" }, + "phone_number_id": { "type": "string" }, + "pin": { "type": "string", "pattern": "^\\d{4,8}$", "example": "123456", "description": "Two-step verification PIN (4–8 digits)" } + }, + "required": ["account_id", "phone_number_id", "pin"] + } + } + } + }, + "responses": { + "200": { "description": "Phone number registered", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StatusOk" } } } }, + "400": { "description": "Bad request / invalid PIN format" }, "401": { "description": "Unauthorized" }, "500": { "description": "Meta API error" } } diff --git a/pkg/whatsapp/businessapi/client.go b/pkg/whatsapp/businessapi/client.go index 691be7b..d79a73b 100644 --- a/pkg/whatsapp/businessapi/client.go +++ b/pkg/whatsapp/businessapi/client.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "net/url" "strings" "sync" "time" @@ -20,18 +21,20 @@ import ( // Client represents a WhatsApp Business API client type Client struct { - id string - phoneNumber string - config config.BusinessAPIConfig - httpClient *http.Client - eventBus *events.EventBus - mediaConfig config.MediaConfig - connected bool - mu sync.RWMutex + id string + phoneNumber string + config config.BusinessAPIConfig + httpClient *http.Client + eventBus *events.EventBus + mediaConfig config.MediaConfig + connected bool + wabaID string // WhatsApp Business Account ID, resolved at connect time + onWABAResolved func(accountID, wabaID string) // called when WABA ID is resolved for the first time + mu sync.RWMutex } // NewClient creates a new Business API client -func NewClient(cfg config.WhatsAppConfig, eventBus *events.EventBus, mediaConfig config.MediaConfig) (*Client, error) { +func NewClient(cfg config.WhatsAppConfig, eventBus *events.EventBus, mediaConfig config.MediaConfig, onWABAResolved func(accountID, wabaID string)) (*Client, error) { if cfg.Type != "business-api" { return nil, fmt.Errorf("invalid client type for business-api: %s", cfg.Type) } @@ -60,9 +63,10 @@ func NewClient(cfg config.WhatsAppConfig, eventBus *events.EventBus, mediaConfig httpClient: &http.Client{ Timeout: 30 * time.Second, }, - eventBus: eventBus, - mediaConfig: mediaConfig, - connected: false, + eventBus: eventBus, + mediaConfig: mediaConfig, + connected: false, + onWABAResolved: onWABAResolved, }, nil } @@ -123,7 +127,24 @@ func (c *Client) Connect(ctx context.Context) error { "status", phoneDetails.CodeVerificationStatus) } - // Step 3: Get business account details (if business_account_id is provided) + // Step 3: Resolve the WhatsApp Business Account (WABA) ID from the phone number. + // The WABA ID is required for phone number listing; it differs from the Facebook Business Manager ID. + // Use cached value from config if already known. + if c.config.WABAId != "" { + c.wabaID = c.config.WABAId + logging.Info("WhatsApp Business Account ID loaded from config", "account_id", c.id, "waba_id", c.wabaID) + } else if wabaID, err := c.fetchWABAID(ctx); err != nil { + logging.Warn("Failed to resolve WABA ID (non-critical)", "account_id", c.id, "error", err) + } else { + c.wabaID = wabaID + c.config.WABAId = wabaID + logging.Info("WhatsApp Business Account ID resolved", "account_id", c.id, "waba_id", wabaID) + if c.onWABAResolved != nil { + c.onWABAResolved(c.id, wabaID) + } + } + + // Step 4: Get business account details (if business_account_id is provided) if c.config.BusinessAccountID != "" { businessDetails, err := c.getBusinessAccountDetails(ctx) if err != nil { @@ -532,6 +553,42 @@ func (c *Client) checkMissingScopes(currentScopes []string, requiredScopes []str return missing } +// fetchWABAID resolves the WhatsApp Business Account (WABA) ID by iterating the WABAs +// owned by the configured business account and matching against our phone number ID. +// Requires business_account_id to be set in config. +// GET /{business-id}/owned_whatsapp_business_accounts?fields=id,phone_numbers{id} +func (c *Client) fetchWABAID(ctx context.Context) (string, error) { + if c.config.BusinessAccountID == "" { + return "", fmt.Errorf("waba_id or business_account_id must be set in config to resolve WABA") + } + + var result struct { + Data []struct { + ID string `json:"id"` + PhoneNumbers struct { + Data []struct { + ID string `json:"id"` + } `json:"data"` + } `json:"phone_numbers"` + } `json:"data"` + } + + params := url.Values{"fields": {"id,phone_numbers{id}"}} + if err := c.graphAPIGet(ctx, c.config.BusinessAccountID+"/owned_whatsapp_business_accounts", params, &result); err != nil { + return "", fmt.Errorf("failed to list WABAs for business %s: %w", c.config.BusinessAccountID, err) + } + + for _, waba := range result.Data { + for _, phone := range waba.PhoneNumbers.Data { + if phone.ID == c.config.PhoneNumberID { + return waba.ID, nil + } + } + } + + return "", fmt.Errorf("phone number %s not found in any WABA owned by business %s", c.config.PhoneNumberID, c.config.BusinessAccountID) +} + // formatExpiry formats the expiry timestamp for logging func (c *Client) formatExpiry(expiresAt int64) string { if expiresAt == 0 { diff --git a/pkg/whatsapp/businessapi/sending.go b/pkg/whatsapp/businessapi/sending.go index 25f20d5..9950b36 100644 --- a/pkg/whatsapp/businessapi/sending.go +++ b/pkg/whatsapp/businessapi/sending.go @@ -226,27 +226,37 @@ func (c *Client) MarkAsRead(ctx context.Context, messageID string) error { return nil } -// ListPhoneNumbers returns all phone numbers for the business account. +// ListPhoneNumbers returns all phone numbers for the WhatsApp Business Account. +// The WABA ID is resolved from the phone number at connect time; it is distinct from +// the Facebook Business Manager ID stored in business_account_id. func (c *Client) ListPhoneNumbers(ctx context.Context) (*PhoneNumberListResponse, error) { - if c.config.BusinessAccountID == "" { - return nil, errNoBusinessAccount + wabaID := c.wabaID + if wabaID == "" { + // Fallback: resolve on demand if Connect() was not called + id, err := c.fetchWABAID(ctx) + if err != nil { + return nil, fmt.Errorf("could not resolve WABA ID: %w", err) + } + c.wabaID = id + wabaID = id } params := url.Values{ - "fields": {"id,display_phone_number,phone_number,verified_name,code_verification_status,quality_rating,throughput"}, + "fields": {"id,display_phone_number,verified_name,code_verification_status,quality_rating,throughput"}, } var resp PhoneNumberListResponse - if err := c.graphAPIGet(ctx, c.config.BusinessAccountID+"/phone_numbers", params, &resp); err != nil { + if err := c.graphAPIGet(ctx, wabaID+"/phone_numbers", params, &resp); err != nil { return nil, err } return &resp, nil } // RequestVerificationCode sends a verification code (SMS or VOICE) to the given phone number. -func (c *Client) RequestVerificationCode(ctx context.Context, phoneNumberID string, method string) error { +func (c *Client) RequestVerificationCode(ctx context.Context, phoneNumberID string, method string, language string) error { body := map[string]string{ - "verification_method": method, + "code_method": method, + "language": language, } var resp RequestCodeResponse @@ -261,6 +271,17 @@ func (c *Client) VerifyCode(ctx context.Context, phoneNumberID string, code stri return c.graphAPIPost(ctx, phoneNumberID+"/verify_code", body, &resp) } +// RegisterPhoneNumber registers a phone number for use with the WhatsApp Cloud API. +// POST /{phone-number-id}/register +func (c *Client) RegisterPhoneNumber(ctx context.Context, phoneNumberID string, pin string) error { + body := RegisterPhoneNumberRequest{ + MessagingProduct: "whatsapp", + Pin: pin, + } + var resp RegisterPhoneNumberResponse + return c.graphAPIPost(ctx, phoneNumberID+"/register", body, &resp) +} + // DeleteMedia deletes a previously uploaded media file. func (c *Client) DeleteMedia(ctx context.Context, mediaID string) error { return c.graphAPIDelete(ctx, mediaID, nil) diff --git a/pkg/whatsapp/businessapi/types.go b/pkg/whatsapp/businessapi/types.go index aedb0a6..9300df0 100644 --- a/pkg/whatsapp/businessapi/types.go +++ b/pkg/whatsapp/businessapi/types.go @@ -774,6 +774,15 @@ type VerifyCodeData struct { MessageStatus string `json:"message_status"` // "CODE_VERIFIED" } +type RegisterPhoneNumberRequest struct { + MessagingProduct string `json:"messaging_product"` // always "whatsapp" + Pin string `json:"pin"` // 6-digit two-step verification PIN +} + +type RegisterPhoneNumberResponse struct { + Success bool `json:"success"` +} + // --------------------------------------------------------------------------- // Business profile // --------------------------------------------------------------------------- diff --git a/pkg/whatsapp/manager.go b/pkg/whatsapp/manager.go index 55c3f53..0fb7a24 100644 --- a/pkg/whatsapp/manager.go +++ b/pkg/whatsapp/manager.go @@ -51,7 +51,10 @@ func (m *Manager) Connect(ctx context.Context, cfg config.WhatsAppConfig) error // Factory pattern based on type switch cfg.Type { case "business-api": - client, err = businessapi.NewClient(cfg, m.eventBus, m.mediaConfig) + onWABAResolved := func(accountID, wabaID string) { + m.updateWABAInConfig(accountID, wabaID) + } + client, err = businessapi.NewClient(cfg, m.eventBus, m.mediaConfig, onWABAResolved) case "whatsmeow", "": // Create callback for phone number updates onPhoneUpdate := func(accountID, phoneNumber string) { @@ -187,6 +190,32 @@ func (m *Manager) GetClient(id string) (Client, bool) { return client, exists } +// updateWABAInConfig stores the resolved WABA ID back into the in-memory config and syncs to DB/file +func (m *Manager) updateWABAInConfig(accountID, wabaID string) { + m.mu.Lock() + defer m.mu.Unlock() + + for i := range m.config.WhatsApp { + if m.config.WhatsApp[i].ID == accountID { + if m.config.WhatsApp[i].BusinessAPI == nil { + return + } + if m.config.WhatsApp[i].BusinessAPI.WABAId == wabaID { + return // already up to date + } + m.config.WhatsApp[i].BusinessAPI.WABAId = wabaID + logging.Info("Updated WABA ID in config", "account_id", accountID, "waba_id", wabaID) + + if m.onConfigUpdate != nil { + if err := m.onConfigUpdate(m.config); err != nil { + logging.Error("Failed to save config after WABA ID update", "account_id", accountID, "error", err) + } + } + break + } + } +} + // updatePhoneNumberInConfig updates the phone number for an account in config and saves it func (m *Manager) updatePhoneNumberInConfig(accountID, phoneNumber string) { m.mu.Lock() diff --git a/pkg/whatshooked/server.go b/pkg/whatshooked/server.go index 17d1da4..7da8196 100644 --- a/pkg/whatshooked/server.go +++ b/pkg/whatshooked/server.go @@ -274,6 +274,7 @@ func (s *Server) setupRoutes() *http.ServeMux { mux.HandleFunc("/api/phone-numbers", h.Auth(h.ListPhoneNumbers)) mux.HandleFunc("/api/phone-numbers/request-code", h.Auth(h.RequestVerificationCode)) mux.HandleFunc("/api/phone-numbers/verify-code", h.Auth(h.VerifyCode)) + mux.HandleFunc("/api/phone-numbers/register", h.Auth(h.RegisterPhoneNumber)) // Media management (with auth) mux.HandleFunc("/api/media/upload", h.Auth(h.UploadMedia)) diff --git a/web/public/api.json b/web/public/api.json index 1e09d2d..47d4ca5 100644 --- a/web/public/api.json +++ b/web/public/api.json @@ -76,7 +76,8 @@ "properties": { "phone_number_id": { "type": "string" }, "access_token": { "type": "string" }, - "business_account_id": { "type": "string" }, + "waba_id": { "type": "string", "description": "WhatsApp Business Account ID — resolved automatically at connect time from business_account_id if not set" }, + "business_account_id": { "type": "string", "description": "Facebook Business Manager ID — used to resolve waba_id on first connect" }, "api_version": { "type": "string", "default": "v21.0" }, "webhook_path": { "type": "string" }, "verify_token": { "type": "string" } @@ -1246,15 +1247,15 @@ "account_id": { "type": "string" }, "phone_number_id": { "type": "string" }, "code_method": { "type": "string", "enum": ["SMS", "VOICE"] }, - "language": { "type": "string" } + "language": { "type": "string", "pattern": "^[a-z]{2,3}(_[A-Z]{2})?$", "default": "en_US", "example": "en_US", "description": "BCP-47 locale e.g. en_US, pt_BR — defaults to en_US if omitted or invalid" } }, - "required": ["account_id", "phone_number_id", "code_method", "language"] + "required": ["account_id", "phone_number_id", "code_method"] } } } }, "responses": { - "200": { "description": "Verification code requested", "content": { "application/json": { "schema": { "type": "object" } } } }, + "200": { "description": "Verification code requested", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StatusOk" } } } }, "400": { "description": "Bad request" }, "401": { "description": "Unauthorized" }, "500": { "description": "Meta API error" } @@ -1263,7 +1264,7 @@ }, "/api/phone-numbers/verify-code": { "post": { - "summary": "Verify a phone number with the received code (Business API only)", + "summary": "Verify a phone number with the received OTP code (Business API only)", "operationId": "verifyCode", "tags": ["Phone Numbers"], "requestBody": { @@ -1275,7 +1276,7 @@ "properties": { "account_id": { "type": "string" }, "phone_number_id": { "type": "string" }, - "code": { "type": "string" } + "code": { "type": "string", "pattern": "^\\d{4,8}$", "example": "123456", "description": "4–8 digit OTP received via SMS or VOICE" } }, "required": ["account_id", "phone_number_id", "code"] } @@ -1283,8 +1284,37 @@ } }, "responses": { - "200": { "description": "Code verified", "content": { "application/json": { "schema": { "type": "object" } } } }, - "400": { "description": "Bad request" }, + "200": { "description": "Code verified", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StatusOk" } } } }, + "400": { "description": "Bad request / invalid code format" }, + "401": { "description": "Unauthorized" }, + "500": { "description": "Meta API error" } + } + } + }, + "/api/phone-numbers/register": { + "post": { + "summary": "Register a phone number with the WhatsApp Cloud API (Business API only)", + "operationId": "registerPhoneNumber", + "tags": ["Phone Numbers"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "account_id": { "type": "string" }, + "phone_number_id": { "type": "string" }, + "pin": { "type": "string", "pattern": "^\\d{4,8}$", "example": "123456", "description": "Two-step verification PIN (4–8 digits)" } + }, + "required": ["account_id", "phone_number_id", "pin"] + } + } + } + }, + "responses": { + "200": { "description": "Phone number registered", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StatusOk" } } } }, + "400": { "description": "Bad request / invalid PIN format" }, "401": { "description": "Unauthorized" }, "500": { "description": "Meta API error" } }