Whatsapp Business enhancements
This commit is contained in:
@@ -11,6 +11,7 @@ A Go library and service that connects to WhatsApp and forwards messages to regi
|
||||
|
||||
## Documentation
|
||||
|
||||
- [WhatsApp Business API Setup](WHATSAPP_BUSINESS.md) - Complete guide for configuring WhatsApp Business API credentials
|
||||
- [TODO List](TODO.md) - Current tasks and planned improvements
|
||||
- [AI Usage Guidelines](AI_USE.md) - Rules when using AI tools with this project
|
||||
- [Project Plan](PLAN.md) - Development plan and architecture decisions
|
||||
|
||||
281
WHATSAPP_BUSINESS.md
Normal file
281
WHATSAPP_BUSINESS.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# WhatsApp Business API Setup Guide
|
||||
|
||||
This guide will help you set up WhatsApp Business API credentials for use with WhatsHooked.
|
||||
|
||||
## Common Error: "Object does not exist or missing permissions"
|
||||
|
||||
If you see this error:
|
||||
```
|
||||
Failed to connect client account_id=test error="API returned status 400:
|
||||
{\"error\":{\"message\":\"Unsupported get request. Object with ID 'XXXXXXXXX' does not exist,
|
||||
cannot be loaded due to missing permissions, or does not support this operation...\",
|
||||
\"type\":\"GraphMethodException\",\"code\":100,\"error_subcode\":33...}}"
|
||||
```
|
||||
|
||||
This means your **access token lacks the required WhatsApp Business API permissions**.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have:
|
||||
|
||||
1. A Meta Business Account
|
||||
2. WhatsApp Business API access (approved by Meta)
|
||||
3. A verified WhatsApp Business phone number
|
||||
4. Admin access to your Meta Business Manager
|
||||
|
||||
## Step 1: Access Meta Business Manager
|
||||
|
||||
1. Go to [Meta Business Manager](https://business.facebook.com/)
|
||||
2. Select your business account
|
||||
3. Navigate to **Business Settings** (gear icon)
|
||||
|
||||
## Step 2: Create a System User (Recommended for Production)
|
||||
|
||||
System Users provide permanent access tokens that don't expire with user sessions.
|
||||
|
||||
1. In Business Settings, go to **Users** → **System Users**
|
||||
2. Click **Add** to create a new system user
|
||||
3. Enter a name (e.g., "WhatsHooked API Access")
|
||||
4. Select **Admin** role
|
||||
5. Click **Create System User**
|
||||
|
||||
## Step 3: Assign the System User to WhatsApp
|
||||
|
||||
1. In the System User details, scroll to **Assign Assets**
|
||||
2. Click **Add Assets**
|
||||
3. Select **Apps**
|
||||
4. Choose your WhatsApp Business app
|
||||
5. Grant **Full Control**
|
||||
6. Click **Add People**
|
||||
7. Select **WhatsApp Accounts**
|
||||
8. Choose your WhatsApp Business Account
|
||||
9. Grant **Full Control**
|
||||
10. Click **Save Changes**
|
||||
|
||||
## Step 4: Generate Access Token with Required Permissions
|
||||
|
||||
1. In the System User details, click **Generate New Token**
|
||||
2. Select your app from the dropdown
|
||||
3. **IMPORTANT**: Check these permissions:
|
||||
- ✅ `whatsapp_business_management`
|
||||
- ✅ `whatsapp_business_messaging`
|
||||
4. Set token expiration (choose "Never" for permanent tokens)
|
||||
5. Click **Generate Token**
|
||||
6. **CRITICAL**: Copy the token immediately - you won't see it again!
|
||||
|
||||
### Verify Token Permissions
|
||||
|
||||
You can verify your token has the correct permissions:
|
||||
|
||||
```bash
|
||||
# Replace YOUR_TOKEN with your actual access token
|
||||
curl -X GET 'https://graph.facebook.com/v21.0/debug_token?input_token=YOUR_TOKEN' \
|
||||
-H 'Authorization: Bearer YOUR_TOKEN'
|
||||
```
|
||||
|
||||
Look for `"scopes"` in the response - it should include:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"scopes": [
|
||||
"whatsapp_business_management",
|
||||
"whatsapp_business_messaging",
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: Get Your Phone Number ID
|
||||
|
||||
The Phone Number ID is **NOT** your actual phone number - it's a unique identifier from Meta.
|
||||
|
||||
### Method 1: Via WhatsApp Manager (Easiest)
|
||||
|
||||
1. Go to [WhatsApp Manager](https://business.facebook.com/wa/manage/home/)
|
||||
2. Select your WhatsApp Business Account
|
||||
3. Click **API Setup** in the left sidebar
|
||||
4. Copy the **Phone Number ID** (looks like: `123456789012345`)
|
||||
|
||||
### Method 2: Via API
|
||||
|
||||
```bash
|
||||
# Replace YOUR_TOKEN and YOUR_BUSINESS_ACCOUNT_ID
|
||||
curl -X GET 'https://graph.facebook.com/v21.0/YOUR_BUSINESS_ACCOUNT_ID/phone_numbers' \
|
||||
-H 'Authorization: Bearer YOUR_TOKEN'
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"verified_name": "Your Business Name",
|
||||
"display_phone_number": "+1 234-567-8900",
|
||||
"id": "123456789012345", // <- This is your Phone Number ID
|
||||
"quality_rating": "GREEN"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Step 6: Get Your Business Account ID (Optional)
|
||||
|
||||
```bash
|
||||
# Get all WhatsApp Business Accounts you have access to
|
||||
curl -X GET 'https://graph.facebook.com/v21.0/me/businesses' \
|
||||
-H 'Authorization: Bearer YOUR_TOKEN'
|
||||
```
|
||||
|
||||
Or find it in WhatsApp Manager:
|
||||
1. Go to WhatsApp Manager
|
||||
2. Click on **Settings** (gear icon)
|
||||
3. The Business Account ID is shown in the URL: `https://business.facebook.com/wa/manage/home/?waba_id=XXXXXXXXX`
|
||||
|
||||
## Step 7: Test Your Credentials
|
||||
|
||||
Before configuring WhatsHooked, test your credentials:
|
||||
|
||||
```bash
|
||||
# Replace PHONE_NUMBER_ID and YOUR_TOKEN
|
||||
curl -X GET 'https://graph.facebook.com/v21.0/PHONE_NUMBER_ID' \
|
||||
-H 'Authorization: Bearer YOUR_TOKEN'
|
||||
```
|
||||
|
||||
If successful, you'll get a response like:
|
||||
```json
|
||||
{
|
||||
"verified_name": "Your Business Name",
|
||||
"display_phone_number": "+1 234-567-8900",
|
||||
"id": "123456789012345",
|
||||
"quality_rating": "GREEN"
|
||||
}
|
||||
```
|
||||
|
||||
If you get an error like `"error_subcode":33`, your token lacks permissions - go back to Step 4.
|
||||
|
||||
## Step 8: Configure WhatsHooked
|
||||
|
||||
Update your `config.json` with the Business API configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"whatsapp": [
|
||||
{
|
||||
"id": "business",
|
||||
"type": "business-api",
|
||||
"phone_number": "+1234567890",
|
||||
"business_api": {
|
||||
"phone_number_id": "123456789012345",
|
||||
"access_token": "EAAxxxxxxxxxxxx_your_permanent_token_here",
|
||||
"business_account_id": "987654321098765",
|
||||
"api_version": "v21.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `id` | Yes | Unique identifier for this account in WhatsHooked |
|
||||
| `type` | Yes | Must be `"business-api"` |
|
||||
| `phone_number` | Yes | Your WhatsApp Business phone number (E.164 format) |
|
||||
| `phone_number_id` | Yes | Phone Number ID from Meta (from Step 5) |
|
||||
| `access_token` | Yes | Permanent access token (from Step 4) |
|
||||
| `business_account_id` | No | WhatsApp Business Account ID (optional, for reference) |
|
||||
| `api_version` | No | Graph API version (defaults to `"v21.0"`) |
|
||||
|
||||
## Step 9: Start WhatsHooked
|
||||
|
||||
```bash
|
||||
./bin/whatshook-server -config config.json
|
||||
```
|
||||
|
||||
You should see:
|
||||
```
|
||||
INFO Business API client connected account_id=business phone=+1234567890
|
||||
```
|
||||
|
||||
If you see `Failed to connect client`, check the error message and verify:
|
||||
1. Phone Number ID is correct
|
||||
2. Access token has required permissions
|
||||
3. Access token hasn't expired
|
||||
4. Business Account has WhatsApp API access enabled
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "Object with ID does not exist" (error_subcode: 33)
|
||||
|
||||
**Cause**: One of the following:
|
||||
- Incorrect Phone Number ID
|
||||
- Access token lacks permissions
|
||||
- Access token expired
|
||||
|
||||
**Fix**:
|
||||
1. Verify token permissions (see Step 4)
|
||||
2. Double-check Phone Number ID (see Step 5)
|
||||
3. Generate a new token if needed
|
||||
|
||||
### Error: "Invalid OAuth access token"
|
||||
|
||||
**Cause**: Token is invalid or expired
|
||||
|
||||
**Fix**: Generate a new access token (Step 4)
|
||||
|
||||
### Error: "Application does not have permission"
|
||||
|
||||
**Cause**: App not added to WhatsApp Business Account
|
||||
|
||||
**Fix**: Complete Step 3 to assign System User to WhatsApp
|
||||
|
||||
### Token Expires Too Quickly
|
||||
|
||||
**Issue**: Using a User Access Token instead of System User token
|
||||
|
||||
**Fix**:
|
||||
- Use a System User (Step 2) for permanent tokens
|
||||
- User Access Tokens expire in 60 days
|
||||
- System User tokens can be set to "Never expire"
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit tokens to version control**
|
||||
- Add `config.json` to `.gitignore`
|
||||
- Use environment variables for sensitive data
|
||||
|
||||
2. **Rotate tokens regularly**
|
||||
- Even "permanent" tokens should be rotated periodically
|
||||
- Revoke old tokens when generating new ones
|
||||
|
||||
3. **Use System Users for production**
|
||||
- Don't use personal User Access Tokens
|
||||
- System Users provide better security and permanence
|
||||
|
||||
4. **Limit token permissions**
|
||||
- Only grant the minimum required permissions
|
||||
- For WhatsHooked, you only need:
|
||||
- `whatsapp_business_management`
|
||||
- `whatsapp_business_messaging`
|
||||
|
||||
5. **Monitor token usage**
|
||||
- Check token status regularly via debug_token endpoint
|
||||
- Watch for unexpected API calls
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [WhatsApp Business Platform Documentation](https://developers.facebook.com/docs/whatsapp)
|
||||
- [Graph API Reference](https://developers.facebook.com/docs/graph-api)
|
||||
- [System Users Guide](https://www.facebook.com/business/help/503306463479099)
|
||||
- [WhatsApp Business API Getting Started](https://developers.facebook.com/docs/whatsapp/cloud-api/get-started)
|
||||
|
||||
## Support
|
||||
|
||||
If you continue to have issues:
|
||||
|
||||
1. Verify your Meta Business Account has WhatsApp API access
|
||||
2. Check that your phone number is verified in WhatsApp Manager
|
||||
3. Ensure you're using Graph API v21.0 or later
|
||||
4. Review the [WhatsApp Business API changelog](https://developers.facebook.com/docs/whatsapp/changelog) for updates
|
||||
@@ -68,38 +68,87 @@ func NewClient(cfg config.WhatsAppConfig, eventBus *events.EventBus, mediaConfig
|
||||
|
||||
// Connect validates the Business API credentials
|
||||
func (c *Client) Connect(ctx context.Context) error {
|
||||
// Validate credentials by making a test request to get phone number details
|
||||
url := fmt.Sprintf("https://graph.facebook.com/%s/%s",
|
||||
c.config.APIVersion,
|
||||
c.config.PhoneNumberID)
|
||||
logging.Info("Validating WhatsApp Business API credentials", "account_id", c.id)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.config.AccessToken)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
// Step 1: Validate token and check permissions
|
||||
tokenInfo, err := c.validateToken(ctx)
|
||||
if err != nil {
|
||||
c.eventBus.Publish(events.WhatsAppPairFailedEvent(ctx, c.id, err))
|
||||
return fmt.Errorf("failed to validate credentials: %w", err)
|
||||
return fmt.Errorf("token validation failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
err := fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
|
||||
// Log token information
|
||||
logging.Info("Access token validated",
|
||||
"account_id", c.id,
|
||||
"token_type", tokenInfo.Type,
|
||||
"app", tokenInfo.Application,
|
||||
"app_id", tokenInfo.AppID,
|
||||
"expires", c.formatExpiry(tokenInfo.ExpiresAt),
|
||||
"scopes", strings.Join(tokenInfo.Scopes, ", "))
|
||||
|
||||
// Check for required permissions
|
||||
requiredScopes := []string{"whatsapp_business_management", "whatsapp_business_messaging"}
|
||||
missingScopes := c.checkMissingScopes(tokenInfo.Scopes, requiredScopes)
|
||||
if len(missingScopes) > 0 {
|
||||
err := fmt.Errorf("token missing required permissions: %s", strings.Join(missingScopes, ", "))
|
||||
logging.Error("Insufficient token permissions",
|
||||
"account_id", c.id,
|
||||
"missing_scopes", strings.Join(missingScopes, ", "),
|
||||
"current_scopes", strings.Join(tokenInfo.Scopes, ", "))
|
||||
c.eventBus.Publish(events.WhatsAppPairFailedEvent(ctx, c.id, err))
|
||||
return err
|
||||
}
|
||||
|
||||
// Step 2: Get phone number details
|
||||
phoneDetails, err := c.getPhoneNumberDetails(ctx)
|
||||
if err != nil {
|
||||
c.eventBus.Publish(events.WhatsAppPairFailedEvent(ctx, c.id, err))
|
||||
return fmt.Errorf("failed to get phone number details: %w", err)
|
||||
}
|
||||
|
||||
// Log phone number information
|
||||
logging.Info("Phone number details retrieved",
|
||||
"account_id", c.id,
|
||||
"phone_number_id", phoneDetails.ID,
|
||||
"display_number", phoneDetails.DisplayPhoneNumber,
|
||||
"verified_name", phoneDetails.VerifiedName,
|
||||
"verification_status", phoneDetails.CodeVerificationStatus,
|
||||
"quality_rating", phoneDetails.QualityRating,
|
||||
"throughput_level", phoneDetails.Throughput.Level)
|
||||
|
||||
// Warn if phone number is not verified
|
||||
if phoneDetails.CodeVerificationStatus != "VERIFIED" {
|
||||
logging.Warn("Phone number is not verified - messaging capabilities may be limited",
|
||||
"account_id", c.id,
|
||||
"status", phoneDetails.CodeVerificationStatus)
|
||||
}
|
||||
|
||||
// Step 3: Get business account details (if business_account_id is provided)
|
||||
if c.config.BusinessAccountID != "" {
|
||||
businessDetails, err := c.getBusinessAccountDetails(ctx)
|
||||
if err != nil {
|
||||
logging.Warn("Failed to get business account details (non-critical)",
|
||||
"account_id", c.id,
|
||||
"business_account_id", c.config.BusinessAccountID,
|
||||
"error", err)
|
||||
} else {
|
||||
logging.Info("Business account details retrieved",
|
||||
"account_id", c.id,
|
||||
"business_account_id", businessDetails.ID,
|
||||
"business_name", businessDetails.Name,
|
||||
"timezone_id", businessDetails.TimezoneID)
|
||||
}
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.connected = true
|
||||
c.mu.Unlock()
|
||||
|
||||
logging.Info("Business API client connected", "account_id", c.id, "phone", c.phoneNumber)
|
||||
c.eventBus.Publish(events.WhatsAppConnectedEvent(ctx, c.id, c.phoneNumber))
|
||||
logging.Info("Business API client connected successfully",
|
||||
"account_id", c.id,
|
||||
"phone", phoneDetails.DisplayPhoneNumber,
|
||||
"verified_name", phoneDetails.VerifiedName)
|
||||
c.eventBus.Publish(events.WhatsAppConnectedEvent(ctx, c.id, phoneDetails.DisplayPhoneNumber))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -340,3 +389,154 @@ func jidToPhoneNumber(jid types.JID) string {
|
||||
|
||||
return phone
|
||||
}
|
||||
|
||||
// validateToken validates the access token and returns token information
|
||||
func (c *Client) validateToken(ctx context.Context) (*TokenDebugData, error) {
|
||||
url := fmt.Sprintf("https://graph.facebook.com/%s/debug_token?input_token=%s",
|
||||
c.config.APIVersion,
|
||||
c.config.AccessToken)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create token validation request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.config.AccessToken)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to validate token: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read token validation response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var errResp ErrorResponse
|
||||
if err := json.Unmarshal(body, &errResp); err == nil {
|
||||
return nil, fmt.Errorf("token validation failed: %s (code: %d)", errResp.Error.Message, errResp.Error.Code)
|
||||
}
|
||||
return nil, fmt.Errorf("token validation returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var tokenResp TokenDebugResponse
|
||||
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse token validation response: %w", err)
|
||||
}
|
||||
|
||||
if !tokenResp.Data.IsValid {
|
||||
return nil, fmt.Errorf("access token is invalid or expired")
|
||||
}
|
||||
|
||||
return &tokenResp.Data, nil
|
||||
}
|
||||
|
||||
// getPhoneNumberDetails retrieves details about the phone number
|
||||
func (c *Client) getPhoneNumberDetails(ctx context.Context) (*PhoneNumberDetails, error) {
|
||||
url := fmt.Sprintf("https://graph.facebook.com/%s/%s",
|
||||
c.config.APIVersion,
|
||||
c.config.PhoneNumberID)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create phone number details request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.config.AccessToken)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get phone number details: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read phone number details response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var errResp ErrorResponse
|
||||
if err := json.Unmarshal(body, &errResp); err == nil {
|
||||
return nil, fmt.Errorf("API error: %s (code: %d, subcode: %d)",
|
||||
errResp.Error.Message, errResp.Error.Code, errResp.Error.ErrorSubcode)
|
||||
}
|
||||
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var phoneDetails PhoneNumberDetails
|
||||
if err := json.Unmarshal(body, &phoneDetails); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse phone number details: %w", err)
|
||||
}
|
||||
|
||||
return &phoneDetails, nil
|
||||
}
|
||||
|
||||
// getBusinessAccountDetails retrieves details about the business account
|
||||
func (c *Client) getBusinessAccountDetails(ctx context.Context) (*BusinessAccountDetails, error) {
|
||||
url := fmt.Sprintf("https://graph.facebook.com/%s/%s",
|
||||
c.config.APIVersion,
|
||||
c.config.BusinessAccountID)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create business account details request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.config.AccessToken)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get business account details: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read business account details response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var errResp ErrorResponse
|
||||
if err := json.Unmarshal(body, &errResp); err == nil {
|
||||
return nil, fmt.Errorf("API error: %s (code: %d)", errResp.Error.Message, errResp.Error.Code)
|
||||
}
|
||||
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var businessDetails BusinessAccountDetails
|
||||
if err := json.Unmarshal(body, &businessDetails); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse business account details: %w", err)
|
||||
}
|
||||
|
||||
return &businessDetails, nil
|
||||
}
|
||||
|
||||
// checkMissingScopes checks which required scopes are missing from the token
|
||||
func (c *Client) checkMissingScopes(currentScopes []string, requiredScopes []string) []string {
|
||||
scopeMap := make(map[string]bool)
|
||||
for _, scope := range currentScopes {
|
||||
scopeMap[scope] = true
|
||||
}
|
||||
|
||||
var missing []string
|
||||
for _, required := range requiredScopes {
|
||||
if !scopeMap[required] {
|
||||
missing = append(missing, required)
|
||||
}
|
||||
}
|
||||
|
||||
return missing
|
||||
}
|
||||
|
||||
// formatExpiry formats the expiry timestamp for logging
|
||||
func (c *Client) formatExpiry(expiresAt int64) string {
|
||||
if expiresAt == 0 {
|
||||
return "never"
|
||||
}
|
||||
expiryTime := time.Unix(expiresAt, 0)
|
||||
return expiryTime.Format("2006-01-02 15:04:05 MST")
|
||||
}
|
||||
|
||||
@@ -191,3 +191,45 @@ type WebhookError struct {
|
||||
Code int `json:"code"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// TokenDebugResponse represents the response from debug_token endpoint
|
||||
type TokenDebugResponse struct {
|
||||
Data TokenDebugData `json:"data"`
|
||||
}
|
||||
|
||||
// TokenDebugData contains token validation information
|
||||
type TokenDebugData struct {
|
||||
AppID string `json:"app_id"`
|
||||
Type string `json:"type"`
|
||||
Application string `json:"application"`
|
||||
DataAccessExpiresAt int64 `json:"data_access_expires_at"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
IssuedAt int64 `json:"issued_at,omitempty"`
|
||||
Scopes []string `json:"scopes"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
// PhoneNumberDetails represents phone number information from the API
|
||||
type PhoneNumberDetails struct {
|
||||
ID string `json:"id"`
|
||||
VerifiedName string `json:"verified_name"`
|
||||
CodeVerificationStatus string `json:"code_verification_status"`
|
||||
DisplayPhoneNumber string `json:"display_phone_number"`
|
||||
QualityRating string `json:"quality_rating"`
|
||||
PlatformType string `json:"platform_type"`
|
||||
Throughput ThroughputInfo `json:"throughput"`
|
||||
}
|
||||
|
||||
// ThroughputInfo contains throughput information
|
||||
type ThroughputInfo struct {
|
||||
Level string `json:"level"`
|
||||
}
|
||||
|
||||
// BusinessAccountDetails represents business account information
|
||||
type BusinessAccountDetails struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
TimezoneID string `json:"timezone_id"`
|
||||
MessageTemplateNamespace string `json:"message_template_namespace,omitempty"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user