Skip to main content

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.

GitHub Repository

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):
PackagePurpose
Simple.OData.ClientTyped OData client for .NET
InLoox.PM.Domain.Model.PublicInLoox 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.

Important

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);
InLoox Self-Hosted URL

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/AccountInfo to retrieve a single ApiAccountInfo object
  • 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/Project to retrieve a collection of ApiProject entities
  • 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})");
}
note

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:

  1. Creates a date range filter — calculates the first and last day of the specified month
  2. Queries DynamicTimeEntry — uses the Dynamic endpoint variant, which includes custom fields (see Custom Fields below)
  3. Uses ODataFeedAnnotations — this object receives pagination metadata from the response, including the NextPageLink
  4. Pages through all results — the while loop follows the NextPageLink until all matching entries are loaded
  5. Reports progress via the loadedFunc callback

Usage:

var entries = await GetAllTimeEntriesForMonth(
DateTime.Now,
msg => Console.WriteLine(msg)
);
Console.WriteLine($"Total time entries this month: {entries.Count}");
Paging Pattern

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 ApiTimeEntry object is created with the required fields
  • ProjectId links the entry to an existing project
  • InsertEntryAsync() sends a POST request to the TimeEntry endpoint
  • Start and end time define the duration of the entry (here: 1 hour)
info

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 update
  • Set(new { Name = newName }) defines the fields to change — here only the name
  • UpdateEntryAsync() sends a PATCH request 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 EntityDynamic VariantDescription
ProjectDynamicProjectProjects with custom fields
TaskDynamicTaskItemTasks with custom fields
TimeEntryDynamicTimeEntryTime entries with custom fields
BudgetDynamicBudgetBudgets with custom fields
LineItemDynamicLineItemLine items with custom fields
ClientDynamicContactContacts 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();
Extending Models

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)

ModelDescription
ApiProjectProject entity
ApiDynamicProjectProject with custom fields
ApiTimeEntryTime entry entity
ApiDynamicTimeEntryTime entry with custom fields
ApiAccountInfoAccount information
ApiTaskItemTask entity
ApiDynamicTaskItemTask 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 Project endpoint reference
  • Tasks — Detailed Task endpoint reference
  • Time Entries — Detailed TimeEntry endpoint reference
Need Help?

If you encounter issues with the examples, contact InLoox support.