Skip to main content

Code Examples

InLoox provides a ready-to-run C# sample project that demonstrates the most common API operations. Clone the repository, plug in your token, and start experimenting.

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 queries in a type-safe and convenient way.


Repository Structure

The sample project is a .NET console application that uses the Simple.OData.Client library together with the official InLoox.PM.Domain.Model.Public NuGet package to interact with the InLoox API.

inloox-api-examples-current/
├── InLooxApiExamples.csproj # Project file with NuGet references
├── Program.cs # All examples in a single file
└── README.md

Prerequisites

  • Visual Studio 2022 or later (or the .NET SDK for command-line usage)
  • .NET 6.0 or later
  • An InLoox account with a valid Personal Access Token
  • The following NuGet packages (restored automatically on 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 Your Token

Open Program.cs and replace the placeholder token with your Personal Access Token:

var token = "INSERT YOUR TOKEN";  // ← Replace with your actual token
warning

Never commit your real token to a public repository. For production use, store it in environment variables or a secrets manager.

3. Build and Run

dotnet restore
dotnet run

The application will execute all examples sequentially and print results to the console.


Client Initialization

Every example starts with the same client setup. The ODataClientSettings configure the base URL and attach 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);
Self-Hosted Users

If you use InLoox Self-Hosted, change the endpoint to:

var EndPoint = new Uri("https://YOUR-SELF-HOSTED-URL");
var EndPointOdata = new Uri(EndPoint, "/api/v1/odata/");

Examples

1. GetAccountInfo — Retrieve Current User

Fetches the profile information for the authenticated user. This is a good "hello world" call to verify your token works.

async Task<ApiAccountInfo> GetAccountInfo()
{
if (client == null) throw new InvalidOperationException("Initialize client first");
return await client.For<ApiAccountInfo>("AccountInfo").FindEntryAsync();
}

What it does:

  • Calls GET /odata/AccountInfo to retrieve a single ApiAccountInfo object
  • FindEntryAsync() returns a single entity (not a collection)
  • The response includes the user's name, email, and account details

Usage:

var accountInfo = await GetAccountInfo();
Console.WriteLine($"Logged in as: {accountInfo.DisplayName}");

2. GetProjects — 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 it does:

  • Calls GET /odata/Project to fetch a collection of ApiProject entities
  • FindEntriesAsync() returns the first page (default limit: 100 items)
  • Each project includes properties like 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 at most 100 projects. To retrieve all projects, you need to implement paging — see the next example for the pattern.


3. GetAllTimeEntriesForMonth — Paging & 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-item 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 it does:

  1. Builds a date range filter — calculates the first and last day of the given month
  2. Queries DynamicTimeEntry — uses the Dynamic endpoint variant which includes custom fields (see Custom Fields below)
  3. Uses ODataFeedAnnotations — this object receives paging metadata from the response, including the NextPageLink
  4. Pages through all results — the while loop follows 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 fetch all records from any entity. Reuse this pattern wherever you need complete datasets.


4. CreateTimeEntry — Create a New Entity

Demonstrates how to create a new entity using a dictionary of property values.

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);
}

What it does:

  • Sends a POST /odata/TimeEntry request with a JSON body
  • Uses InsertEntryAsync with an untyped dictionary — useful when you only need to set a few properties
  • The ProjectId links the time entry to an existing project
  • StartDateTime and EndDateTime define the 2-hour time span

Usage:

var projects = await GetProjects();
await CreateTimeEntry(
projects.First().ProjectId,
"API Integration Work",
DateTime.Now
);
info

You can also use strongly-typed objects instead of dictionaries. For example:

var entry = new ApiTimeEntry
{
ProjectId = projectId,
DisplayName = name,
StartDateTime = start,
EndDateTime = start.AddHours(2)
};
await client.For<ApiTimeEntry>("TimeEntry").Set(entry).InsertEntryAsync();

5. UpdateProjectName — Update an Existing Entity

Shows how to perform a partial update (PATCH) on an existing entity, changing only specific properties.

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();
}

What it does:

  • Sends a PATCH /odata/Project({projectId}) request
  • .Key(projectId) identifies the specific project to update
  • .Set(new { project.Name }) specifies only the properties to change — this creates a partial update, leaving all other project properties untouched
  • UpdateEntryAsync() executes the PATCH request

Usage:

var project = projects.First();
await UpdateProjectName(project.ProjectId, project.Name + " (Updated)");

Custom Fields

InLoox supports custom fields on projects, tasks, time entries, and other entities. To access custom fields via the API, use the Dynamic endpoint variants:

Standard EndpointDynamic EndpointPurpose
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

Dynamic endpoints return all standard properties plus custom field values as additional columns. Custom field property names are prefixed with the entity name (e.g., TimeEntry_StartDateTime for the DynamicTimeEntry entity).

Reading Custom Fields

// Fetch dynamic time entries that include custom fields
var entries = await client
.For<ApiDynamicTimeEntry>("DynamicTimeEntry")
.FindEntriesAsync();
Extending the Models

The InLoox.PM.Domain.Model.Public NuGet package provides base model classes like ApiDynamicTimeEntry. If you've defined custom fields in InLoox, you can extend these classes to add strongly-typed properties for your custom fields. Alternatively, use the untyped dictionary approach via InsertEntryAsync / UpdateEntryAsync.


NuGet Package: InLoox.PM.Domain.Model.Public

The InLoox.PM.Domain.Model.Public NuGet package provides the official C# entity models for the InLoox API. It includes:

  • Typed entity classesApiProject, ApiTimeEntry, ApiTask, ApiAccountInfo, ApiBudget, and more
  • Dynamic entity classesApiDynamicProject, ApiDynamicTimeEntry, etc. for custom field support
  • Enum types — status values, permission flags, and other constants

Installation

dotnet add package InLoox.PM.Domain.Model.Public
dotnet add package Simple.OData.Client

Benefits

Using the typed models with Simple.OData.Client gives you:

  • IntelliSense — auto-complete property names in your IDE
  • Compile-time checking — catch property name typos before runtime
  • Strongly-typed filters — use lambda expressions instead of string-based filters:
// Strongly-typed filter with IntelliSense
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 sample 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 setup and OData query basics
  • Projects — Detailed Project endpoint reference
  • Tasks — Detailed Task endpoint reference
  • Time Entries — Detailed TimeEntry endpoint reference
Need Help?

If you run into issues with the examples, check the GitHub Issues page or contact InLoox support.