I kind of did. This was my first real attempt at an n8n integration, and it feels a bit convoluted to me.
A couple of caveats, one of them big:
It depends on a cron task to activate the webhook. The n8n execution logs show that it’s firing properly every night. But I just looked in InvoiceNinja, and it hadn’t logged anything since mid-April. So there’s a breakdown somewhere, I’m just not sure where.
I just ran an every-minute cron to fire the webhook, and it worked properly. So if I had to guess, it has something to do with when the cron is firing.
I did my best to configure it so that the cron fires at the end of the day and that it only grabs the time entries for the previous 24 hours. But I couldn’t quite wrap my head around the timezones. After some trial and error, I thought I had it working (the logs show it activating at 23:46). I’ll dive back in when I have some time.
Hopefully this is helpful to you. I’ve indicated places in the JSON where you’ll need to add some API auth or workspace-specific numbers. You can search for “ADD_YOUR” to find them all.
Let me know if you or anyone else finds a way to improve it.
{
"name": "Toggl Time -> Invoice Ninja Tasks",
"nodes": [
{
"parameters": {},
"name": "Start",
"type": "n8n-nodes-base.start",
"typeVersion": 1,
"position": [
150,
850
]
},
{
"parameters": {
"functionCode": "Date.prototype.toIsoString = function () {\n var tzo = -this.getTimezoneOffset(),\n dif = tzo >= 0 ? '+' : '-',\n pad = function (num) {\n var norm = Math.floor(Math.abs(num));\n return (norm < 10 ? '0' : '') + norm;\n };\n return this.getFullYear() +\n '-' + pad(this.getMonth() + 1) +\n '-' + pad(this.getDate()) +\n 'T' + pad(this.getHours()) +\n ':' + pad(this.getMinutes()) +\n ':' + pad(this.getSeconds()) +\n dif + pad(tzo / 60) +\n ':' + pad(tzo % 60);\n}\n\nfunction getMidnight(time) {\n time.setHours(0, 0, 0, 0);\n return time.toIsoString();\n}\n\nvar current_time_plain = new Date();\nvar current_time = current_time_plain.toIsoString();\n\nvar midnight_today = getMidnight(current_time_plain);\n\n// console.log('current: ' + current_time);\n// console.log('midnight: ' + midnight_today);\n\nitems[0].json.current_time = current_time;\nitems[0].json.midnight_today = midnight_today;\n\nreturn items;\n"
},
"name": "Today's Times",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
500,
150
]
},
{
"parameters": {
"functionCode": "return items[0].json.map(item => {\n return {\n json: item\n }\n});\n \n"
},
"name": "Split Time Entries",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
800,
150
]
},
{
"parameters": {
"resource": "task",
"additionalFields": {
"client": "={{$node[\"Merge Time Entries\"].json[\"client_id\"]}}",
"description": "={{$node[\"Merge Time Entries\"].json[\"description\"]}}",
"project": "={{$node[\"Merge Time Entries\"].json[\"in_project_id\"]}}"
},
"timeLogsUi": {
"timeLogsValues": [
{
"startDate": "={{$node[\"Merge Time Entries\"].json[\"start\"]}}",
"endDate": "={{$node[\"Merge Time Entries\"].json[\"stop\"]}}"
}
]
}
},
"name": "Invoice Ninja",
"type": "n8n-nodes-base.invoiceNinja",
"typeVersion": 1,
"position": [
1450,
550
],
"credentials": {
"invoiceNinjaApi": "Invoice Ninja API"
}
},
{
"parameters": {
"url": "https://app.invoiceninja.com/api/v1/projects",
"options": {},
"headerParametersUi": {
"parameter": [
{
"name": "X-Ninja-Token",
"value": "ADD_YOUR_API_TOKEN"
}
]
}
},
"name": "Get IN Projects",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 1,
"position": [
500,
950
]
},
{
"parameters": {
"url": "https://app.invoiceninja.com/api/v1/clients",
"options": {},
"headerParametersUi": {
"parameter": [
{
"name": "X-Ninja-Token",
"value": "ADD_YOUR_API_TOKEN"
}
]
}
},
"name": "Get IN Clients",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 1,
"position": [
500,
750
]
},
{
"parameters": {
"functionCode": "let new_arr = [];\nlet new_obj = {};\n\nfor(let i in items[0].json.data){\n value=items[0].json.data[i].id;\n new_obj[value] = items[0].json.data[i]\n}\nfor(let i in new_obj){\n new_arr.push(new_obj[i]);\n}\nreturn [{json:new_arr}];\n"
},
"name": "Get Clients Data",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
650,
750
]
},
{
"parameters": {
"functionCode": "let new_arr = [];\nlet new_obj = {};\n\nfor(let i in items[0].json.data){\n value=items[0].json.data[i].id;\n new_obj[value] = items[0].json.data[i]\n}\nfor(let i in new_obj){\n new_arr.push(new_obj[i]);\n}\nreturn [{json:new_arr}];\n"
},
"name": "Get Projects Data",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
650,
950
]
},
{
"parameters": {
"functionCode": "return items[0].json.map(item => {\n item = JSON.parse(JSON.stringify(item).split('\"id\":').join('\"in_project_id\":'));\n return {\n json: item\n }\n});\n \n"
},
"name": "Split IN Projects",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
800,
950
]
},
{
"parameters": {
"functionCode": "return items[0].json.map(item => {\n delete item['name'];\n item = JSON.parse(JSON.stringify(item).split('\"id\":').join('\"client_id\":'));\n return {\n json: item\n }\n});\n \n"
},
"name": "Split IN Clients",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
800,
750
]
},
{
"parameters": {
"mode": "mergeByKey",
"propertyName1": "client_id",
"propertyName2": "client_id",
"overwrite": "blank"
},
"name": "Merge InvoiceNinja",
"type": "n8n-nodes-base.merge",
"typeVersion": 1,
"position": [
1050,
750
]
},
{
"parameters": {
"mode": "mergeByKey",
"propertyName1": "pid",
"propertyName2": "id"
},
"name": "Merge Toggl",
"type": "n8n-nodes-base.merge",
"typeVersion": 1,
"position": [
1050,
250
]
},
{
"parameters": {
"mode": "mergeByKey",
"propertyName1": "name",
"propertyName2": "name"
},
"name": "Merge Time Entries",
"type": "n8n-nodes-base.merge",
"typeVersion": 1,
"position": [
1200,
550
]
},
{
"parameters": {
"authentication": "basicAuth",
"url": "https://api.track.toggl.com/api/v8/workspaces/ADD_YOUR_TOGGL_WORKSPACE_NUMBER/projects",
"options": {},
"queryParametersUi": {
"parameter": []
}
},
"name": "Get Toggl Projects",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 1,
"position": [
500,
350
],
"credentials": {
"httpBasicAuth": "Toggl API Basic Auth"
}
},
{
"parameters": {
"functionCode": "return items[0].json.map(item => {\n return {\n json: item\n }\n});\n \n"
},
"name": "Split Toggl Projects",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
650,
350
]
},
{
"parameters": {
"authentication": "basicAuth",
"url": "https://api.track.toggl.com/api/v8/time_entries",
"options": {},
"queryParametersUi": {
"parameter": [
{
"name": "start_date",
"value": "={{$node[\"Today's Times\"].json[\"midnight_today\"]}}"
},
{
"name": "end_date",
"value": "={{$node[\"Today's Times\"].json[\"current_time\"]}}"
}
]
}
},
"name": "Get Time Entries",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 1,
"position": [
650,
150
],
"credentials": {
"httpBasicAuth": "Toggl API Basic Auth"
}
},
{
"parameters": {
"httpMethod": "POST",
"path": "ADD_YOUR_N8N_WEBHOOK",
"options": {}
},
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
150,
550
],
"webhookId": "ADD_YOUR_N8N_WEBHOOK"
},
{
"parameters": {
"authentication": "basicAuth",
"url": "https://api.track.toggl.com/api/v8/workspaces/ADD_YOUR_TOGGL_WORKSPACE_NUMBER/clients",
"options": {},
"queryParametersUi": {
"parameter": []
}
},
"name": "Get Toggl Clients",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 1,
"position": [
500,
550
],
"credentials": {
"httpBasicAuth": "Toggl API Basic Auth"
}
},
{
"parameters": {
"functionCode": "return items[0].json.map(item => {\n item = JSON.parse(JSON.stringify(item).split('\"name\":').join('\"display_name\":'));\n return {\n json: item\n }\n});\n"
},
"name": "Split Toggl Clients",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
650,
550
]
},
{
"parameters": {
"mode": "mergeByKey",
"propertyName1": "cid",
"propertyName2": "id"
},
"name": "Merge Projects",
"type": "n8n-nodes-base.merge",
"typeVersion": 1,
"position": [
900,
350
]
},
{
"parameters": {
"mode": "mergeByKey",
"propertyName1": "display_name",
"propertyName2": "display_name"
},
"name": "Merge Clients",
"type": "n8n-nodes-base.merge",
"typeVersion": 1,
"position": [
900,
550
]
}
],
"connections": {
"Today's Times": {
"main": [
[
{
"node": "Get Time Entries",
"type": "main",
"index": 0
}
]
]
},
"Split Time Entries": {
"main": [
[
{
"node": "Merge Toggl",
"type": "main",
"index": 0
}
]
]
},
"Get IN Projects": {
"main": [
[
{
"node": "Get Projects Data",
"type": "main",
"index": 0
}
]
]
},
"Get IN Clients": {
"main": [
[
{
"node": "Get Clients Data",
"type": "main",
"index": 0
}
]
]
},
"Get Projects Data": {
"main": [
[
{
"node": "Split IN Projects",
"type": "main",
"index": 0
}
]
]
},
"Get Clients Data": {
"main": [
[
{
"node": "Split IN Clients",
"type": "main",
"index": 0
}
]
]
},
"Split IN Projects": {
"main": [
[
{
"node": "Merge InvoiceNinja",
"type": "main",
"index": 0
}
]
]
},
"Merge Toggl": {
"main": [
[
{
"node": "Merge Time Entries",
"type": "main",
"index": 0
}
]
]
},
"Merge InvoiceNinja": {
"main": [
[
{
"node": "Merge Time Entries",
"type": "main",
"index": 1
}
]
]
},
"Merge Time Entries": {
"main": [
[
{
"node": "Invoice Ninja",
"type": "main",
"index": 0
}
]
]
},
"Get Toggl Projects": {
"main": [
[
{
"node": "Split Toggl Projects",
"type": "main",
"index": 0
}
]
]
},
"Split Toggl Projects": {
"main": [
[
{
"node": "Merge Projects",
"type": "main",
"index": 0
}
]
]
},
"Get Time Entries": {
"main": [
[
{
"node": "Split Time Entries",
"type": "main",
"index": 0
}
]
]
},
"Get Toggl Clients": {
"main": [
[
{
"node": "Split Toggl Clients",
"type": "main",
"index": 0
}
]
]
},
"Merge Projects": {
"main": [
[
{
"node": "Merge Toggl",
"type": "main",
"index": 1
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "Get Toggl Clients",
"type": "main",
"index": 0
},
{
"node": "Get Toggl Projects",
"type": "main",
"index": 0
},
{
"node": "Get IN Projects",
"type": "main",
"index": 0
},
{
"node": "Get IN Clients",
"type": "main",
"index": 0
},
{
"node": "Today's Times",
"type": "main",
"index": 0
}
]
]
},
"Split Toggl Clients": {
"main": [
[
{
"node": "Merge Clients",
"type": "main",
"index": 1
}
]
]
},
"Split IN Clients": {
"main": [
[
{
"node": "Merge Clients",
"type": "main",
"index": 0
}
]
]
},
"Merge Clients": {
"main": [
[
{
"node": "Merge Projects",
"type": "main",
"index": 1
},
{
"node": "Merge InvoiceNinja",
"type": "main",
"index": 1
}
]
]
}
},
"active": false,
"settings": {}
}