Preparation
Create a Helio account on https://helio.exchange
Request your personal API Key via the support chat or https://helio.exchange/help/contact
Install Rclone on your operating system https://rclone.org/downloads
Environment Variables
Create a .env
file in the root of your project and add the following values. Make sure you complete all the missing values and update the placeholder values accordingly.
HELIO_ID_URL=https://id.helio.exchange/auth/realms/helio/protocol/openid-connect/token
HELIO_ID_YOUR_EMAIL=
HELIO_ID_YOUR_PASSWORD=
HELIO_ID_CLIENT_ID=
HELIO_ID_CLIENT_SECRET=
HELIO_RENDER_API_URL=https://render-api.helio.exchange/v1RCLONE_PATH=/path/to/rclone
PROJECT_PATH=/path/to/project/root
RENDER_RESULT_PATH=/path/to/download/results
PROJECT_FILE=file_name
Environment Requirements
You'll need to have Node.js installed with a version >= 20
. To make sure you have the right version and that everything works as expected, run the following code which should output the currently installed version.
❯ node --version
v20.10.0
Write Your Script
Create a helio.mjs
file in the root of your project. Fill it up with the following code sections. To load the .env
file and run the script, execute the following code.
node --env-file .env helio.mjs
Root Imports
These imports and the async exec are required in the rest of the script. Add this to the top of the file.
import util from 'util'
import child_process from 'child_process'
import fs from 'fs'
const exec = util.promisify(child_process.exec)
Get Access Token
Call the Helio ID to log in and get the access token.
let accessToken
try {
const response = await fetch(process.env.HELIO_ID_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'password',
scope: 'openid',
username: process.env.HELIO_ID_YOUR_EMAIL,
password: process.env.HELIO_ID_YOUR_PASSWORD,
client_id: process.env.HELIO_ID_CLIENT_ID,
client_secret: process.env.HELIO_ID_CLIENT_SECRET,
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(`HTTP error! status: ${response.status}: ${error.message}`)
}
const data = await response.json()
accessToken = data['access_token']
console.info('Success: access token: ', accessToken)
} catch (error) {
console.error('Error: access token:', error)
}
Get Your User
Get your authenticated user ID to be able to fetch your teams.
try {
const response = await fetch(process.env.HELIO_RENDER_API_URL + `/user`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(`HTTP error! status: ${response.status}: ${error.message}`)
}
const data = await response.json()
userId = data['id']
console.info('Success: user id:', userId)
} catch (error) {
console.error('Error: user id:', error)
}
Select your Team
If you have multiple teams, make sure you choose the right team by index. In this example, we just select the first team.
let teamId
try {
const response = await fetch(process.env.HELIO_RENDER_API_URL + `/users/${userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(`HTTP error! status: ${response.status}: ${error.message}`)
}
const data = await response.json()
teamId = data.teams[0]['id']
console.info('Success: team id:', teamId)
} catch (error) {
console.error('Error: team id:', error)
}
Create Project
Create your project within the selected team. The only required field is the name in the body.
let project
try {
const response = await fetch(process.env.HELIO_RENDER_API_URL + `/workspaces`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
'Team-Id': teamId,
},
body: JSON.stringify({
name: 'My Project',
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(`HTTP error! status: ${response.status}: ${error.message}`)
}
project = await response.json()
console.info('Success: new project:', project)
} catch (error) {
console.error('Error: new project:', error)
}
Upload Project files
First, we load the environment variables that are required to run Rclone.
let projectUploadRcloneEnvVars
try {
const response = await fetch(
process.env.HELIO_RENDER_API_URL + `/storage/${project.storageId}/credential`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
accessMode: 'ReadWrite',
type: 'rclone',
}),
}
)
if (!response.ok) {
const error = await response.json()
throw new Error(`HTTP error! status: ${response.status}: ${error.message}`)
}
const data = await response.json()
projectUploadRcloneEnvVars = data.config
console.info('Success: project rclone storage config: ', projectUploadRcloneEnvVars)
} catch (error) {
console.error('Error: project rclone storage config:', error)
}
Then we run Rclone to upload the files.
try {
const command = `${process.env.RCLONE_PATH} sync ${process.env.PROJECT_PATH} ${projectUploadRcloneEnvVars['RCLONE_CONFIG_ENDPOINT']}`
await exec(command, { env: projectUploadRcloneEnvVars, stdio: 'inherit' })
console.info('Success: uploaded')
} catch (error) {
console.error('Error: uploading:', error)
}
Start a Rendering
Each render engine has it's own required fields which you can find a detailed description inside our Render API reference in the POST /renders
section.
let render
try {
const response = await fetch(process.env.HELIO_RENDER_API_URL + `/renders`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
'Team-Id': teamId,
},
body: JSON.stringify({
projectName: project.name,
file: process.env.PROJECT_FILE,
workspaceId: project.id,
engine: 'blender_4_0_cycles',
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(`HTTP error! status: ${response.status}: ${error.message}`)
}
render = await response.json()
console.info('Success: start rendering', render)
} catch (error) {
console.error(error.message)
console.error(error.data)
console.error('Error: start rendering:', error)
}
If you want to do a mass submit of like 10+ jobs with one API call, you use this code:
let renders
try {
const response = await fetch(process.env.HELIO_RENDER_API_URL + `/renders`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
'Team-Id': teamId,
},
body: JSON.stringify([
{
projectName: project.name,
file: process.env.PROJECT_FILE,
workspaceId: project.id,
engine: 'blender_4_0_cycles',
},
{
projectName: project.name,
file: process.env.PROJECT_FILE,
workspaceId: project.id,
engine: 'blender_4_0_cycles',
},
]),
})
if (!response.ok) {
const error = await response.json()
throw new Error(`HTTP error! status: ${response.status}: ${error.message}`)
}
renders = await response.json()
console.info('Success: start rendering', render)
} catch (error) {
console.error(error.message)
console.error(error.data)
console.error('Error: start rendering:', error)
}
Be aware that if you use mass submit, you're using a new variable called renders
, which is an array of render objects. In the following sections, we still depend on a variable called render
which is a single render object.
Render Status Check
To know when a rendering is done, you'll have to poll the render detail endpoint every few minutes.
try {
const response = await fetch(process.env.HELIO_RENDER_API_URL + `/renders/${render.name}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
'Team-Id': teamId,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(`HTTP error! status: ${response.status}: ${error.message}`)
}
render = await response.json()
console.info('Success: status rendering:', render)
} catch (error) {
console.error('Error: status rendering:', error)
}
Download Render Results
A rendering has multiple storages, an input and an output. to be able to download the render result files, we would need to get the output storage ID first.
let renderOutputStorageId
try {
const response = await fetch(
process.env.HELIO_RENDER_API_URL + `/renders/${render.name}/storage`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
'Team-Id': teamId,
},
}
)
if (!response.ok) {
const error = await response.json()
throw new Error(`HTTP error! status: ${response.status}: ${error.message}`)
}
const data = await response.json()
renderOutputStorageId = data['outputStorageId']
console.info('Success: rendering storages:', renderOutputStorageId)
} catch (error) {
console.error('Error: rendering storages:', error)
}
Then we can load the environment variables that are required to run Rclone.
et renderDownloadRcloneEnvVars
try {
const response = await fetch(
process.env.HELIO_RENDER_API_URL + `/storage/${renderOutputStorageId}/credential`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
accessMode: 'ReadWrite',
type: 'rclone',
}),
}
)
if (!response.ok) {
const error = await response.json()
throw new Error(`HTTP error! status: ${response.status}: ${error.message}`)
}
const data = await response.json()
renderDownloadRcloneEnvVars = data.config
console.info('Success: results storage config: ', renderDownloadRcloneEnvVars)
} catch (error) {
console.error('Error: results storage config:', error)
}
Then we run Rclone to download the render result files.
try {
await fs.promises.mkdir(process.env.RENDER_RESULT_PATH, { recursive: true })
const command = `${process.env.RCLONE_PATH} copy ${renderDownloadRcloneEnvVars['RCLONE_CONFIG_ENDPOINT']} ${process.env.RENDER_RESULT_PATH}`
await exec(command, { env: renderDownloadRcloneEnvVars, stdio: 'inherit' })
console.info('Success: download completed.')
} catch (error) {
console.error('Error: download:', error)
}