Code Examples
InLoox provides an official example repository that helps you get started with API usage using C# and the Simple.OData.Client NuGet package.
inlooxgroup/inloox-api-examples-current — Clone or download the examples.
Introduction
The repository inloox-api-examples-current contains a ready-to-use .NET console application that demonstrates the most important API operations:
- Retrieve account information
- List projects
- Read time entries with paging and filtering
- Create new time entries
- Update project names
All examples use the Simple.OData.Client library to formulate OData requests in a type-safe and convenient way.
Repository Structure
inloox-api-examples-current/
├── InLoox.Api.Examples.sln # Visual Studio Solution
├── InLoox.Api.Examples/
│ ├── Program.cs # Main program with all examples
│ ├── InLoox.Api.Examples.csproj # Project file with NuGet references
│ └── ...
└── README.md
Prerequisites
- Visual Studio 2022 or later (or the .NET SDK for command-line usage)
- .NET 6.0 or later
- An InLoox account and a valid Personal Access Token
- The following NuGet packages (automatically restored during build):
| Package | Purpose |
|---|---|
Simple.OData.Client | Typed OData client for .NET |
InLoox.PM.Domain.Model.Public | InLoox entity models (ApiProject, ApiTimeEntry, etc.) |
Setup
1. Clone the repository
git clone https://github.com/inlooxgroup/inloox-api-examples-current.git
cd inloox-api-examples-current
2. Configure the API token
Open the Program.cs file and enter your Personal Access Token:
var token = "INSERT YOUR TOKEN";
Replace "INSERT YOUR TOKEN" with your actual token.
Your Personal Access Token grants full access to the API on your behalf. Treat it like a password — never commit it to version control and never share it publicly.
3. Run the project
dotnet run --project InLoox.Api.Examples
Or open the solution in Visual Studio and press F5.
Client Initialization
Every example starts with the same client configuration. The ODataClientSettings define the base URL and automatically add the API token to every request:
using InLoox.PM.Domain.Model.Aggregates.Api;
using Simple.OData.Client;
var EndPoint = new Uri("https://app.inloox.com");
var EndPointOdata = new Uri(EndPoint, "/api/odata/");
var token = "INSERT YOUR TOKEN";
var settings = new ODataClientSettings(EndPointOdata);
settings.BeforeRequest += delegate (HttpRequestMessage message)
{
message.Headers.Add("x-api-key", token);
};
var client = new ODataClient(settings);
If you are using InLoox Self-Hosted, change the endpoint as follows:
var EndPoint = new Uri("https://YOUR-SELF-HOSTED-URL");
var EndPointOdata = new Uri(EndPoint, "/api/v1/odata/");
Examples
Example 1: Retrieve account information
Retrieves the profile information of the authenticated user. This is a good "Hello World" call to verify that your token is working.
async Task<ApiAccountInfo> GetAccountInfo()
{
if (client == null) throw new InvalidOperationException("Initialize client first");
return await client.For<ApiAccountInfo>("AccountInfo").FindEntryAsync();
}
What happens here:
- Calls
GET /odata/AccountInfoto retrieve a singleApiAccountInfoobject FindEntryAsync()returns a single entity (not a collection)- The response contains the user's name, email address, and other account details
Usage:
var accountInfo = await GetAccountInfo();
Console.WriteLine($"Logged in as: {accountInfo.DisplayName}");
Example 2: List projects
Retrieves the first page of projects (up to 100) that the authenticated user has access to.
async Task<IEnumerable<ApiProject>> GetProjects()
{
if (client == null) throw new InvalidOperationException("Initialize client first");
return await client.For<ApiProject>("Project").FindEntriesAsync();
}
What happens here:
- Calls
GET /odata/Projectto retrieve a collection ofApiProjectentities FindEntriesAsync()returns the first page (default limit: 100 entries)- Each project contains properties such as
ProjectId,Name,StartDate,EndDate, and more
Usage:
var projects = await GetProjects();
foreach (var project in projects)
{
Console.WriteLine($"{project.Name} (ID: {project.ProjectId})");
}
This returns a maximum of 100 projects. To retrieve all projects, you need to implement paging — see the next example for the pattern.
Example 3: Time entries with paging and filtering
This is the most instructive example: it demonstrates both filtering (by date range) and automatic paging (to retrieve all matching records beyond the 100-entry limit).
async Task<List<ApiDynamicTimeEntry>> GetAllTimeEntriesForMonth(
DateTime month, Action<string> loadedFunc)
{
if (client == null) throw new InvalidOperationException("Initialize client first");
var filterStart = new DateTime(month.Year, month.Month, 1);
var filterEnd = new DateTime(month.Year, month.Month, 1).AddMonths(1);
var annotations = new ODataFeedAnnotations();
var timeentries = (await client
.For<ApiDynamicTimeEntry>("DynamicTimeEntry")
.Filter(k => k.TimeEntry_StartDateTime > filterStart
&& k.TimeEntry_EndDateTime < filterEnd)
.FindEntriesAsync(annotations)).ToList();
while (annotations.NextPageLink != null)
{
timeentries.AddRange(await client
.For<ApiDynamicTimeEntry>("DynamicTimeEntry")
.FindEntriesAsync(annotations.NextPageLink, annotations));
loadedFunc($"Loaded {timeentries.Count()} entries");
}
return timeentries;
}
What happens here:
- Creates a date range filter — calculates the first and last day of the specified month
- Queries
DynamicTimeEntry— uses the Dynamic endpoint variant, which includes custom fields (see Custom Fields below) - Uses
ODataFeedAnnotations— this object receives pagination metadata from the response, including theNextPageLink - Pages through all results — the
whileloop follows theNextPageLinkuntil all matching entries are loaded - Reports progress via the
loadedFunccallback
Usage:
var entries = await GetAllTimeEntriesForMonth(
DateTime.Now,
msg => Console.WriteLine(msg)
);
Console.WriteLine($"Total time entries this month: {entries.Count}");
This ODataFeedAnnotations + while (NextPageLink != null) pattern is the recommended way to retrieve all records from any entity. Use this pattern wherever you need complete datasets.
Example 4: Create a new time entry
Creates a new time entry for a specific project.
async Task CreateTimeEntry(Guid projectId, string name, DateTime start)
{
var newEntry = new ApiTimeEntry
{
ProjectId = projectId,
DisplayName = name,
StartDateTime = start,
EndDateTime = start.AddHours(1),
};
await client
.For<ApiTimeEntry>("TimeEntry")
.Set(newEntry)
.InsertEntryAsync();
Console.WriteLine($"Time entry '{name}' created.");
}
What happens here:
- A new
ApiTimeEntryobject is created with the required fields ProjectIdlinks the entry to an existing projectInsertEntryAsync()sends aPOSTrequest to theTimeEntryendpoint- Start and end time define the duration of the entry (here: 1 hour)
You can also use the untyped dictionary approach instead of typed objects. For example:
var values = new Dictionary<string, object>
{
{ "ProjectId", projectId },
{ "DisplayName", name },
{ "StartDateTime", start },
{ "EndDateTime", start.AddHours(2) }
};
var res = await client.InsertEntryAsync("TimeEntry", values);
Example 5: Update a project name
Updates the name of an existing project.
async Task UpdateProjectName(Guid projectId, string newName)
{
await client
.For<ApiProject>("Project")
.Key(projectId)
.Set(new { Name = newName })
.UpdateEntryAsync();
Console.WriteLine($"Project renamed to '{newName}'.");
}
What happens here:
Key(projectId)identifies the project to updateSet(new { Name = newName })defines the fields to change — here only the nameUpdateEntryAsync()sends aPATCHrequest that updates only the specified fields- You can specify any combination of fields in the
Set()call
Custom Fields
To access custom fields, use the Dynamic variants of the entities:
| Standard Entity | Dynamic Variant | Description |
|---|---|---|
Project | DynamicProject | Projects with custom fields |
Task | DynamicTaskItem | Tasks with custom fields |
TimeEntry | DynamicTimeEntry | Time entries with custom fields |
Budget | DynamicBudget | Budgets with custom fields |
LineItem | DynamicLineItem | Line items with custom fields |
Client | DynamicContact | Contacts with custom fields |
The Dynamic variants contain all standard fields plus additional properties for your custom fields. The custom field names correspond to the names configured in InLoox.
Reading custom fields
// Retrieve dynamic time entries with custom fields
var entries = await client
.For<ApiDynamicTimeEntry>("DynamicTimeEntry")
.FindEntriesAsync();
The NuGet package InLoox.PM.Domain.Model.Public provides base model classes such as ApiDynamicTimeEntry. If you have defined custom fields in InLoox, you can extend these classes to add strongly typed properties for your custom fields. Alternatively, you can use the untyped dictionary approach via InsertEntryAsync / UpdateEntryAsync.
NuGet Package
The examples use the official NuGet package InLoox.PM.Domain.Model.Public, which provides typed C# models for all API entities.
Included Models (Selection)
| Model | Description |
|---|---|
ApiProject | Project entity |
ApiDynamicProject | Project with custom fields |
ApiTimeEntry | Time entry entity |
ApiDynamicTimeEntry | Time entry with custom fields |
ApiAccountInfo | Account information |
ApiTaskItem | Task entity |
ApiDynamicTaskItem | Task with custom fields |
Installation
dotnet add package InLoox.PM.Domain.Model.Public
dotnet add package Simple.OData.Client
Benefits
Using the typed models with Simple.OData.Client provides:
- IntelliSense — automatic completion of property names in your IDE
- Compile-time checking — typos in property names are caught before runtime
- Typed filters — lambda expressions instead of string-based filters:
// Typed filter with IntelliSense support
var projects = await client
.For<ApiProject>("Project")
.Filter(p => p.IsClosed == false && p.StartDate > new DateTime(2024, 1, 1))
.OrderBy(p => p.Name)
.FindEntriesAsync();
Full Program.cs
For reference, here is the complete Program.cs from the example repository:
using InLoox.PM.Domain.Model.Aggregates.Api;
using Simple.OData.Client;
var EndPoint = new Uri("https://app.inloox.com");
var EndPointOdata = new Uri(EndPoint, "/api/odata/");
var token = "INSERT YOUR TOKEN";
var settings = new ODataClientSettings(EndPointOdata);
settings.BeforeRequest += delegate (HttpRequestMessage message)
{
message.Headers.Add("x-api-key", token);
};
var client = new ODataClient(settings);
var accountInfo = await GetAccountInfo();
var projects = await GetProjects();
await GetAllTimeEntriesForMonth(DateTime.Now, a => Console.WriteLine(a));
await CreateTimeEntry(projects.First().ProjectId, "Sample Time", DateTime.Now);
var project = projects.First();
await UpdateProjectName(project.ProjectId, project.Name + " updated");
async Task<ApiAccountInfo> GetAccountInfo()
{
if (client == null) throw new InvalidOperationException("Initialize client first");
return await client.For<ApiAccountInfo>("AccountInfo").FindEntryAsync();
}
async Task<IEnumerable<ApiProject>> GetProjects()
{
if (client == null) throw new InvalidOperationException("Initialize client first");
return await client.For<ApiProject>("Project").FindEntriesAsync();
}
async Task<List<ApiDynamicTimeEntry>> GetAllTimeEntriesForMonth(
DateTime month, Action<string> loadedFunc)
{
if (client == null) throw new InvalidOperationException("Initialize client first");
var filterStart = new DateTime(month.Year, month.Month, 1);
var filterEnd = new DateTime(month.Year, month.Month, 1).AddMonths(1);
var annotations = new ODataFeedAnnotations();
var timeentries = (await client
.For<ApiDynamicTimeEntry>("DynamicTimeEntry")
.Filter(k => k.TimeEntry_StartDateTime > filterStart
&& k.TimeEntry_EndDateTime < filterEnd)
.FindEntriesAsync(annotations)).ToList();
while (annotations.NextPageLink != null)
{
timeentries.AddRange(await client
.For<ApiDynamicTimeEntry>("DynamicTimeEntry")
.FindEntriesAsync(annotations.NextPageLink, annotations));
loadedFunc($"Loaded {timeentries.Count()} entries");
}
return timeentries;
}
async Task CreateTimeEntry(Guid projectId, string name, DateTime start)
{
if (client == null) throw new InvalidOperationException("Initialize client first");
var values = new Dictionary<string, object>
{
{ "ProjectId", projectId },
{ "DisplayName", name },
{ "StartDateTime", start },
{ "EndDateTime", start.AddHours(2) }
};
var res = await client.InsertEntryAsync("TimeEntry", values);
}
async Task UpdateProjectName(Guid projectId, string newName)
{
if (client == null) throw new InvalidOperationException("Initialize client first");
var project = new ApiProject() { Name = newName };
await client.For<ApiProject>().Key(projectId)
.Set(new { project.Name }).UpdateEntryAsync();
}
Next Steps
- Getting Started — Authentication and OData query basics
- Projects — Detailed
Projectendpoint reference - Tasks — Detailed
Taskendpoint reference - Time Entries — Detailed
TimeEntryendpoint reference
If you encounter issues with the examples, contact InLoox support.