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.
👉 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):
| 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 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
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);
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/AccountInfoto retrieve a singleApiAccountInfoobject 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/Projectto fetch a collection ofApiProjectentities 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})");
}
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:
- Builds a date range filter — calculates the first and last day of the given month
- Queries
DynamicTimeEntry— uses the Dynamic endpoint variant which includes custom fields (see Custom Fields below) - Uses
ODataFeedAnnotations— this object receives paging metadata from the response, including theNextPageLink - Pages through all results — the
whileloop followsNextPageLinkuntil 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 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/TimeEntryrequest with a JSON body - Uses
InsertEntryAsyncwith an untyped dictionary — useful when you only need to set a few properties - The
ProjectIdlinks the time entry to an existing project StartDateTimeandEndDateTimedefine the 2-hour time span
Usage:
var projects = await GetProjects();
await CreateTimeEntry(
projects.First().ProjectId,
"API Integration Work",
DateTime.Now
);
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 untouchedUpdateEntryAsync()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 Endpoint | Dynamic Endpoint | Purpose |
|---|---|---|
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 |
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();
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 classes —
ApiProject,ApiTimeEntry,ApiTask,ApiAccountInfo,ApiBudget, and more - Dynamic entity classes —
ApiDynamicProject,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
Projectendpoint reference - Tasks — Detailed
Taskendpoint reference - Time Entries — Detailed
TimeEntryendpoint reference
If you run into issues with the examples, check the GitHub Issues page or contact InLoox support.