diff --git a/CHANGES.MD b/CHANGES.MD index c5ea4eb..e9d63d0 100644 --- a/CHANGES.MD +++ b/CHANGES.MD @@ -1,4 +1,11 @@ -# CHANGES +# CHANGES + +## 1.8.0 (2026-08-18) + +### Added +- **Global Profile API** support: `GlobalProfileRequest`/`GlobalProfileResponse` for + `GET /v3/accounts/{accountId}/global_profile/users/{userId}` and + `GlobalProfileLookupRequest` for `POST /v3/accounts/{accountId}/global_profile/lookup` ## 1.7.0 (2025-12-03) diff --git a/README.md b/README.md index f5e2940..132f4da 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The official Sift .NET client, supporting .NET Standard 2.0+ -## Latest Release (v1.7.0) +## Latest Release (v1.8.0) See [CHANGES.MD](CHANGES.MD) for full release history. @@ -1052,6 +1052,40 @@ var booking = new Booking // Handle InnerException } +### Global Profile + + // Get Global Profile for a user + try + { + GlobalProfileResponse response = sift.SendAsync(new GlobalProfileRequest + { + AccountId = "ACCOUNT_ID", + UserId = "gary", + GlobalOnly = false, + IncludeOwnData = true + }).Result; + } + catch (AggregateException ae) + { + // Handle InnerException + } + + // Look up a Global Profile by email and/or phone. + // At least one of Email or Phone is required; omitting both throws a MissingFieldException. + try + { + GlobalProfileResponse response = sift.SendAsync(new GlobalProfileLookupRequest + { + AccountId = "ACCOUNT_ID", + Email = "gary@example.com", + Phone = "+15555550100" + }).Result; + } + catch (AggregateException ae) + { + // Handle InnerException + } + ### Workflows // Workflow Status diff --git a/Sift/Core/Client.cs b/Sift/Core/Client.cs index 6049e17..5fa6d04 100644 --- a/Sift/Core/Client.cs +++ b/Sift/Core/Client.cs @@ -120,6 +120,16 @@ public async Task SendAsync(GetMerchantDetailsReques return await SendAsync(getMerchantRequest); } + public async Task SendAsync(GlobalProfileRequest globalProfileRequest) + { + return await SendAsync(globalProfileRequest); + } + + public async Task SendAsync(GlobalProfileLookupRequest globalProfileLookupRequest) + { + return await SendAsync(globalProfileLookupRequest); + } + async Task SendAsync(SiftRequest siftRequest) where T : SiftResponse { siftRequest.ApiKey = this.apiKey; diff --git a/Sift/Request/GlobalProfileRequest.cs b/Sift/Request/GlobalProfileRequest.cs new file mode 100644 index 0000000..4ae5ca6 --- /dev/null +++ b/Sift/Request/GlobalProfileRequest.cs @@ -0,0 +1,94 @@ +using Newtonsoft.Json; +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; + +namespace Sift +{ + public class GlobalProfileRequest : SiftRequest + { + static readonly String GlobalProfileUrl = @"https://api.sift.com/v3/accounts/{0}/global_profile/users/{1}"; + + public string AccountId { get; set; } + public string UserId { get; set; } + public bool? GlobalOnly { get; set; } + public bool? IncludeOwnData { get; set; } + + public override HttpRequestMessage Request + { + get + { + var request = new HttpRequestMessage(HttpMethod.Get, Url); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.Default.GetBytes(ApiKey))); + return request; + } + } + + protected override Uri Url + { + get + { + var url = new Uri(String.Format(GlobalProfileUrl, + Uri.EscapeDataString(AccountId), + Uri.EscapeDataString(UserId))); + + if (GlobalOnly.HasValue) + { + url = url.AddQuery("global_only", GlobalOnly.Value.ToString().ToLowerInvariant()); + } + + if (IncludeOwnData.HasValue) + { + url = url.AddQuery("include_own_data", IncludeOwnData.Value.ToString().ToLowerInvariant()); + } + + return url; + } + } + } + + public class GlobalProfileLookupRequest : SiftRequest + { + static readonly String GlobalProfileLookupUrl = @"https://api.sift.com/v3/accounts/{0}/global_profile/lookup"; + + [JsonIgnore] + public string AccountId { get; set; } + + [JsonIgnore] + public override string ApiKey { get; set; } + + [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] + public string Email { get; set; } + + [JsonProperty("phone", NullValueHandling = NullValueHandling.Ignore)] + public string Phone { get; set; } + + [JsonIgnore] + public override HttpRequestMessage Request + { + get + { + if (String.IsNullOrEmpty(Email) && String.IsNullOrEmpty(Phone)) + { + throw new MissingFieldException("At least one of Email or Phone is required."); + } + + var request = new HttpRequestMessage(HttpMethod.Post, Url); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.Default.GetBytes(ApiKey))); + request.Content = new StringContent(JsonConvert.SerializeObject(this), Encoding.UTF8, "application/json"); + return request; + } + } + + [JsonIgnore] + protected override Uri Url + { + get + { + return new Uri(String.Format(GlobalProfileLookupUrl, + Uri.EscapeDataString(AccountId))); + } + } + } +} diff --git a/Sift/Response/GlobalProfileResponse.cs b/Sift/Response/GlobalProfileResponse.cs new file mode 100644 index 0000000..75782ec --- /dev/null +++ b/Sift/Response/GlobalProfileResponse.cs @@ -0,0 +1,182 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Sift +{ + public class GlobalProfileResponse : SiftResponse + { + [JsonProperty("error_code")] + public int? ErrorCode { get; set; } + + [JsonProperty("lookback_months")] + public int? LookbackMonths { get; set; } + + [JsonProperty("profile_summary")] + public ProfileSummaryJson ProfileSummary { get; set; } + + [JsonProperty("identity_age")] + public IdentityAgeJson IdentityAge { get; set; } + + [JsonProperty("user_decisions")] + public UserDecisionsJson UserDecisions { get; set; } + + [JsonProperty("chargebacks")] + public ChargebacksJson Chargebacks { get; set; } + + [JsonProperty("orders")] + public OrdersJson Orders { get; set; } + + [JsonProperty("transactions")] + public TransactionsJson Transactions { get; set; } + + [JsonProperty("locations")] + public LocationsJson Locations { get; set; } + + public class ProfileSummaryJson + { + [JsonProperty("identity_found")] + public bool IdentityFound { get; set; } + + [JsonProperty("has_links")] + public bool? HasLinks { get; set; } + + [JsonProperty("link_count")] + public int? LinkCount { get; set; } + + [JsonProperty("linked_accounts_count_per_industry")] + public Dictionary LinkedAccountsCountPerIndustry { get; set; } + } + + public class IdentityAgeJson + { + [JsonProperty("oldest_account_age_timestamp")] + public long? OldestAccountAgeTimestamp { get; set; } + + [JsonProperty("newest_account_age_timestamp")] + public long? NewestAccountAgeTimestamp { get; set; } + + [JsonProperty("average_account_age_timestamp")] + public long? AverageAccountAgeTimestamp { get; set; } + } + + public class UserDecisionsJson + { + [JsonProperty("total")] + public long Total { get; set; } + + [JsonProperty("blocked")] + public long Blocked { get; set; } + + [JsonProperty("watched")] + public long Watched { get; set; } + + [JsonProperty("accepted")] + public long Accepted { get; set; } + + [JsonProperty("manual")] + public long Manual { get; set; } + + [JsonProperty("auto")] + public long Auto { get; set; } + + [JsonProperty("last_type")] + public string LastType { get; set; } + + [JsonProperty("last_timestamp")] + public long? LastTimestamp { get; set; } + } + + public class ChargebacksJson + { + [JsonProperty("total")] + public long Total { get; set; } + + [JsonProperty("fraudulent")] + public long Fraudulent { get; set; } + + [JsonProperty("other")] + public long Other { get; set; } + + [JsonProperty("last_timestamp")] + public long? LastTimestamp { get; set; } + + [JsonProperty("last_fraudulent_timestamp")] + public long? LastFraudulentTimestamp { get; set; } + } + + public class OrdersJson + { + [JsonProperty("total")] + public long Total { get; set; } + + [JsonProperty("blocked")] + public long Blocked { get; set; } + + [JsonProperty("watched")] + public long Watched { get; set; } + + [JsonProperty("accepted")] + public long Accepted { get; set; } + + [JsonProperty("last_timestamp")] + public long? LastTimestamp { get; set; } + + [JsonProperty("last_blocked_timestamp")] + public long? LastBlockedTimestamp { get; set; } + } + + public class TransactionsJson + { + [JsonProperty("total")] + public long Total { get; set; } + + [JsonProperty("failed_fraud")] + public long FailedFraud { get; set; } + + [JsonProperty("failed_other")] + public long FailedOther { get; set; } + + [JsonProperty("successful")] + public long Successful { get; set; } + + [JsonProperty("last_timestamp")] + public long? LastTimestamp { get; set; } + + [JsonProperty("last_failed_fraud_timestamp")] + public long? LastFailedFraudTimestamp { get; set; } + } + + public class LocationsJson + { + [JsonProperty("unique_billing_addresses")] + public long UniqueBillingAddresses { get; set; } + + [JsonProperty("unique_shipping_addresses")] + public long UniqueShippingAddresses { get; set; } + + [JsonProperty("distinct_countries_count")] + public long DistinctCountriesCount { get; set; } + + [JsonProperty("distinct_regions_count")] + public long DistinctRegionsCount { get; set; } + + [JsonProperty("location_connected_accounts")] + public List LocationConnectedAccounts { get; set; } + + [JsonProperty("location_last_used_timestamp")] + public long? LocationLastUsedTimestamp { get; set; } + } + + public class LocationConnectedAccountJson + { + [JsonProperty("city")] + public string City { get; set; } + + [JsonProperty("country")] + public string Country { get; set; } + + [JsonProperty("region")] + public string Region { get; set; } + } + } +} diff --git a/Sift/Sift.csproj b/Sift/Sift.csproj index 76009fc..7dbe498 100644 --- a/Sift/Sift.csproj +++ b/Sift/Sift.csproj @@ -4,8 +4,8 @@ Sift Sift Sift - 1.7.0 - Release 1.7.0 + 1.8.0 + Release 1.8.0 netstandard2.0 Sift sift;siftscience;client;api;client;async diff --git a/Test/Test.cs b/Test/Test.cs index 35648b2..1654b18 100644 --- a/Test/Test.cs +++ b/Test/Test.cs @@ -1083,6 +1083,199 @@ public void TestGetMerchantDetailsRequest() getMerchantDetailRequest.Request.RequestUri!.ToString()); } + [Fact] + public void TestGlobalProfileRequest() + { + //Please provide the valid account id in place of dummy number; + var accountId = "12345678"; + var userId = "user-1"; + //Please provide the valid api key in place of 'key' + var apiKey = "key"; + var globalProfileRequest = new GlobalProfileRequest + { + AccountId = accountId, + UserId = userId + }; + globalProfileRequest.ApiKey = apiKey; + + Assert.Equal(Convert.ToBase64String(Encoding.Default.GetBytes(apiKey)), + globalProfileRequest.Request.Headers.Authorization!.Parameter); + + Assert.Equal("https://api.sift.com/v3/accounts/" + accountId + "/global_profile/users/" + userId, + globalProfileRequest.Request.RequestUri!.ToString()); + } + + [Fact] + public void TestGlobalProfileRequestWithQueryParams() + { + var accountId = "12345678"; + var userId = "user-1"; + var apiKey = "key"; + var globalProfileRequest = new GlobalProfileRequest + { + AccountId = accountId, + UserId = userId, + GlobalOnly = true, + IncludeOwnData = false + }; + globalProfileRequest.ApiKey = apiKey; + + Assert.Equal("https://api.sift.com/v3/accounts/" + accountId + "/global_profile/users/" + userId + + "?global_only=true&include_own_data=false", + Uri.UnescapeDataString(globalProfileRequest.Request.RequestUri!.ToString())); + } + + [Fact] + public void TestGlobalProfileLookupRequest() + { + var accountId = "12345678"; + var apiKey = "key"; + var globalProfileLookupRequest = new GlobalProfileLookupRequest + { + AccountId = accountId, + Email = "gary@example.com", + Phone = "+15555550100" + }; + globalProfileLookupRequest.ApiKey = apiKey; + + Assert.Equal(Convert.ToBase64String(Encoding.Default.GetBytes(apiKey)), + globalProfileLookupRequest.Request.Headers.Authorization!.Parameter); + + Assert.Equal("https://api.sift.com/v3/accounts/" + accountId + "/global_profile/lookup", + globalProfileLookupRequest.Request.RequestUri!.ToString()); + + Assert.Equal("{\"email\":\"gary@example.com\",\"phone\":\"+15555550100\"}", + JsonConvert.SerializeObject(globalProfileLookupRequest)); + } + + [Fact] + public void TestGlobalProfileLookupRequestWithEmailOnly() + { + var accountId = "12345678"; + var apiKey = "key"; + var globalProfileLookupRequest = new GlobalProfileLookupRequest + { + AccountId = accountId, + Email = "gary@example.com" + }; + globalProfileLookupRequest.ApiKey = apiKey; + + Assert.Equal("https://api.sift.com/v3/accounts/" + accountId + "/global_profile/lookup", + globalProfileLookupRequest.Request.RequestUri!.ToString()); + + Assert.Equal("{\"email\":\"gary@example.com\"}", + JsonConvert.SerializeObject(globalProfileLookupRequest)); + } + + [Fact] + public void TestGlobalProfileLookupRequestRequiresEmailOrPhone() + { + var globalProfileLookupRequest = new GlobalProfileLookupRequest + { + AccountId = "12345678" + }; + globalProfileLookupRequest.ApiKey = "key"; + + Assert.Throws( + () => globalProfileLookupRequest.Request + ); + } + + [Fact] + public void TestGlobalProfileResponseDeserialization() + { + var json = "{" + + "\"status\":0," + + "\"error_message\":\"OK\"," + + "\"error_code\":null," + + "\"lookback_months\":12," + + "\"profile_summary\":{" + + "\"identity_found\":true," + + "\"has_links\":true," + + "\"link_count\":7," + + "\"linked_accounts_count_per_industry\":{\"finances\":3,\"internet\":4}" + + "}," + + "\"identity_age\":{" + + "\"oldest_account_age_timestamp\":1681090536," + + "\"newest_account_age_timestamp\":1881090536," + + "\"average_account_age_timestamp\":1781090536" + + "}," + + "\"user_decisions\":{" + + "\"total\":12,\"blocked\":2,\"watched\":3,\"accepted\":6," + + "\"manual\":4,\"auto\":8,\"last_type\":\"BLOCK\",\"last_timestamp\":1881090536" + + "}," + + "\"chargebacks\":{" + + "\"total\":3,\"fraudulent\":2,\"other\":1," + + "\"last_timestamp\":1881090536,\"last_fraudulent_timestamp\":1881090536" + + "}," + + "\"orders\":{" + + "\"total\":50,\"blocked\":2,\"watched\":5,\"accepted\":40," + + "\"last_timestamp\":1881090536,\"last_blocked_timestamp\":1881090536" + + "}," + + "\"transactions\":{" + + "\"total\":120,\"failed_fraud\":3,\"failed_other\":5,\"successful\":112," + + "\"last_timestamp\":1881090536,\"last_failed_fraud_timestamp\":1881090536" + + "}," + + "\"locations\":{" + + "\"unique_billing_addresses\":2,\"unique_shipping_addresses\":4," + + "\"distinct_countries_count\":3,\"distinct_regions_count\":5," + + "\"location_connected_accounts\":[{\"city\":\"Kyiv\",\"country\":\"UA\",\"region\":\"Kyiv Oblast\"}]," + + "\"location_last_used_timestamp\":1881090536" + + "}" + + "}"; + + var response = JsonConvert.DeserializeObject(json); + + Assert.Equal(0, response!.Status); + Assert.Equal(12, response.LookbackMonths); + Assert.True(response.ProfileSummary!.IdentityFound); + Assert.Equal(7, response.ProfileSummary.LinkCount); + Assert.Equal(3L, response.ProfileSummary.LinkedAccountsCountPerIndustry!["finances"]); + Assert.Equal(1681090536, response.IdentityAge!.OldestAccountAgeTimestamp); + Assert.Equal(12L, response.UserDecisions!.Total); + Assert.Equal(3L, response.Chargebacks!.Total); + Assert.Equal(50L, response.Orders!.Total); + Assert.Equal(120L, response.Transactions!.Total); + Assert.Equal(2L, response.Locations!.UniqueBillingAddresses); + Assert.Single(response.Locations.LocationConnectedAccounts!); + Assert.Equal("Kyiv", response.Locations.LocationConnectedAccounts![0].City); + Assert.Equal("UA", response.Locations.LocationConnectedAccounts![0].Country); + Assert.Equal("Kyiv Oblast", response.Locations.LocationConnectedAccounts![0].Region); + } + + [Fact] + public void TestGlobalProfileResponseWhenIdentityNotFound() + { + var json = "{" + + "\"status\":0," + + "\"error_message\":\"OK\"," + + "\"error_code\":null," + + "\"lookback_months\":null," + + "\"profile_summary\":{" + + "\"identity_found\":false," + + "\"has_links\":false," + + "\"link_count\":0," + + "\"linked_accounts_count_per_industry\":{}" + + "}," + + "\"identity_age\":null," + + "\"user_decisions\":null," + + "\"chargebacks\":null," + + "\"orders\":null," + + "\"transactions\":null," + + "\"locations\":null" + + "}"; + + var response = JsonConvert.DeserializeObject(json); + + Assert.False(response!.ProfileSummary!.IdentityFound); + Assert.Null(response.IdentityAge); + Assert.Null(response.UserDecisions); + Assert.Null(response.Chargebacks); + Assert.Null(response.Orders); + Assert.Null(response.Transactions); + Assert.Null(response.Locations); + } + [Fact] public void TestChargebackEvent() {