gRPC API
BidZenith's APIs are implemented as gRPC APIs. gRPC is a modern Remote Procedure Call (RPC) framework developed by Google that provides high performance and low latency.
gRPC APIs are the recommended choice to use if your integration language of choice and environment support it.
Protocol Buffers
The gRPC APIs are defined using Protocol Buffers, a powerful language-neutral, platform-neutral serialization toolset and language that makes it easy to automatically generate idiomatic clients in a variety of languages and platforms.
The protocol buffer definition (proto
) files are available to download from this website
Proto File | Description |
---|---|
account.proto | Messages to work with Accounts |
account_service.proto | Services for managing Accounts |
app_client.proto | Messages to work with App Clients |
app_client_service.proto | Services for managing App Clients |
auction_service.proto | Services for managing Auctions |
auction_settings.proto | Messages to work with Auctions |
campaign.proto | Messages to work with Campaigns |
campaign_service.proto | Services for managing Campaigns |
data_auction_service.proto | Services for managing settings related to Auctions |
data_instance.proto | Messages to work with Data Instances |
data_instance_service.proto | Services for managing Data Instances |
data_source_service.proto | Services for managing Data Sources |
data_specification.proto | Messages to work with Data Specifications |
data_specification_service.proto | Services for managing Data Specifications |
date_range.proto | A date range message |
organization_service.proto | Services for managing Organizations |
Well-Known Types
Google Protocol Buffers Well-Known Types are a set of predefined, commonly used message types that are included in the protobuf library to simplify development and ensure interoperability. These types cover a wide range of standard data representations, are available out-of-the-box and are designed to represent common concepts, such as timestamps, durations, wrappers for primitive types, and more.
BidZenith's gRPC APIs use Well-Known Types where suitable.
Authentication
BidZenith uses Bearer tokens to authorize requests, which are obtained
using the OAuth 2.0 Client Credentials Flow, using the client_id
and
client_secret
generated upon creating an app client. Consult the OAuth documentation for steps
on how to create an app client.
gRPC provides a generic mechanism to attach metadata based credentials to requests and responses using Call Credentials. Many gRPC language implementations allow a Bearer token to be acquired as part of the request flow.
Here's an implementation of CallCredentials
for C# clients that can be used to get access tokens on demand when
making requests.
- OAuth2CallCredentials
- OAuth2TokenService
// Copyright 2024 BidZenith
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using Grpc.Core;
namespace BidZenith.Api.Authentication;
/// <summary>
/// OAuth 2.0 authentication using JWT tokens.
/// </summary>
public class OAuth2CallCredentials : CallCredentials, IDisposable
{
private readonly OAuth2TokenService _tokenService;
/// <summary>
/// Instantiates a new instance of <see cref="OAuth2CallCredentials"/>
/// </summary>
public OAuth2CallCredentials(OAuth2TokenService tokenService) =>
_tokenService = tokenService ??
throw new ArgumentNullException(nameof(tokenService), "tokenService must not be null");
/// <inheritdoc />
public override void InternalPopulateConfiguration(CallCredentialsConfiguratorBase configurator, object? state) =>
configurator.SetAsyncAuthInterceptorCredentials(state, AsyncAuthInterceptor);
private async Task AsyncAuthInterceptor(AuthInterceptorContext context, Metadata metadata)
{
var accessToken = await _tokenService.GetAccessTokenAsync(context.CancellationToken).ConfigureAwait(false);
metadata.Add("Authorization", accessToken);
}
/// <inheritdoc />
public void Dispose() => _tokenService.Dispose();
}
// Copyright 2024 BidZenith
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Text.Json;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace BidZenith.Api.Authentication;
/// <summary>
/// Retrieves and refreshes access tokens for OAuth2.0 authentication
/// </summary>
/// <remarks>
/// The service retrieves an access token on the initial request and
/// reuses the token for subsequent requests. An asynchronous background task
/// waits until 30 seconds before the access token expires and refreshes it.
/// </remarks>
public class OAuth2TokenService : IDisposable
{
private static readonly JsonSerializerOptions JsonSerializerOptions =
new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
private static readonly TimeSpan ThirtySeconds = TimeSpan.FromSeconds(30);
private readonly HttpClient _client;
private readonly KeyValuePair<string, string>[] _nameValueCollection;
private readonly SemaphoreSlim _semaphore;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly ILogger _logger;
private readonly Uri _authenticationEndpoint;
private Task? _refreshTokenLoop;
private DateTime _expiresIn;
private bool _disposed;
private volatile string? _accessToken;
/// <summary>
/// Instantiates a new instance of <see cref="OAuth2TokenService"/>
/// </summary>
/// <param name="clientId">The client Id</param>
/// <param name="clientSecret">The client secret</param>
/// <param name="authenticationEndpoint">The authentication endpoint</param>
/// <param name="loggerFactory">A logger factory through which to log messages.</param>
/// <param name="client">A client used to fetch access tokens.</param>
/// <exception cref="ArgumentNullException">
/// Thrown if <paramref name="clientId" />, <paramref name="clientSecret"/>, or
// <paramref name="authenticationEndpoint"/> are null.
/// </exception>
public OAuth2TokenService(
string clientId,
string clientSecret,
string authenticationEndpoint,
ILoggerFactory? loggerFactory = null,
HttpClient? client = null)
{
if (clientId is null)
throw new ArgumentNullException(nameof(clientId), "clientId must not be null");
if (clientSecret is null)
throw new ArgumentNullException(nameof(clientSecret), "clientSecret must not be null");
if (authenticationEndpoint is null)
throw new ArgumentNullException(nameof(authenticationEndpoint), "authenticationEndpoint must not be null");
_authenticationEndpoint = new Uri(authenticationEndpoint);
_client = client ?? new HttpClient();
_nameValueCollection =
[
new KeyValuePair<string, string>("grant_type", "client_credentials"),
new KeyValuePair<string, string>("client_id", clientId),
new KeyValuePair<string, string>("client_secret", clientSecret)
];
_semaphore = new SemaphoreSlim(1, 1);
_cancellationTokenSource = new CancellationTokenSource();
_logger = loggerFactory?.CreateLogger("BidZenith.Client") ?? NullLogger.Instance;
}
/// <summary>
/// Gets an access token to authenticate to platform.
/// </summary>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>An access token to include in the Authorization header</returns>
public async Task<string> GetAccessTokenAsync(CancellationToken cancellationToken)
{
if (_accessToken is null)
_accessToken = await InitializeAccessTokenAsync(cancellationToken).ConfigureAwait(false);
return _accessToken;
}
private async Task<string> InitializeAccessTokenAsync(CancellationToken cancellationToken)
{
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (_accessToken is not null)
return _accessToken;
var token = await GetJwtTokenAsync(cancellationToken).ConfigureAwait(false);
_expiresIn = DateTime.UtcNow.AddSeconds(token.ExpiresIn);
_refreshTokenLoop = RefreshTokenLoopAsync(_cancellationTokenSource.Token);
return $"Bearer {token.AccessToken}";
}
finally
{
_semaphore.Release();
_logger.LogDebug("retrieved initial token");
}
}
private async Task RefreshTokenLoopAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
// wait until up to 30 seconds before the token expires.
var delay = _expiresIn - DateTime.UtcNow.AddSeconds(30);
if (delay > TimeSpan.Zero && delay > ThirtySeconds)
{
try
{
var maxMillisecondsDelay = Math.Min(delay.TotalMilliseconds, 4294967294);
await Task.Delay((int)maxMillisecondsDelay, cancellationToken).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
// Cancellation requested, exit the loop.
break;
}
}
try
{
var token = await GetJwtTokenAsync(cancellationToken).ConfigureAwait(false);
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
_accessToken = $"Bearer {token.AccessToken}";
_expiresIn = DateTimeHelper.UtcNow().AddSeconds(token.ExpiresIn);
}
finally
{
_semaphore.Release();
_logger.LogDebug("token refreshed");
}
}
catch (OperationCanceledException)
{
// Cancellation requested, exit the loop.
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "token refresh failed");
}
}
}
private async Task<JwtToken> GetJwtTokenAsync(CancellationToken cancellationToken)
{
var request = new HttpRequestMessage(HttpMethod.Post, _authenticationEndpoint)
{
Content = new FormUrlEncodedContent(_nameValueCollection)
};
var response = await _client.SendAsync(request, cancellationToken).ConfigureAwait(false);
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogError("ClientId/ClientSecret is invalid. Received response {response}", content);
throw new RpcException(new Status(StatusCode.Unauthenticated,
"ClientId/ClientSecret is invalid."));
}
return JsonSerializer.Deserialize<JwtToken>(content, JsonSerializerOptions);
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
return;
_cancellationTokenSource.Cancel();
if (!_refreshTokenLoop?.IsCompleted ?? false)
_refreshTokenLoop?.Wait();
_refreshTokenLoop?.Dispose();
_client.Dispose();
_semaphore.Dispose();
_cancellationTokenSource.Dispose();
_disposed = true;
}
private record struct JwtToken(string AccessToken, int ExpiresIn, string Scope, string TokenType);
}
To use this to construct a gRPC channel to use with generated C# clients
// Copyright 2024 BidZenith
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
var address = "https://app.bidzenith.com";
var tokenService = new OAuth2TokenService(clientId, clientSecret, $"{address}/oauth2/token");
var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions
{
Credentials = ChannelCredentials.Create(
ChannelCredentials.SecureSsl,
new OAuth2CallCredentials(tokenService))
});
// use the channel with the generated client.
Authorization
When creating an app client, a collection of roles are specified that determine what the app client has access to. These roles are encoded within the JSON Web Token (JWT) bearer token, and can be viewed using jwt.io.
In all cases, if an app client does not have permission to do
something, you'll get a PERMISSION_DENIED
error.
Pagination
Some APIs support pagination. Pagination is performed with a pageToken
property. This property
is an opaque string whose format can and will change over time, so its format should not
be inspected or relied upon.
A nextPageToken
property in the response of APIs that support pagination indicates whether more records can be
fetched by using the same query, and the value of nextPageToken
assigned to pageToken
on the next request.
You can change the number of items to be returned with the pageSize
parameter, defaulting to 10
. This can be a value between 1
and 100
.
Errors
We don't usually have any trouble on our end, but when we do we'll let you know!
The BidZenith API uses the following gRPC status codes:
Code | Number | Description |
---|---|---|
OK | 0 | Not an error; returned on success. |
CANCELLED | 1 | Your request was cancelled, typically by you. |
UNKNOWN | 2 | We've got some problem with our service. Please try again later. |
INVALID_ARGUMENT | 3 | Your request is malformed in some way. The response usually indicates what's wrong. |
DEADLINE_EXCEEDED | 4 | A deadline expired before the request could complete. For requests that change the state of the system, this error may be returned even if the request has completed successfully. |
NOT_FOUND | 5 | The specified resource was not found. |
ALREADY_EXISTS | 6 | The resource that you're trying to create already exists. |
PERMISSION_DENIED | 7 | Your request is not authorized to perform the operation. |
RESOURCE_EXHAUSTED | 8 | You're making too many requests at once. Slow down! |
FAILED_PRECONDITION | 9 | Your request is malformed in some way. The response usually indicates what's wrong. |
ABORTED | 10 | Your request conflicted with another operation. |
OUT_OF_RANGE | 11 | Your request has tried to perform some action outside of a valid range. |
UNIMPLEMENTED | 12 | You've hit an operation that is not implemented or is not supported/enabled. |
INTERNAL | 13 | We've got some problem with our service. Please try again later. |
UNAVAILABLE | 14 | We're temporarily offline for maintenance. Please try again later. |
DATA_LOSS | 15 | There's been an unrecoverable data loss or corruption. |
UNAUTHENTICATED | 16 | Your request does not have valid authentication credentials for the operation. |
When API errors occur, it is up to you to retry your request - BidZenith does not keep track of failed requests.
Versioning
The BidZenith API is versioned. A new API version is released when we introduce a backwards-incompatible change to the API. For example, changing a field type or name, or deprecating endpoints.
While we're adding functionality to our API, we won't release a new API version. You'll be able to take advantage of this non-breaking backwards-compatible changes directly on the API version you're currently using.
BidZenith considers the following changes to be backwards-compatible:
-
Adding new API resources.
-
Adding new optional request parameters to existing API methods.
-
Adding new properties to existing API responses.
-
Changing the order of properties in existing API responses.
-
Changing the length or format of opaque strings such as object IDs, error messages, and other human-readable strings.
Your integration should gracefully handle backwards-compatible changes. Changes at the proto level should be automatically taken care of by the Protocol Buffers tooling.