From d095b86192bbcec802ad38486a1c738bd6a9e0a0 Mon Sep 17 00:00:00 2001 From: Andrey Skoskin Date: Thu, 29 Jan 2026 11:40:21 +0300 Subject: [PATCH 1/6] Add Reports API basic implementation --- report.go | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 report.go diff --git a/report.go b/report.go new file mode 100644 index 0000000..9438452 --- /dev/null +++ b/report.go @@ -0,0 +1,141 @@ +package nexmo + +import ( + "encoding/base64" + "encoding/json" + "io/ioutil" + "net/http" + "time" +) + +// Report represents the Report API functions for sending text messages. +// https://developer.vonage.com/en/api/reports +type Report struct { + client *Client +} + +// Product types. +const ( + ProductSMS = "SMS" + ProductSMSTrafficControl = "SMS-TRAFFIC-CONTROL" + ProductVoiceCall = "VOICE-CALL" + ProductVoiceFailed = "VOICE-FAILED" + ProductVoiceTTS = "VOICE-TTS" + ProductInAppVoice = "IN-APP-VOICE" + ProductWebSocketCall = "WEBSOCKET-CALL" + ProductASR = "ASR" + ProductAMD = "AMD" + ProductVerifyAPI = "VERIFY-API" + ProductVerifyV2 = "VERIFY-V2" + ProductNumberInsight = "NUMBER-INSIGHT" + ProductNumberInsightV2 = "NUMBER-INSIGHT-V2" + ProductConversationEvent = "CONVERSATION-EVENT" + ProductConversationMessage = "CONVERSATION-MESSAGE" + ProductMessages = "MESSAGES" + ProductVideoAPI = "VIDEO-API" + ProductNetworkAPIEvent = "NETWORK-API-EVENT" + ProductReportsUsage = "REPORTS-USAGE" +) + +// Direction types. +const ( + DirectionInbound = "inbound" + DirectionOutbound = "outbound" +) + +// RecordsRequest defines a records request message. +type RecordsRequest struct { + AccountID string + ID string + Product string + Direction string +} + +// RecordsResponse defines a records response message. +type RecordsResponse struct { + Links struct { + Self struct { + Href time.Time `json:"href"` + } `json:"self"` + Next struct { + Href string `json:"href"` + } `json:"next"` + } `json:"_links"` + Cursor string `json:"cursor"` + Iv string `json:"iv"` + RequestID string `json:"request_id"` + RequestStatus string `json:"request_status"` + ReceivedAt time.Time `json:"received_at"` + ItemsCount int `json:"items_count"` + IdsNotFound string `json:"ids_not_found"` + Product string `json:"product"` + Records []struct { + AccountID string `json:"account_id"` + MessageID string `json:"message_id"` + AccountRef string `json:"account_ref"` + ClientRef string `json:"client_ref"` + Direction string `json:"direction"` + From string `json:"from"` + To string `json:"to"` + ForcedFrom string `json:"forced_from"` + ChangedFrom string `json:"changed_from"` + Concatenated string `json:"concatenated"` + MessageBody string `json:"message_body"` + Network string `json:"network"` + NetworkName string `json:"network_name"` + Country string `json:"country"` + CountryName string `json:"country_name"` + DateReceived time.Time `json:"date_received"` + DateFinalized time.Time `json:"date_finalized"` + Latency string `json:"latency"` + Status string `json:"status"` + ErrorCode string `json:"error_code"` + ErrorCodeDescription string `json:"error_code_description"` + Currency string `json:"currency"` + TotalPrice string `json:"total_price"` + ID string `json:"id"` + Dcs string `json:"dcs"` + ValidityPeriod string `json:"validity_period"` + IPAddress string `json:"ip_address"` + Udh string `json:"udh"` + WorkflowID string `json:"workflow_id"` + } `json:"records"` +} + +// Send the message using the specified Nexmo API client. +func (c *Report) Send(req *RecordsRequest) (*RecordsResponse, error) { + var r, err = http.NewRequest("GET", apiRootv2+"/v2/reports/records", nil) + if err != nil { + return nil, err + } + + var q = r.URL.Query() + q.Add("account_id", req.AccountID) + q.Add("id", req.ID) + q.Add("product", req.Product) + q.Add("direction", req.Direction) + + var auth = base64.RawURLEncoding.EncodeToString([]byte(c.client.apiKey + ":" + c.client.apiSecret)) + + r.Header.Add("Authorization", "Basic "+auth) + r.Header.Add("Accept", "application/json") + r.Header.Add("Content-Type", "application/json") + + var resp *http.Response + if resp, err = c.client.HTTPClient.Do(r); err != nil { + return nil, err + } + defer resp.Body.Close() + + var body []byte + if body, err = ioutil.ReadAll(resp.Body); err != nil { + return nil, err + } + + var records RecordsResponse + err = json.Unmarshal(body, &records) + if err != nil { + return nil, err + } + return &records, nil +} From 0a266d55003925cf5f8d1c4b6eb8bfee31272a6e Mon Sep 17 00:00:00 2001 From: Andrey Skoskin Date: Thu, 29 Jan 2026 13:26:09 +0300 Subject: [PATCH 2/6] Add Reports API object to Client --- client.go | 2 ++ report.go => reports.go | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) rename report.go => reports.go (96%) diff --git a/client.go b/client.go index 2986cc5..37aa925 100644 --- a/client.go +++ b/client.go @@ -12,6 +12,7 @@ type Client struct { SMS *SMS USSD *USSD Verify *Verification + Reports *Reports HTTPClient *http.Client apiKey string apiSecret string @@ -37,6 +38,7 @@ func NewClient(apiKey, apiSecret string) (*Client, error) { c.SMS = &SMS{c} c.USSD = &USSD{c} c.Verify = &Verification{c} + c.Reports = &Reports{c} c.HTTPClient = http.DefaultClient return c, nil } diff --git a/report.go b/reports.go similarity index 96% rename from report.go rename to reports.go index 9438452..e04b4b3 100644 --- a/report.go +++ b/reports.go @@ -8,9 +8,9 @@ import ( "time" ) -// Report represents the Report API functions for sending text messages. +// Reports represents the Reports API functions for sending text messages. // https://developer.vonage.com/en/api/reports -type Report struct { +type Reports struct { client *Client } @@ -103,7 +103,7 @@ type RecordsResponse struct { } // Send the message using the specified Nexmo API client. -func (c *Report) Send(req *RecordsRequest) (*RecordsResponse, error) { +func (c *Reports) Send(req *RecordsRequest) (*RecordsResponse, error) { var r, err = http.NewRequest("GET", apiRootv2+"/v2/reports/records", nil) if err != nil { return nil, err From 5f0c371b0fe9a7b7788f738faf880904479f3151 Mon Sep 17 00:00:00 2001 From: Andrey Skoskin Date: Fri, 30 Jan 2026 12:19:42 +0300 Subject: [PATCH 3/6] Add Reports API object to Client - account ID --- reports.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/reports.go b/reports.go index e04b4b3..c4efede 100644 --- a/reports.go +++ b/reports.go @@ -45,7 +45,6 @@ const ( // RecordsRequest defines a records request message. type RecordsRequest struct { - AccountID string ID string Product string Direction string @@ -110,7 +109,7 @@ func (c *Reports) Send(req *RecordsRequest) (*RecordsResponse, error) { } var q = r.URL.Query() - q.Add("account_id", req.AccountID) + q.Add("account_id", c.client.apiKey) q.Add("id", req.ID) q.Add("product", req.Product) q.Add("direction", req.Direction) From fa6ebdff20ef3204a15f6e11d6816e5ef6fdf8fa Mon Sep 17 00:00:00 2001 From: Andrey Skoskin Date: Wed, 4 Feb 2026 10:42:17 +0300 Subject: [PATCH 4/6] Add Reports API object to Client - HTTP error statuses handling --- error.go | 14 ++++++++++++++ reports.go | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 error.go diff --git a/error.go b/error.go new file mode 100644 index 0000000..7efbd58 --- /dev/null +++ b/error.go @@ -0,0 +1,14 @@ +package nexmo + +// Error defines an error response message. +type Error struct { + Type string `json:"type"` + Title string `json:"title"` + Detail string `json:"detail"` + Instance string `json:"instance"` + HTTPStatus int `json:"-"` +} + +func (err *Error) Error() string { + return err.Title +} diff --git a/reports.go b/reports.go index c4efede..49afb7f 100644 --- a/reports.go +++ b/reports.go @@ -3,6 +3,7 @@ package nexmo import ( "encoding/base64" "encoding/json" + "fmt" "io/ioutil" "net/http" "time" @@ -126,15 +127,33 @@ func (c *Reports) Send(req *RecordsRequest) (*RecordsResponse, error) { } defer resp.Body.Close() + if resp.StatusCode >= 500 { + return nil, fmt.Errorf("internal server error: %d", resp.StatusCode) + } + var body []byte if body, err = ioutil.ReadAll(resp.Body); err != nil { return nil, err } + if resp.StatusCode >= 400 { + var errMsg = Error{ + HTTPStatus: resp.StatusCode, + } + + err = json.Unmarshal(body, &errMsg) + if err != nil { + return nil, err + } + + return nil, &errMsg + } + var records RecordsResponse err = json.Unmarshal(body, &records) if err != nil { return nil, err } + return &records, nil } From 56374b499b3b7316c26caf423e978ba48727865f Mon Sep 17 00:00:00 2001 From: Andrey Skoskin Date: Thu, 5 Mar 2026 15:30:38 +0300 Subject: [PATCH 5/6] Add Reports API object to Client - fix URL query params encoding --- reports.go | 1 + 1 file changed, 1 insertion(+) diff --git a/reports.go b/reports.go index 49afb7f..2a8825e 100644 --- a/reports.go +++ b/reports.go @@ -114,6 +114,7 @@ func (c *Reports) Send(req *RecordsRequest) (*RecordsResponse, error) { q.Add("id", req.ID) q.Add("product", req.Product) q.Add("direction", req.Direction) + r.URL.RawQuery = q.Encode() var auth = base64.RawURLEncoding.EncodeToString([]byte(c.client.apiKey + ":" + c.client.apiSecret)) From 26d99a35b9d36d8a6e44ae9afcb1a5fb8e932c24 Mon Sep 17 00:00:00 2001 From: Andrey Skoskin Date: Thu, 5 Mar 2026 17:18:27 +0300 Subject: [PATCH 6/6] Add Reports API object to Client - fix JSON and Auth Base64 encoding --- reports.go | 79 +++++++++++++++++++++++++++--------------------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/reports.go b/reports.go index 2a8825e..f16eb39 100644 --- a/reports.go +++ b/reports.go @@ -6,7 +6,6 @@ import ( "fmt" "io/ioutil" "net/http" - "time" ) // Reports represents the Reports API functions for sending text messages. @@ -55,50 +54,50 @@ type RecordsRequest struct { type RecordsResponse struct { Links struct { Self struct { - Href time.Time `json:"href"` + Href string `json:"href"` } `json:"self"` Next struct { Href string `json:"href"` } `json:"next"` } `json:"_links"` - Cursor string `json:"cursor"` - Iv string `json:"iv"` - RequestID string `json:"request_id"` - RequestStatus string `json:"request_status"` - ReceivedAt time.Time `json:"received_at"` - ItemsCount int `json:"items_count"` - IdsNotFound string `json:"ids_not_found"` - Product string `json:"product"` + Cursor string `json:"cursor"` + Iv string `json:"iv"` + RequestID string `json:"request_id"` + RequestStatus string `json:"request_status"` + ReceivedAt string `json:"received_at"` + ItemsCount int `json:"items_count"` + IdsNotFound string `json:"ids_not_found"` + Product string `json:"product"` Records []struct { - AccountID string `json:"account_id"` - MessageID string `json:"message_id"` - AccountRef string `json:"account_ref"` - ClientRef string `json:"client_ref"` - Direction string `json:"direction"` - From string `json:"from"` - To string `json:"to"` - ForcedFrom string `json:"forced_from"` - ChangedFrom string `json:"changed_from"` - Concatenated string `json:"concatenated"` - MessageBody string `json:"message_body"` - Network string `json:"network"` - NetworkName string `json:"network_name"` - Country string `json:"country"` - CountryName string `json:"country_name"` - DateReceived time.Time `json:"date_received"` - DateFinalized time.Time `json:"date_finalized"` - Latency string `json:"latency"` - Status string `json:"status"` - ErrorCode string `json:"error_code"` - ErrorCodeDescription string `json:"error_code_description"` - Currency string `json:"currency"` - TotalPrice string `json:"total_price"` - ID string `json:"id"` - Dcs string `json:"dcs"` - ValidityPeriod string `json:"validity_period"` - IPAddress string `json:"ip_address"` - Udh string `json:"udh"` - WorkflowID string `json:"workflow_id"` + AccountID string `json:"account_id"` + MessageID string `json:"message_id"` + AccountRef string `json:"account_ref"` + ClientRef string `json:"client_ref"` + Direction string `json:"direction"` + From string `json:"from"` + To string `json:"to"` + ForcedFrom string `json:"forced_from"` + ChangedFrom string `json:"changed_from"` + Concatenated string `json:"concatenated"` + MessageBody string `json:"message_body"` + Network string `json:"network"` + NetworkName string `json:"network_name"` + Country string `json:"country"` + CountryName string `json:"country_name"` + DateReceived string `json:"date_received"` + DateFinalized string `json:"date_finalized"` + Latency string `json:"latency"` + Status string `json:"status"` + ErrorCode string `json:"error_code"` + ErrorCodeDescription string `json:"error_code_description"` + Currency string `json:"currency"` + TotalPrice string `json:"total_price"` + ID string `json:"id"` + Dcs string `json:"dcs"` + ValidityPeriod string `json:"validity_period"` + IPAddress string `json:"ip_address"` + Udh string `json:"udh"` + WorkflowID string `json:"workflow_id"` } `json:"records"` } @@ -116,7 +115,7 @@ func (c *Reports) Send(req *RecordsRequest) (*RecordsResponse, error) { q.Add("direction", req.Direction) r.URL.RawQuery = q.Encode() - var auth = base64.RawURLEncoding.EncodeToString([]byte(c.client.apiKey + ":" + c.client.apiSecret)) + var auth = base64.StdEncoding.EncodeToString([]byte(c.client.apiKey + ":" + c.client.apiSecret)) r.Header.Add("Authorization", "Basic "+auth) r.Header.Add("Accept", "application/json")