Introduction
The CupixWorks API Skill lets you work with the CupixWorks REST API in plain language from Claude Code. Claude calls the API directly — there is no separate server to run — to read and create data, and to upload large local files such as 360° videos and panoramas. You do not need to know the API. Claude works out which request to make, signs in for you, pages through long lists, and answers in plain language rather than raw data.
It reaches essentially all of CupixWorks — 383 separate API operations — and runs on macOS, Linux, and Windows.
Note:
- The skill runs on your own machine. Tasks that upload files must run in an environment that can read your local files.
- You need a CupixWorks account and an API token before you start.
How It Works
The whole workflow, start to finish:
| Step | What you do | Where |
|---|---|---|
| 1. Download | Get cupixworks-api.zip from the bottom of this article and extract it. | Your computer |
| 2. Install | Copy the cupixworks-api folder into your Claude skills folder: ~/.claude/skills/ on macOS and Linux, %USERPROFILE%\.claude\skills\ on Windows. Step 2 of the install section does this for you. | One command, once |
| 3. Check | Start Claude Code and run /skills. cupixworks-api should be in the list. | In Claude |
| 4. First request | Ask for something in plain language. Claude asks for your host and API token — paste them in. | In Claude |
| 5. From then on | Keep asking in plain language. You do not run commands again. | In Claude |
That is the whole setup. The command-line examples later in this article are an alternative for anyone who wants to script against the API directly — they are not part of normal use.
What happens behind the scenes each time you ask:
- You ask Claude to do something with CupixWorks.
- Claude checks your host and api_token, and asks for them if they are not configured.
- A small helper script inside the download signs in and keeps the session alive. It is called cupix.sh on macOS and Linux and cupix.ps1 on Windows. You never run it yourself — Claude does.
- Claude calls the appropriate endpoint and formats the response into an answer.
Both helper scripts take the same commands and arguments, print the same output, and return the same exit codes, so every example in this article is given in both forms.
Prerequisites
- Claude Code (Anthropic's command-line app) or Claude Cowork, already installed and signed in.
- Your CupixWorks host and API token, prepared as described below. Claude asks for both the first time you make a request.
The rest depends on your operating system.
| Platform | Shell | Also Required | Helper Script |
|---|---|---|---|
| macOS, Linux | bash | curl and python3 — included by default | scripts/cupix.sh |
| Windows 10, Windows 11 | Windows PowerShell 5.1 (built in) or PowerShell 7+ | None — curl.exe is built into Windows 10 1803 and later | scripts/cupix.ps1 |
Note:
- If you have not used a terminal before. On Windows, press the Start button, type PowerShell, and open Windows PowerShell. On macOS, open Terminal from Applications — Utilities. A window appears where you type commands and press Enter after each one.
- Paste one line at a time. Pasting a whole block at once can misbehave — run each line, wait for it to finish, then run the next.
- Commands in this article are shown twice: once for a macOS or Linux shell, and once for Windows PowerShell. Run the pair that matches your machine. Do not run cupix.sh on Windows — it is a bash script, and cupix.ps1 is its Windows equivalent.
Important: The Windows wrapper (cupix.ps1) is new and has not yet been confirmed against a live CupixWorks host. Run scripts/Test-CupixPS.ps1 first — it checks the wrapper offline in about a minute — and please report anything that fails before relying on the Windows procedure with a customer.
Find Your Host
Your host is the address you use to open CupixWorks in a browser. It contains both your team domain and the environment.
| Item | Value |
|---|---|
| Host format | {team-domain}.cupix.works |
| Example | acme.cupix.works |
| API base URL | https://{host}/api/v1 |
| Team domain | The first label of the host — in acme.cupix.works, the team domain is acme |
The skill separates the team domain from the host for you, so you only need the host. If your team is on a different environment, the address in your browser is still the value to use.
Issue an API Token
- Sign in to CupixWorks.
- Open the profile menu in the top-right corner and go to Account Settings.
- Select Personal in the left sidebar.
- Scroll to Your Personal API Token.
- Copy the token with the copy button beside it, or select Generate New Token to issue one.
The API token is a property of your user account, so requests made with it carry your own permissions. Generate New Token replaces the token you had, so any script still using the old value stops working. Authenticating with the token returns a short-lived access token, valid for about one hour, which the helper script caches and reissues for you. A refresh token, valid for about seven days, is returned at the same time.
Important: The token is shown in full on this page. Take care when sharing a screen or a screenshot of it, and redact the value before putting it in a ticket.
Important: Treat your API token like a password. Do not put it in documents, chat messages, or commits. The skill is designed not to print the token in its output.
Save Your Credentials
Claude asks for your host and token the first time you make a request. Saving them once avoids re-entering them. There are two ways to do it, and you need only one.
Note:
- Choose either Option 1 or Option 2 below, not both. If both are present, the environment variables win and the configuration file is ignored.
- Replace PASTE_YOUR_TOKEN_HERE with the token itself and nothing else — no angle brackets, and no quotes of your own.
- Throughout this article, values you must replace with your own are shown in bold red inside the command blocks. Only the highlighted part changes — in acme.cupix.works, for example, .cupix.works stays exactly as written. Copying a command without changing the highlighted values is the most common setup mistake.
- acme.cupix.works is an example, not a real host. Use the one you found under Find Your Host.
Option 1 — configuration file. Recommended. It persists across restarts and applies in every terminal window. The file lives at ~/.cupixworks/config.json on macOS and Linux, and at %USERPROFILE%\.cupixworks\config.json on Windows.
macOS · Linux (bash)
mkdir -p ~/.cupixworks
cat > ~/.cupixworks/config.json << 'EOF'
{"host": "acme.cupix.works", "api_token": "PASTE_YOUR_TOKEN_HERE"}
EOF
# Restrict the file to your account
chmod 600 ~/.cupixworks/config.json
Windows (PowerShell)
$cfgDir = "$env:USERPROFILE\.cupixworks"
$cfg = "$cfgDir\config.json"
$json = '{"host": "acme.cupix.works", "api_token": "PASTE_YOUR_TOKEN_HERE"}'
New-Item -ItemType Directory -Force -Path $cfgDir | Out-Null
Set-Content -Path $cfg -Encoding UTF8 -Value $json
# Restrict the file to your account (the Windows equivalent of chmod 600)
icacls $cfg /inheritance:r /grant:r "$($env:USERNAME):(R,W)"
Option 2 — environment variables. Quicker, but the first form below lasts only until you close the terminal. Use the second form to keep the values for future sessions — note that it takes effect in new terminal windows only, so the window you type it in will still see nothing until you open a fresh one.
macOS · Linux (bash)
# This shell session only export CUPIX_HOST="acme.cupix.works" export CUPIX_API_TOKEN="PASTE_YOUR_TOKEN_HERE" # To keep them, add the same two lines to ~/.bashrc or ~/.zshrc
Windows (PowerShell)
# This session only $env:CUPIX_HOST = "acme.cupix.works" $env:CUPIX_API_TOKEN = "PASTE_YOUR_TOKEN_HERE" # To keep them for future sessions, set them at user scope instead [Environment]::SetEnvironmentVariable('CUPIX_HOST', 'acme.cupix.works', 'User') [Environment]::SetEnvironmentVariable('CUPIX_API_TOKEN', 'PASTE_YOUR_TOKEN_HERE', 'User')
Verify Your Credentials
Check what you saved before moving on. Run the lines for the option you chose. This step confirms the values are stored and readable on this machine. It does not contact CupixWorks, and it does not install anything — the helper script does not exist yet. Authentication itself is verified later, under Verify the Install.
macOS · Linux (bash)
# --- if you chose Option 1 (configuration file) ---
cat ~/.cupixworks/config.json
# --- if you chose Option 2 (environment variables) ---
echo "$CUPIX_HOST"
echo "${#CUPIX_API_TOKEN} characters" # length only, never the token itself
case "$CUPIX_API_TOKEN" in *[<>]*) echo "REMOVE THE ANGLE BRACKETS";; esac
Windows (PowerShell)
# --- if you chose Option 1 (configuration file) ---
Get-Content "$env:USERPROFILE\.cupixworks\config.json"
# --- if you chose Option 2 (environment variables) ---
# The user-scope form only reaches NEW windows - open a fresh one first.
$env:CUPIX_HOST
"$(($env:CUPIX_API_TOKEN).Length) characters" # length only, never the token
if ($env:CUPIX_API_TOKEN -match '[<>]') { 'REMOVE THE ANGLE BRACKETS' }
The host should print exactly as you entered it. The token check prints only a character count, so your token never lands in the terminal scrollback — if the count is longer than the token you were issued, or the bracket warning appears, the value still has the placeholder punctuation around it.
Install the Skill
A skill is a folder of instructions that Claude Code reads. This one is named cupixworks-api, and it contains the skill definition, the two helper scripts, and the API reference files listed at the end of this section.
Download cupixworks-api.zip, attached to this article, and extract it. Then install it with one of the two methods below.
Note: Whichever method you use, the folder you install is the one with SKILL.md directly inside it.
- On Windows, Extract All creates a folder named after the zip, so you end up with cupixworks-api\cupixworks-api. The inner folder is the skill.
- On macOS and Linux, extracting produces a single cupixworks-api folder.
Install for All Projects
This is the recommended method. Copy the skill folder into your personal Claude skills directory, and Claude recognizes the skill from any directory. Both methods below produce the same result — use Method 1 if you would rather not type commands.
Method 1 — copy the folder by hand
Windows
- Press Win+E to open File Explorer.
- Click the address bar, paste %USERPROFILE%\.claude, and press Enter.
- If there is no skills folder, right-click and choose New → Folder, name it skills, and open it.
- Open a second File Explorer window at your Downloads folder and find the cupixworks-api folder that has SKILL.md directly inside it.
- Drag that folder into the skills window.
macOS
- In Finder, press Shift+Command+G, type ~/.claude, and press Enter.
- If there is no skills folder, choose File → New Folder, name it skills, and open it.
- Find the extracted cupixworks-api folder in Downloads and drag it in.
Method 2 — copy the folder with a command
macOS · Linux (bash)
SRC=~/Downloads/cupixworks-api # the folder holding SKILL.md mkdir -p ~/.claude/skills cp -R "$SRC" ~/.claude/skills/ # Make the helper script executable chmod +x ~/.claude/skills/cupixworks-api/scripts/cupix.sh
Windows (PowerShell)
$src = "$env:USERPROFILE\Downloads\cupixworks-api\cupixworks-api" # the folder holding SKILL.md New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.claude\skills" | Out-Null Copy-Item -Recurse -Force $src "$env:USERPROFILE\.claude\skills\"
Required on Windows, after either method
Run these in PowerShell even if you copied the folder by hand.
# Allow local scripts to run for your account (one time, if not already set) Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned # Clear the "downloaded from the internet" flag, which otherwise blocks the script Get-ChildItem -Recurse "$env:USERPROFILE\.claude\skills\cupixworks-api" | Unblock-File
Note: On Windows, the two extra commands matter. Without Unblock-File, PowerShell refuses to run a script that was saved from a browser or extracted from a zip, and reports that the file is not digitally signed.
Check the copy worked
macOS · Linux (bash)
ls ~/.claude/skills/cupixworks-api/SKILL.md
Expected: the path prints back. No such file means the copy did not land.
Windows (PowerShell)
Test-Path "$env:USERPROFILE\.claude\skills\cupixworks-api\SKILL.md"
Expected: True.
If you copied the folder by hand and would rather not open a terminal, open the skills folder instead. You should see cupixworks-api with SKILL.md and a scripts folder directly inside it — not another cupixworks-api folder.
If the copy failed
| What you see | What it means | What to do |
|---|---|---|
| Cannot find path 'C:\Users\<you>\cupixworks-api' | The command looked for the folder in whatever directory the terminal was pointing at, and it is not there. | Use Method 1, or run the Method 2 commands, which set the source path explicitly. |
| The check returns False, or the file is not found | The folder reached the skills directory but the level is wrong, or it landed somewhere else. | Open the skills folder and confirm SKILL.md sits directly inside cupixworks-api. |
| skills\cupixworks-api\cupixworks-api\SKILL.md | The outer folder created by Extract All was copied instead of the skill folder. | Move the inner cupixworks-api folder up one level and delete the empty outer folder. |
Use the Skill in One Project
To limit the skill to a single project instead, put the cupixworks-api folder in that project's .claude/skills/ directory and run claude from the project root.
Verify the Install
Start Claude Code, then check that the skill is registered and make a first request. The lines beginning with > are typed inside Claude Code, not in the shell.
claude > /skills # cupixworks-api appears in the list > Show me 5 facilities from CupixWorks
On first use, Claude asks for your host and API token. To test the helper script on its own, without Claude, use the credentials you already saved under Save Your Credentials — do not set them again here, or you will overwrite what is working.
macOS · Linux (bash)
S=~/.claude/skills/cupixworks-api/scripts/cupix.sh "$S" auth # authenticated OK "$S" whoami # your name and email
Windows (PowerShell)
$S = "$env:USERPROFILE\.claude\skills\cupixworks-api\scripts\cupix.ps1" & $S auth # authenticated OK & $S whoami # your name and email
A successful run looks like this. Your host, name and cache path will differ; the shape of the output is what matters. The home folder is written here as ~ and %USERPROFILE% — the real output prints it in full.
macOS · Linux (bash)
# auth authenticated OK (token cached at ~/.cupixworks/token_acme.cupix.works.json) # whoami Alex Park <alex.park@acme.com> team_domain=acme
Windows (PowerShell)
# auth authenticated OK (token cached at %USERPROFILE%\.cupixworks\token_acme.cupix.works.json) # whoami Alex Park <alex.park@acme.com> team_domain=acme
Two things to look at rather than skim past. The team_domain should be the first part of your host — if you set the host to acme.cupix.works and this says something else, a leftover CUPIX_TEAM_DOMAIN is overriding it. And the cache path is where your token now sits on disk, so it is the file to delete if you ever need to force a fresh login.
If it did not work, the message names the cause. These are the ones worth recognising:
| What you see | What it means | What to do |
|---|---|---|
| ERROR: CUPIX_HOST and CUPIX_API_TOKEN are required | Neither the environment variables nor the configuration file reached this shell. | For the Option 2 user-scope form, open a new terminal window. For Option 1, confirm the file exists at the path given in Save Your Credentials. |
| auth failed (HTTP 401) | The host answered but rejected the token. | Check the token for angle brackets or stray spaces, and confirm it was issued on the same host you set. |
| The term ... is not recognized as the name of a cmdlet | You are not in the folder holding the script. | Call it through the full path, as the $S variable above does. |
| ... cannot be loaded because running scripts is disabled on this system | Windows is blocking the script because it came from a downloaded file. | Run the Set-ExecutionPolicy and Unblock-File lines from Install for All Projects. |
| cupixworks-api missing from the /skills list | The folder is not where Claude looks, or is nested one level too deep. | The path should end in skills\cupixworks-api\SKILL.md — not skills\cupixworks-api\cupixworks-api\SKILL.md. |
| A command prints a response, then exits with code 22 | The request reached CupixWorks and was refused. This is not a setup problem. | Read the printed response body — it names the field or identifier at fault. Exit code 22 means any HTTP status of 400 or above. |
On Windows, the skill also includes an offline self-test that runs the helper script against a built-in mock server. It needs no CupixWorks account and no network access, so it is the quickest way to separate a setup problem from a credential problem.
powershell -ExecutionPolicy Bypass -File "$env:USERPROFILE\.claude\skills\cupixworks-api\scripts\Test-CupixPS.ps1" # Expected: 10 passed, 1 failed
Important: One check is expected to fail: POST sends a UTF-8 body intact. Windows PowerShell 5.1 does not preserve the quotation marks in a JSON request body passed as a command-line argument, so a body sent that way arrives unparseable. Reads are unaffected, and typing the same command directly in your own PowerShell window works. This check calls the script in a way that neither Claude nor ordinary command-line use does, so a failure here does not mean your setup, your credentials, or the API is broken. It is being looked into. Any other failing check does indicate a problem worth reporting.
Note: For Claude Cowork, place the same skill folder in the Cowork skills directory. Run file upload tasks in an environment that has access to your local files.
Skill Contents
You download one file and copy one folder. Nothing inside needs editing, and you never run the scripts yourself — Claude runs them for you. This table is here so you know what you installed, not because you have to do anything with it.
| File | What it does | Do you open it? |
|---|---|---|
| cupixworks-api.zip | The only file you download. Everything below is inside it. | Extract it, copy the folder, done. |
| SKILL.md | Tells Claude how the CupixWorks API works — authentication, rules, and the data hierarchy. | No. |
| scripts/cupix.sh | Makes the API calls on macOS and Linux. Handles login and the token cache. | No — Claude runs it. |
| scripts/cupix.ps1 | The same, on Windows. Identical commands, output, and exit codes. | No — Claude runs it. |
| scripts/Test-CupixPS.ps1 | Offline self-test for the Windows script, against a built-in mock server. | Only when troubleshooting. |
| references/api-reference.md | All 383 endpoints, grouped by domain, described by what each one does. | No — Claude consults it. |
| references/entity-fields.md | Which fields can be requested for each entity. | No — Claude consults it. |
| references/uploads.md | The detailed large-file upload procedure. | No — Claude consults it. |
Keep the folder structure intact. SKILL.md refers to the scripts and reference files by these relative paths, so moving or renaming anything inside will break it.
Basic Usage
Type these as ordinary messages to Claude — at the Claude Code prompt in your terminal, or in the Claude Cowork chat. Not in PowerShell or Terminal directly. Describe what you want in plain language. Refer to facilities and SiteViews by name — Claude searches for the name and resolves it to the underlying key. If required information is missing, such as a workspace, a level, or a required field, Claude asks before it proceeds.
| Goal | Prompt |
|---|---|
| Find a facility | "Find facilities in CupixWorks with 'Tower' in the name." |
| Review capture history | "Show the 10 most recent Records for Site 42 in date order." |
| Monitor processing | "Check the processing status of the captures I uploaded yesterday, and show error codes for any failures." |
| Create | "Create a facility named 'Site 42' in the B workspace with this address." |
| Create in bulk | "Create a facility for every row in projects.csv, in workspace 123." |
| Upload | "Upload the 360° videos in my Downloads folder to Site 42 as a new Record." |
| Share | "Create a SiteView from this week's captures, publish it, and turn on the public link." |
| Export in bulk | "Export all facilities to CSV with name, key, record count, and last capture date." |
- A Review is the same object as a SiteView.
- There is no entity called a project. The object you would call a project — one site or building — is a Facility. A Workspace is the group above it, used for a division, a region, or a client.
- Depending on how it was created, a Capture contains a source Video, Pano images, or both. For a video capture, the panos are derived from the video.
Safeguards
- Claude asks for confirmation before any operation that cannot be undone, including permanent deletion (purge).
- Delete operations move items to the trash by default, and trashed items can be restored.
- The API token is not written to logs or output.
What You Can Do
Everything below is something you ask Claude for in plain language. Each one shows the request you would make and what comes back. The equivalent commands are given too, for anyone scripting against the API without Claude — they are not required.
At a glance. Square brackets mark the parts you replace with your own values. Four rows have nothing to type — they explain how something works, so you read them rather than ask for them.
| Section | Ask Claude | What you get back |
|---|---|---|
| Search and List | Find facilities in CupixWorks with [text] in the name, then show the most recent capture sessions for one of them. | Matching sites with their names and keys, then a dated list of captures. |
| Monitor Capture Processing | Has processing finished on the capture from yesterday at [facility name]? If anything failed, tell me why. | The current stage of each capture, and the reason for any failure. |
| Export Every Page | Export every facility in our team to a CSV with name, key, status and record count. | A CSV covering every page of results, not just the first. |
| Link an External System | Tag [facility name] with our ERP code [code], then read it back to confirm. | Confirmation that the value was stored and reads back correctly. |
| Create and Share a SiteView | Create a SiteView for [level name] of [facility name] from the [month] capture, publish it, and give me a link I can send to the client. | The SiteView key and a shareable link. |
| About Bulk Creation | Nothing to type — this section explains how it works. | Why bulk work runs as a loop, and what happens if it stops partway through. |
| Create Facilities in Bulk | Here is a spreadsheet of new sites with names and addresses. Create them all in [workspace name] and tell me which ones succeeded. | Each site created, with its key, and anything that failed. |
| How Annotations Are Organized | Nothing to type — this section explains how it works. | What a layer is, and why annotations need one before they can be created. |
| Create Annotations in Bulk | Import this punch list as annotations on the [layer name] layer at [facility name], one per row. | How many annotations were created, with their ids. |
| How Uploading Works | Nothing to type — this section explains how it works. | The three steps of an upload, and why the final confirmation step matters. |
| Upload a 360 Video | Upload [file name] to [facility name], [level name], as a new capture dated [date], and tell me when processing starts. | Upload progress, then confirmation that processing began. |
| Upload Several Files | Upload every video in [folder] to one capture at [facility name], and report progress as you go. | Per-file progress, then a final summary. |
| Upload a Very Large File | This video is 40 GB. Upload it to [facility name] — use whatever method handles a file that size. | Claude selects the multipart method itself and reports progress. |
| Before You Upload | Nothing to type — this section explains how it works. | Expiry rules and limits worth knowing before a large upload. |
Each of these is expanded below, with the equivalent commands for anyone scripting without Claude.
Search and List
macOS · Linux (bash)
# Find facilities by name $S GET /facilities 'q=Tower&fields=name,key,state,address,records_count&per_page=20' # Records for one facility, newest first $S GET /records 'facility_key=<KEY>&order_by=captured_at&sort=desc&fields=name,captured_at,captures_count,panos_count&per_page=10' # Levels in a facility $S GET /levels 'facility_key=<KEY>&fields=name,id,is_ground_level' # Panos in one Record, including position $S GET /panos 'record_id=<RECORD_ID>&fields=name,state,captured_at,world_position&per_page=50'
Windows (PowerShell)
# Find facilities by name & $S GET /facilities 'q=Tower&fields=name,key,state,address,records_count&per_page=20' # Records for one facility, newest first & $S GET /records 'facility_key=<KEY>&order_by=captured_at&sort=desc&fields=name,captured_at,captures_count,panos_count&per_page=10' # Levels in a facility & $S GET /levels 'facility_key=<KEY>&fields=name,id,is_ground_level' # Panos in one Record, including position & $S GET /panos 'record_id=<RECORD_ID>&fields=name,state,captured_at,world_position&per_page=50'
Monitor Capture Processing
macOS · Linux (bash)
$S GET /captures 'facility_key=<KEY>&fields=name,state,upload_state,reconstruction_state,progress,error_code&order_by=created_at&sort=desc'
Windows (PowerShell)
& $S GET /captures 'facility_key=<KEY>&fields=name,state,upload_state,reconstruction_state,progress,error_code&order_by=created_at&sort=desc'
Read state and reconstruction_state for the current stage, and error_code for the cause of a failure.
Export Every Page
A list request returns one page at a time. To export everything, keep requesting pages until next_page is null.
macOS · Linux (bash)
#!/usr/bin/env bash
page=1
echo "name,key,state,records_count" > facilities.csv
while : ; do
resp=$($S GET /facilities "fields=name,key,state,records_count&per_page=100&page=$page")
echo "$resp" | python3 -c '
import json, sys, csv
w = csv.writer(sys.stdout)
for it in json.load(sys.stdin)["result"]["data"]:
a = it["attributes"]
w.writerow([a.get("name",""), a.get("key"), a.get("state"), a.get("records_count",0)])
' >> facilities.csv
next=$(echo "$resp" | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["pagination"]["next_page"] or "")')
[ -z "$next" ] && break
page=$next
done
Windows (PowerShell)
$page = 1
$rows = @()
while ($true) {
$resp = & $S GET /facilities "fields=name,key,state,records_count&per_page=100&page=$page" | ConvertFrom-Json
foreach ($it in $resp.result.data) {
$a = $it.attributes
$rows += [pscustomobject]@{
name = $a.name
key = $a.key
state = $a.state
records_count = $a.records_count
}
}
$next = $resp.result.pagination.next_page
if (-not $next) { break }
$page = $next
}
$rows | Export-Csv -Path .\facilities.csv -NoTypeInformation -Encoding UTF8
Link an External System
Every major entity accepts a free-form meta value, which you can use to store an identifier from an external system.
macOS · Linux (bash)
# Write an ERP project code to a facility, then read it back
$S PUT /facilities/<KEY>/meta/erp_code 'fields=meta' '{"value":"PRJ-2026-042"}'
$S GET /facilities/<KEY>/meta/erp_code ''
Windows (PowerShell)
# Write an ERP project code to a facility, then read it back
& $S PUT /facilities/<KEY>/meta/erp_code 'fields=meta' '{"value":"PRJ-2026-042"}'
& $S GET /facilities/<KEY>/meta/erp_code ''
Create and Share a SiteView
A Review, also called a SiteView, is a selection of capture results that you share with other people. The example below covers the full flow: create, publish, and share.
macOS · Linux (bash)
# 1) Find the facility key and the level id
$S GET /facilities 'q=Site 42&fields=name,key'
$S GET /levels 'facility_key=<KEY>&fields=name,id'
# 2) Create the SiteView (levels and areas define what it includes)
$S POST /reviews 'fields=name,key' '{
"facility_key": "<KEY>",
"name": "Site 42 - 1F July Progress",
"levels": {"ids": [<LEVEL_ID>], "all": false},
"areas": {"ids": [], "all": true},
"date_ranges": {"ranges": [{"from": "2026-07-01", "to": "2026-07-31"}], "all": false}
}'
# 3) Publish it — recipients cannot open an unpublished SiteView
$S PUT /reviews/<REVIEW_KEY>/publish 'fields=key,published_at' '{}'
# 4-a) Share with specific people
$S PUT /reviews/<REVIEW_KEY>/share 'fields=key' '{"emails":["pm@customer.com"]}'
# 4-b) Or enable the public link, which opens without a sign-in
$S PUT /reviews/<REVIEW_KEY>/enable_public_access 'fields=key,public_access_enabled_at' '{}'
Windows (PowerShell)
# 1) Find the facility key and the level id
& $S GET /facilities 'q=Site 42&fields=name,key'
& $S GET /levels 'facility_key=<KEY>&fields=name,id'
# 2) Create the SiteView (levels and areas define what it includes)
$body = @{
facility_key = "<KEY>"
name = "Site 42 - 1F July Progress"
levels = @{ ids = @(<LEVEL_ID>); all = $false }
areas = @{ ids = @(); all = $true }
date_ranges = @{ ranges = @(@{ from = "2026-07-01"; to = "2026-07-31" }); all = $false }
} | ConvertTo-Json -Depth 5 -Compress
& $S POST /reviews 'fields=name,key' $body
# 3) Publish it — recipients cannot open an unpublished SiteView
& $S PUT /reviews/<REVIEW_KEY>/publish 'fields=key,published_at' '{}'
# 4-a) Share with specific people
& $S PUT /reviews/<REVIEW_KEY>/share 'fields=key' '{"emails":["pm@customer.com"]}'
# 4-b) Or enable the public link, which opens without a sign-in
& $S PUT /reviews/<REVIEW_KEY>/enable_public_access 'fields=key,public_access_enabled_at' '{}'
Note:
- The public link can be enabled only when your team policy allows it (PUT /teams/enable_review_public_access).
- The required request body structure for levels and areas varies by team configuration. If a request fails, check the error message for the required field.
- Building a nested JSON body with ConvertTo-Json, as in the PowerShell example, avoids most quoting mistakes.
| Task | Endpoint |
|---|---|
| List or read SiteViews | GET /reviews, GET /reviews/{key} |
| Change the included scope | PUT /reviews/{key} (levels, areas, date_ranges) |
| Unpublish | DELETE /reviews/{key}/publish |
| Turn off the public link | PUT /reviews/{key}/disable_public_access |
| Unshare or block access | PUT /reviews/{key}/unshare, PUT /reviews/{key}/deny |
| Read content inside a SiteView | GET /reviews/{review_key}/panos, GET /reviews/{review_key}/annotations — accessible with SiteView permission alone |
About Bulk Creation
This section covers creating many records in one run: a set of new sites, or a list of site issues imported from another system.
Important: There is no batch create endpoint for facilities or annotations. Bulk creation is a loop of single create calls, so your script has to handle partial failure. Batch endpoints do exist for a few other operations — POST /categories/bulk, PUT /panos/bulk_update, PUT /annotations/trash, and PUT /facilities/share — and are noted where relevant below.
Create Facilities in Bulk
A Facility is the object you would call a project — one site or building. POST /facilities requires name, address, and workspace_id. Find the workspace id first with GET /workspaces.
The examples read a CSV with a name column and an address column, create one facility per row, and record the returned key so you can tell which rows succeeded.
macOS · Linux (bash)
# projects.csv — header row: name,address
$S GET /workspaces 'fields=name,id' # find your workspace_id
tail -n +2 projects.csv | while IFS=, read -r name address; do
resp=$($S POST /facilities 'fields=name,key' \
"{\"name\":\"$name\",\"address\":\"$address\",\"workspace_id\":<WORKSPACE_ID>}") || {
echo "FAILED: $name" >> failed.txt; continue; }
key=$(echo "$resp" | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["data"]["attributes"]["key"])')
echo "$name,$key" >> created.csv
sleep 0.2
done
Windows (PowerShell)
# projects.csv — header row: name,address
& $S GET /workspaces 'fields=name,id' # find your workspace_id
$created = @()
$failed = @()
foreach ($row in Import-Csv .\projects.csv) {
$body = @{
name = $row.name
address = $row.address
workspace_id = <WORKSPACE_ID>
} | ConvertTo-Json -Compress
$out = & $S POST /facilities 'fields=name,key' $body
# The helper script exits 22 on HTTP 400 and above. It does not raise a
# PowerShell error, so check $LASTEXITCODE rather than using try/catch.
if ($LASTEXITCODE -ne 0) {
$failed += $row.name
} else {
$key = ($out | ConvertFrom-Json).result.data.attributes.key
$created += [pscustomobject]@{ name = $row.name; key = $key }
}
Start-Sleep -Milliseconds 200
}
$created | Export-Csv .\created.csv -NoTypeInformation -Encoding UTF8
$failed | Set-Content .\failed.txt
Note: Keep a record of what was created, as both examples do. Because there is no idempotency key, re-running the whole file after a partial failure would create duplicates of the rows that already succeeded.
How Annotations Are Organized
An Annotation is a marking placed on a drawing or in the 3D space — a site issue, an inspection entry, or a punch list item. Annotations are grouped into an AnnotationLayer, one layer per purpose, such as Safety Inspection or Quality Check. A layer can have a FormDesign attached to it, and annotations in that layer are then filled in using that form.
So the order of work is: create the layer, then create annotations in it.
| Task | Endpoint |
|---|---|
| Create a layer | POST /annotation_layers |
| Attach a form to a layer | PUT /annotation_layers/{id}/associate_form_designs |
| List available forms | GET /form_designs |
| Create an annotation | POST /annotations |
| Create one inside a SiteView | POST /reviews/{review_key}/annotations |
| Update title, status, or form values | PUT /annotations/{id} |
| Move it on the drawing | PUT /annotations/{id}/position |
| Move many to the trash at once | PUT /annotations/trash |
| Attach a photo | POST /annotations/{id}/resources/{kind}/upload_url, then PUT .../check_uploading |
| List or read annotations | GET /annotations, GET /annotations/{id} |
Useful fields values for an annotation are name, description, state, kind, annotation_layer, form_design, form_status, level, record, review, world_position, world_position_2d, and created_at. The full list is in references/entity-fields.md.
Create Annotations in Bulk
The examples below import a list of issues into one layer. Treat the request bodies as illustrative: the fields required when creating an annotation layer or an annotation are not yet documented, so confirm the shapes against your own host before you rely on them.
macOS · Linux (bash)
# 1) Create the layer once
$S POST /annotation_layers 'fields=name,id' \
'{"facility_key":"<KEY>","name":"Safety Inspection - July"}'
# 2) Create one annotation per row of issues.csv (header: title,description,level_id)
tail -n +2 issues.csv | while IFS=, read -r title description level_id; do
$S POST /annotations 'fields=name,state,id' \
"{\"annotation_layer_id\":<LAYER_ID>,\"name\":\"$title\",\"description\":\"$description\",\"level_id\":$level_id}"
sleep 0.2
done
# 3) Attach a photo to one annotation
resp=$($S POST /annotations/<ANNOTATION_ID>/resources/attachment/upload_url 'fields=upload_url' '{}')
url=$(echo "$resp" | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["data"]["attributes"]["upload_url"])')
$S upload ./crack.jpg "$url"
$S PUT /annotations/<ANNOTATION_ID>/resources/attachment/check_uploading 'fields=state' '{}'
Windows (PowerShell)
# 1) Create the layer once
& $S POST /annotation_layers 'fields=name,id' `
'{"facility_key":"<KEY>","name":"Safety Inspection - July"}'
# 2) Create one annotation per row of issues.csv (header: title,description,level_id)
foreach ($row in Import-Csv .\issues.csv) {
$body = @{
annotation_layer_id = <LAYER_ID>
name = $row.title
description = $row.description
level_id = [int]$row.level_id
} | ConvertTo-Json -Compress
& $S POST /annotations 'fields=name,state,id' $body
if ($LASTEXITCODE -ne 0) { Add-Content .\failed.txt $row.title }
Start-Sleep -Milliseconds 200
}
# 3) Attach a photo to one annotation
$resp = & $S POST /annotations/<ANNOTATION_ID>/resources/attachment/upload_url 'fields=upload_url' '{}' | ConvertFrom-Json
& $S upload .\crack.jpg $resp.result.data.attributes.upload_url
& $S PUT /annotations/<ANNOTATION_ID>/resources/attachment/check_uploading 'fields=state' '{}'
To undo a bulk import, PUT /annotations/trash moves many annotations to the trash in one call, and trashed items can be restored.
How Uploading Works
Uploading takes three steps: request an upload URL, which the server returns as an S3 presigned URL; PUT the file to that URL, which needs no authentication header; then call check_uploading so the server verifies the file and starts processing. Multi-gigabyte files upload as a stream.
Important: If you do not call check_uploading (or check_materials_uploading for capture materials), the file stays in the uploading state and processing never starts.
Upload a 360° Video
macOS · Linux (bash)
# 0) Identify the target
$S GET /facilities 'q=Site 42&fields=name,key'
$S GET /levels 'facility_key=<KEY>&fields=name,id'
# 1) Create the Record
$S POST /records 'fields=id' '{"facility_key":"<KEY>","captured_at":"2026-07-22T09:00:00Z"}'
# 2) Create the Capture and the video together — the response includes videos[].upload_url
$S POST /captures/with_materials 'fields=id,name,upload_state' '{
"name": "2026-07-22 1F Capture",
"record_id": <RECORD_ID>,
"level_id": <LEVEL_ID>,
"creation_platform": "api",
"material": "video",
"videos": [{"name": "walk.mp4"}]
}'
# 3) Upload the file (streaming, three retries)
$S upload ~/Downloads/walk.mp4 "<UPLOAD_URL>"
# 4) Report completion — the server starts frame extraction, pano generation, and 3D alignment
$S PUT /captures/<CAPTURE_ID>/check_materials_uploading 'fields=state,upload_state' '{}'
# 5) Check processing status until it finishes
$S GET /captures/<CAPTURE_ID> 'fields=state,upload_state,reconstruction_state,progress,error_code'
Windows (PowerShell)
# 0) Identify the target
& $S GET /facilities 'q=Site 42&fields=name,key'
& $S GET /levels 'facility_key=<KEY>&fields=name,id'
# 1) Create the Record
& $S POST /records 'fields=id' '{"facility_key":"<KEY>","captured_at":"2026-07-22T09:00:00Z"}'
# 2) Create the Capture and the video together — the response includes videos[].upload_url
$body = @{
name = "2026-07-22 1F Capture"
record_id = <RECORD_ID>
level_id = <LEVEL_ID>
creation_platform = "api"
material = "video"
videos = @(@{ name = "walk.mp4" })
} | ConvertTo-Json -Depth 5 -Compress
$cap = & $S POST /captures/with_materials 'fields=id,name,upload_state' $body | ConvertFrom-Json
$id = $cap.result.data.attributes.id
$url = $cap.result.data.attributes.videos[0].upload_url
# 3) Upload the file (streaming, three retries)
& $S upload "$env:USERPROFILE\Downloads\walk.mp4" $url
# 4) Report completion — the server starts frame extraction, pano generation, and 3D alignment
& $S PUT "/captures/$id/check_materials_uploading" 'fields=state,upload_state' '{}'
# 5) Check processing status until it finishes
& $S GET "/captures/$id" 'fields=state,upload_state,reconstruction_state,progress,error_code'
Upload Several Files
macOS · Linux (bash)
# Register every mp4 in a folder to one capture, then upload in sequence
for f in ~/Downloads/site42/*.mp4; do
name=$(basename "$f")
resp=$($S POST /videos 'fields=id,upload_url' "{\"capture_id\": <CAPTURE_ID>, \"name\": \"$name\"}")
vid=$(echo "$resp" | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["data"]["id"])')
url=$(echo "$resp" | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["data"]["attributes"]["upload_url"])')
$S upload "$f" "$url"
$S PUT /videos/$vid/check_uploading 'fields=state' '{}'
done
Windows (PowerShell)
# Register every mp4 in a folder to one capture, then upload in sequence
Get-ChildItem "$env:USERPROFILE\Downloads\site42\*.mp4" | ForEach-Object {
$body = @{ capture_id = <CAPTURE_ID>; name = $_.Name } | ConvertTo-Json -Compress
$resp = & $S POST /videos 'fields=id,upload_url' $body | ConvertFrom-Json
$vid = $resp.result.data.id
$url = $resp.result.data.attributes.upload_url
& $S upload $_.FullName $url
& $S PUT "/videos/$vid/check_uploading" 'fields=state' '{}'
}
Upload a Very Large File
For files too large for a single PUT — tens of gigabytes — request temporary AWS credentials with upload_credentials and use the aws CLI, which splits the file and resumes an interrupted transfer.
macOS · Linux (bash)
cred=$($S POST /videos/<VIDEO_ID>/upload_credentials 'fields=@default' '{}')
# Response: access_key_id, secret_access_key, session_token, bucket_name, bucket_region, basepath
AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_SESSION_TOKEN=... \
aws s3 cp ./huge.mp4 "s3://<BUCKET>/<BASEPATH>/huge.mp4" --region <REGION>
$S PUT /videos/<VIDEO_ID>/check_uploading 'fields=state' '{}'
Windows (PowerShell)
$cred = & $S POST /videos/<VIDEO_ID>/upload_credentials 'fields=@default' '{}' | ConvertFrom-Json
$c = $cred.result
$env:AWS_ACCESS_KEY_ID = $c.access_key_id
$env:AWS_SECRET_ACCESS_KEY = $c.secret_access_key
$env:AWS_SESSION_TOKEN = $c.session_token
aws s3 cp .\huge.mp4 "s3://$($c.bucket_name)/$($c.basepath)/huge.mp4" --region $c.bucket_region
& $S PUT /videos/<VIDEO_ID>/check_uploading 'fields=state' '{}'
Note: Install the aws CLI with brew install awscli on macOS, or winget install Amazon.AWSCLI on Windows.
Before You Upload
- A presigned URL expires, so upload as soon as you request it.
- Check your credit and storage quota before a bulk upload with GET /license/teams/quota.
- To resume an interrupted upload, list the pending items with GET /videos/upload_candidates or GET /panos/upload_candidates.
Frequently Asked Questions
The attributes in my response are empty
List real field names in the fields parameter, as in fields=name,key,state, rather than a magic value such as fields=@default. See references/entity-fields.md for the field list.
I get a "team_domain is required" error
The authentication request body is missing team_domain, which is the first label of the host — in acme.cupix.works, it is acme. The helper script handles this, so the error appears only when you call curl directly.
I get a 401 Unauthorized error
- The access token expires after about one hour, and the helper script reauthenticates on its own, so retry the request.
- If 401 continues, the API token is incorrect or has been revoked. Check it again in your CupixWorks profile.
- Confirm the authentication header is X-CUPIX-AUTH, not Authorization: Bearer.
On Windows, the script will not run
- "cannot be loaded because running scripts is disabled" — run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned, or call the script as powershell -ExecutionPolicy Bypass -File <path>.
- "not digitally signed" — the file still carries the mark it was downloaded with. Clear it with Unblock-File, as shown in Install the Skill.
- The script runs but nothing works — run scripts/Test-CupixPS.ps1. It checks the wrapper against a built-in mock server, so a pass means the problem is your host or token rather than your setup.
- Do not run cupix.sh on Windows. Use cupix.ps1.
My file uploaded but processing did not start
Check that you called check_uploading, or check_materials_uploading for capture materials. The server starts the processing pipeline only after that call.
How do I tell whether capture processing failed
macOS · Linux (bash)
$S GET /captures/<ID> 'fields=state,upload_state,reconstruction_state,progress,error_code,warnings'
Windows (PowerShell)
& $S GET /captures/<ID> 'fields=state,upload_state,reconstruction_state,progress,error_code,warnings'
An error_code value means the capture failed. Retry processing with POST /captures/{id}/invoke.
Can I recover deleted data
An item in the trash can be restored with PUT .../untrash. An item removed with purge cannot be recovered, which is why the skill always asks for confirmation before a purge.
I get "This feature is only available in CupixVista" (STAT10001)
The feature is not included in your current team plan. Check your plan and license with GET /teams and GET /license/teams/quota.
Where should I store my api_token
Use the configuration file or an environment variable. On macOS and Linux, that is ~/.cupixworks/config.json with permission 600. On Windows, it is %USERPROFILE%\.cupixworks\config.json, restricted to your account with icacls. Do not put the token in source code commits, documents, wikis, or chat logs. The token cache file holds only the one-hour access token.
Can I use more than one team or environment
Yes. The token cache is separated by host, so change CUPIX_HOST and continue.
Reference: Error Codes
| Code or Symptom | Meaning | Action |
|---|---|---|
| HTTP 401 | Token expired or invalid | Authenticate again — the helper script does this automatically |
| ARG10000 | Required parameter missing | Check the parameter named in the message |
| ARG10040 | UUID format error | Check the format of the uuid value |
| STAT10001 | Feature not in the plan | Check your plan and license |
| HTTP 403 | Insufficient permission | Check share and member permissions with GET .../members |
| HTTP 404 | Target not found | Check the identifier — Facility and Review use a key, others use a numeric id |
| Exit code 22 | The helper script received HTTP 400 or above | Read the response body printed above the exit |
Reference: Data Model
Knowing the hierarchy helps you phrase requests precisely.
Team (contract unit)
└─ Workspace (project group)
└─ Facility (site or building, identified by key) ── Level (floor)
└─ Record (capture session)
└─ Capture (processing unit)
├─ Pano (360° panorama image)
└─ Video (source 360° video)
└─ Review = SiteView (shareable view, identified by key)
└─ AnnotationLayer (a group of annotations) ── Annotation (site issue, note, form entry)
Reference: API Rules
These rules apply when you call the API directly. Claude already applies them through the skill.
Authentication
macOS · Linux (bash)
# 1) Exchange the api_token for an access token curl -X POST "https://{host}/api/v1/authenticate?fields=@default" \ -H "Content-Type: application/json" \ -d '{"grant_type":"api_token","api_token":"PASTE_YOUR_TOKEN_HERE","team_domain":"<TEAM>"}' # → result.access_token (valid for about 1 hour) # 2) Send the access token in the X-CUPIX-AUTH header on every request curl "https://{host}/api/v1/facilities?fields=name,key" -H "X-CUPIX-AUTH: <ACCESS_TOKEN>"
Windows (PowerShell)
# 1) Exchange the api_token for an access token curl.exe -X POST "https://{host}/api/v1/authenticate?fields=@default" ` -H "Content-Type: application/json" ` -d '{"grant_type":"api_token","api_token":"PASTE_YOUR_TOKEN_HERE","team_domain":"<TEAM>"}' # → result.access_token (valid for about 1 hour) # 2) Send the access token in the X-CUPIX-AUTH header on every request curl.exe "https://{host}/api/v1/facilities?fields=name,key" -H "X-CUPIX-AUTH: <ACCESS_TOKEN>"
Note:
- The authentication header is X-CUPIX-AUTH, not Authorization: Bearer.
- In PowerShell, call curl.exe by its full name. Plain curl is an alias for Invoke-WebRequest, which takes different arguments.
- PowerShell continues a command onto the next line with a backtick, where bash uses a backslash.
The fields Parameter
Almost every read, create, and update request requires a fields query parameter, and the value must be an explicit comma-separated list such as fields=name,key,state. A magic value such as fields=@default returns empty attributes. The field list for each entity is in references/entity-fields.md.
Response Structure
{
"result": {
"data": [ { "id": "123", "type": "facility", "attributes": { "...": "..." } } ],
"pagination": { "total_entries": 2973, "total_pages": 991, "next_page": 2 }
},
"session": { "...": "..." }
}
- Values live in result.data[].attributes. For a single-item request, result.data is an object rather than an array.
- For pagination, send page and per_page, and keep requesting pages until next_page is null.
Identifiers and List Parameters
| Entity | Path Identifier | Example |
|---|---|---|
| Facility, Review (SiteView) | key (short string) | /facilities/mau01x |
| Record, Capture, Pano, Level, Annotation, and others | id (number) | /records/47060 |
List endpoints share these parameters: q (search term), order_by with sort (asc or desc), page and per_page, visibility (untrashed by default, in_trash, and others), and parent filters such as workspace_id, facility_key, record_id, and capture_id.
Deletion and Errors
Deletion has two stages: PUT .../trash moves the item to the trash and can be reversed, and DELETE .../purge removes it permanently. Records also support PUT .../safe_trash, which checks for references before trashing.
{ "result": { "code": "ARG10000", "type": "Cupix::Errors::Parameter",
"reason": "team_domain is required.", "message": "..." } }
HTTP 401 means the token expired or is invalid, so authenticate again. Use result.code and message to identify the cause, and see the Error Code Reference section.
Reference: Helper Script
The helper script is the execution tool inside the skill. Claude uses it internally, and you can run it as a standalone command-line tool. Use scripts/cupix.sh on macOS and Linux, and scripts/cupix.ps1 on Windows.
Configuration
The script reads configuration from environment variables or from a configuration file, and environment variables take precedence.
| Variable | Description |
|---|---|
| CUPIX_HOST | Required. For example, acme.cupix.works. |
| CUPIX_API_TOKEN | Required. Your API token. |
| CUPIX_TEAM_DOMAIN | Optional. Defaults to the first label of the host. |
| CUPIX_CONFIG | Optional. Configuration file path. Defaults to ~/.cupixworks/config.json, or %USERPROFILE%\.cupixworks\config.json on Windows. |
Commands
Every command example in this article assumes $S is set to the helper script path, as shown below. The command set is identical on both platforms. Only the way you invoke the script differs.
auth # authenticate and cache the token whoami # show the signed-in user GET <path> '<query>' # read POST <path> '<query>' '<json>' # create PUT <path> '<query>' '<json>' # update DELETE <path> '' # delete upload <local-file> '<presigned_url>' # upload a file (S3 presigned PUT)
macOS · Linux (bash)
export CUPIX_HOST="acme.cupix.works" CUPIX_API_TOKEN="PASTE_YOUR_TOKEN_HERE" S=~/.claude/skills/cupixworks-api/scripts/cupix.sh $S auth # test authentication $S whoami # confirm your account $S GET /facilities 'fields=name,key,state&per_page=10' # facility list $S POST /facilities 'fields=name,key' '{"name":"Site 42","address":"Seoul","workspace_id":123}' $S PUT /facilities/abc123 'fields=name' '{"name":"Site 42 - Phase 2"}'
Windows (PowerShell)
$env:CUPIX_HOST = "acme.cupix.works"; $env:CUPIX_API_TOKEN = "PASTE_YOUR_TOKEN_HERE" $S = "$env:USERPROFILE\.claude\skills\cupixworks-api\scripts\cupix.ps1" & $S auth # test authentication & $S whoami # confirm your account & $S GET /facilities 'fields=name,key,state&per_page=10' # facility list & $S POST /facilities 'fields=name,key' '{"name":"Site 42","address":"Seoul","workspace_id":123}' & $S PUT /facilities/abc123 'fields=name' '{"name":"Site 42 - Phase 2"}'
Important: Wrap a JSON body in single quotes on both platforms. In PowerShell, single quotes are literal, so '{"name":"Site 42"}' is passed through unchanged. Double quotes would let PowerShell interpret $ as a variable and corrupt the request.
Script Behavior
- Token cache — the access token is valid for one hour, is cached per host in **~/.cupixworks/token_*.json (%USERPROFILE%\.cupixworks\** on Windows), and is reissued 60 seconds before it expires.
- Automatic 401 recovery — if the server rejects the token, the script authenticates once and retries.
- Upload — streaming PUT with no memory limit, three retries on a transient error, and a transfer size and speed report on completion. On Windows the script uses the built-in curl.exe, and falls back to a streaming .NET client if it is not present.
- File permissions — the cached token file is restricted to your account, with chmod 600 on macOS and Linux and an equivalent ACL on Windows.
- Errors — on HTTP 400 or above, the script prints the response body and exits with code 22.
Appendix: What Is in the Download
The attached cupixworks-api.zip extracts to this structure. Keep it intact — the skill definition refers to the scripts and references by these relative paths.
cupixworks-api/ ├─ SKILL.md ├─ scripts/ │ ├─ cupix.sh (macOS, Linux) │ ├─ cupix.ps1 (Windows) │ └─ Test-CupixPS.ps1 (Windows self-test) └─ references/ ├─ api-reference.md ├─ entity-fields.md └─ uploads.md