Skip to main content

Import clients, projects, phases, people, and tasks via CSV + API

Written by Nermin Music

✅ Use Cases

  • Historical data import

  • Client onboarding

  • Migrating from another PM tool

  • Automating task creation


📦 What You'll Be Able to Import

This import script allows you to create:

  • 🧑 People

  • 🏢 Clients

  • 🗂️ Projects

  • 🧱 Phases

  • ✅ Tasks (allocations)

  • 🔖 Tags for projects and people


🧠 Understand How the Script Works

The script does the following:

  1. Reads the CSV

  2. Fetches existing data from your Float account (clients, projects, people, phases)

  3. Creates new entries only when needed (skipping duplicates)

  4. Posts tasks (allocations) after setting up all dependencies


🧰 Prerequisites

1. Python 3 Installed

  • Download from python.org

  • During install on Windows: ✅ Add Python to PATH

Check it's working:

python3 --version

2. Install Python packages

pip install pandas requests

3. Get Your Float API Key

  • Log in as an Admin

  • Go to: Team Settings > Integrations > API Key


📄 CSV Template Overview

The CSV should contain the following fields (in any order):

✅ Required Columns

Column

Description

project_name

Name of the project

person_name

Name of the person (assignee)

task_name

Task name

start_date

Task start (YYYY-MM-DD)

end_date

Task end (YYYY-MM-DD)

hours

Hours allocated

🧩 Optional Columns

Column

Description

client_name

Client assigned to the project

project_code

Unique identifier for the project

project_notes

Notes stored on the project

budget_type

1 = Hours, 2 = Total Fee, 3 = Hourly Fee

budget_total

Number (hours or currency depending on budget_type)

budget_priority

0 = Project-level, 1 = Phase, 2 = Task

status

0 = Draft, 1 = Tentative, 2 = Confirmed

non_billable

1 = Non-billable, 0 = Billable

people_tags

Comma-separated tags for the person (e.g. Dev, Remote)

default_hourly_rate

Number, e.g. 125.00

project_tags

Comma-separated tags for the project

phase_name

Name of the phase

phase_start_date

Start of the phase

phase_end_date

End of the phase

phase_budget_total

Number (hours/fee)

📥 Example CSV Snippet

project_name,project_code,client_name,project_notes,budget_type,budget_total,budget_priority,status,non_billable,project_tags,phase_name,phase_start_date,phase_end_date,phase_budget_total,person_name,default_hourly_rate,people_tags,task_name,start_date,end_date,hours Website Redesign,WD-001,Acme Corp,Initial rollout,1,200,0,2,0,Marketing,Design Phase,2025-08-01,2025-08-15,100,Jane Smith,85,Remote,UI Design,2025-08-01,2025-08-05,6 Website Redesign,WD-001,Acme Corp,Initial rollout,,,,,,Marketing,,,,,,Jane Smith,85,Remote,UX Research,2025-08-06,2025-08-10,4


⚙️ Uniqueness Strategy Configuration

Set how project uniqueness is determined (to avoid duplicates):

PROJECT_UNIQUENESS_STRATEGY = "project_code" # Options: "name", "name+client", "project_code"

🧠 Deduplication Logic

  • Clients: matched by client_name

  • Projects: matched using the PROJECT_UNIQUENESS_STRATEGY setting (e.g. by project_code or name + client)

  • Phases: matched by combination of client_id + project_id + phase_name

  • People: matched by person_name


🚀 Import Script

#!/usr/bin/env python3
import pandas as pd
import requests
import time
import json
import hashlib
from datetime import datetime

# === Step 1: CSV + Auth Config ===
CSV_FILE = "you_file.csv" #replace with name of your csv file
DRY_RUN = False
SHOW_SUCCESS_OUTPUT = True
BEARER_TOKEN = "Your_API_Token" #Replace with API token from integrations page

# === Error log file ===
ERROR_LOG_FILE = "upload-errors1.txt"
# Initialize error log with start timestamps (local + UTC)
with open(ERROR_LOG_FILE, "w") as f:
f.write("=== Float Upload Errors ===\n")
f.write(
"Import started: "
f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} (local), "
f"{datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC\n\n"
)

# === Step 2: Float API URLs ===
BASE_URL = "https://api.float.com/v3"
CLIENTS_API_URL = f"{BASE_URL}/clients"
PROJECTS_API_URL = f"{BASE_URL}/projects"
PHASES_API_URL = f"{BASE_URL}/phases"
PEOPLE_API_URL = f"{BASE_URL}/people"
TASKS_API_URL = f"{BASE_URL}/tasks"

# === Step 3: Configuration Options ===
PROJECT_UNIQUENESS_STRATEGY = "name+client" # Options: "name", "name+client", "project_code"
PER_PAGE = 200
HEADERS = {
"Authorization": f"Bearer {BEARER_TOKEN}",
"Content-Type": "application/json",
"User-Agent": "Float Data Importer (support@float.com)"
}

# === Step 4: Utility Functions ===
def sanitize(value):
if pd.isna(value) or str(value).strip() in ("", "NaN", "nan"):
return ""
return str(value).strip()

def log_error(row_num, message, payload=None):
"""Append an error entry to upload-errors.txt."""
with open(ERROR_LOG_FILE, "a") as f:
f.write(f"Row {row_num}: {message}\n")
if payload is not None:
try:
f.write(f"Payload: {json.dumps(payload, indent=2)}\n")
except TypeError:
f.write(f"Payload (unserializable): {str(payload)}\n")
f.write("\n")

def generate_color(project_key: str) -> str:
"""
Deterministically generate a Float-compatible hex color (NO # sign).
Produces a stable 6-digit uppercase hex string like "FF4B4B".
"""
if not project_key:
return "CCCCCC" # Neutral gray fallback
h = hashlib.sha1(project_key.encode("utf-8")).hexdigest()
return h[:6].upper() # Float expects "RRGGBB"
def fetch_existing(endpoint, label):
"""Fetch existing items from Float with pagination."""
try:
print(f"📦 Fetching existing {label} from Float...")
cur_page, last_page = 1, 1
data = []

while cur_page <= last_page:
response = requests.get(
f"{BASE_URL}/{endpoint}?per-page={PER_PAGE}&page={cur_page}",
headers=HEADERS
)
response.raise_for_status()

last_page = int(response.headers.get("x-pagination-page-count") or 0) or 1
data.extend(response.json())
cur_page += 1
print(f"→ {label.capitalize()}: {len(data)}")
return data

except Exception as e:
msg = f"Failed to fetch {label}: {e}"
print("❌ " + msg)
log_error("FETCH", msg)
return []

def build_lookup_map(items, key_fn):
return {key_fn(item): item for item in items if key_fn(item)}

def project_key_from_csv(row):
name = sanitize(row.get("project_name") or row.get("name"))
code = sanitize(row.get("project_code"))
client = sanitize(row.get("client_name"))
if PROJECT_UNIQUENESS_STRATEGY == "name":
return name.lower()
elif PROJECT_UNIQUENESS_STRATEGY == "name+client":
return f"{client.lower()}|{name.lower()}"
elif PROJECT_UNIQUENESS_STRATEGY == "project_code":
return code.lower()
return None
def project_key_from_api(p, client_lookup):
name = sanitize(p.get("name"))
code = sanitize(p.get("project_code"))
client_id = sanitize(p.get("client_id"))
client_name = sanitize(client_lookup.get(str(client_id), ""))
if PROJECT_UNIQUENESS_STRATEGY == "name":
return name.lower()
elif PROJECT_UNIQUENESS_STRATEGY == "name+client":
return f"{client_name.lower()}|{name.lower()}"
elif PROJECT_UNIQUENESS_STRATEGY == "project_code":
return code.lower()
return None
def phase_key(client_id, project_id, phase_name):
return f"{client_id}|{project_id}|{phase_name.lower()}"
# === Step 5: Load CSV File ===
try:
df = pd.read_csv(CSV_FILE)
except Exception as e:
print(f"❌ Failed to read CSV: {e}")
log_error("CSV", f"Failed to read CSV: {e}")
# Write completion timestamp before exit
with open(ERROR_LOG_FILE, "a") as f:
f.write(
"\nImport completed (with failure during CSV load): "
f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} (local), "
f"{datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC\n"
)
exit(1)

required_columns = ["project_name", "task_name", "person_name", "start_date", "end_date", "hours"]
missing_columns = [col for col in required_columns if col not in df.columns]
if missing_columns:
msg = f"CSV is missing required columns: {', '.join(missing_columns)}"
print(f"❌ {msg}")
log_error("CSV", msg)
with open(ERROR_LOG_FILE, "a") as f:
f.write(
"\nImport completed (with failure due to missing columns): "
f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} (local), "
f"{datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC\n"
)
exit(1)
else:
print("✅ CSV validation passed. All required columns are present.\n")

# === Step 6: Fetch Existing Data ===
clients = fetch_existing("clients", "clients")
projects = fetch_existing("projects", "projects")
people = fetch_existing("people", "people")
phases = fetch_existing("phases", "phases")

# === Step 7: Build Lookup Maps ===
clients_map = build_lookup_map(clients, lambda c: sanitize(c["name"]).lower())
clients_map_rev = {str(v["client_id"]): k for k, v in clients_map.items()}
people_map = build_lookup_map(people, lambda p: sanitize(p["name"]).lower())
projects_map = build_lookup_map(projects, lambda p: project_key_from_api(p, clients_map_rev))
phases_map = build_lookup_map(
phases,
lambda ph: phase_key(
sanitize(ph.get("client_id", "")),
sanitize(ph.get("project_id", "")),
sanitize(ph.get("name", ""))
)
)

print("\n🚀 Starting task import...\n")

# === Step 8: Main Import Loop ===
for index, row in df.iterrows():
row_num = index + 1
try:
# === CLIENT ===
client_name = sanitize(row.get("client_name"))
client_id = None
if client_name:
client_key = client_name.lower()
if client_key in clients_map:
client_id = clients_map[client_key]["client_id"]
else:
client_payload = {"name": client_name}
if DRY_RUN:
print(f"[Dry-run] Creating client: {client_payload}")
client_id = 999000 + row_num
else:
resp = requests.post(CLIENTS_API_URL, json=client_payload, headers=HEADERS)
if resp.ok:
client = resp.json()
client_id = client["client_id"]
clients_map[client_key] = client
clients_map_rev[str(client_id)] = client_name
else:
raise Exception(f"POST clients failed: {resp.status_code} - {resp.text}")

# === PROJECT ===
proj_key = project_key_from_csv(row)
project_id = None
if proj_key in projects_map:
project_id = projects_map[proj_key]["project_id"]
else:
auto_color = generate_color(proj_key)

project_payload = {
"name": sanitize(row.get("project_name")),
"project_code": sanitize(row.get("project_code")) or None,
"client_id": client_id if client_id else None,
"notes": sanitize(row.get("project_notes")),
"color": auto_color,
# Optional project-level fields:
"rate_type": sanitize(row.get("rate_type")) or None,
"default_hourly_rate": float(row.get("project_default_hourly_rate"))
if not pd.isna(row.get("project_default_hourly_rate")) else None,
"budget_type": int(row.get("budget_type")) if not pd.isna(row.get("budget_type")) else None,
"budget_total": float(row.get("budget_total")) if not pd.isna(row.get("budget_total")) else None,
"budget_priority": int(row.get("budget_priority")) if not pd.isna(row.get("budget_priority")) else None,
"status": int(row.get("status")) if not pd.isna(row.get("status")) else None,
"non_billable": int(row.get("non_billable")) if not pd.isna(row.get("non_billable")) else None,
}

if pd.notna(row.get("project_tags")):
tags = [t.strip() for t in str(row["project_tags"]).split(",") if t.strip()]
if tags:
project_payload["tags"] = tags

project_payload = {k: v for k, v in project_payload.items() if v is not None}

if DRY_RUN:
print(f"[Dry-run] Creating project: {project_payload}")
project_id = 888000 + row_num
else:
resp = requests.post(PROJECTS_API_URL, json=project_payload, headers=HEADERS)
if resp.ok:
project = resp.json()
project_id = project["project_id"]
projects_map[proj_key] = project
else:
raise Exception(f"POST projects failed: {resp.status_code} - {resp.text}")

# === PHASE ===
phase_name = sanitize(row.get("phase_name"))
phase_id = None
if phase_name:
phase_start = sanitize(row.get("phase_start_date"))
phase_end = sanitize(row.get("phase_end_date"))
phase_budget = row.get("phase_budget_total")
phase_status = row.get("phase_status")

phase_lookup = phase_key(client_id, project_id, phase_name)
if phase_lookup in phases_map:
phase_id = phases_map[phase_lookup]["phase_id"]
else:
phase_payload = {
"name": phase_name,
"project_id": project_id,
"start_date": phase_start,
"end_date": phase_end,
"status": int(phase_status) if not pd.isna(phase_status) else None,
}

if not pd.isna(phase_budget):
phase_payload["budget_total"] = float(phase_budget)

phase_payload = {k: v for k, v in phase_payload.items() if v is not None}

if DRY_RUN:
print(f"[Dry-run] Creating phase: {phase_payload}")
phase_id = 777000 + row_num
else:
resp = requests.post(PHASES_API_URL, json=phase_payload, headers=HEADERS)
if resp.ok:
phase = resp.json()
phase_id = phase["phase_id"]
phases_map[phase_lookup] = phase
else:
raise Exception(f"POST phases failed: {resp.status_code} - {resp.text}")

# === PERSON ===
person_name = sanitize(row.get("person_name"))
person_id = None
if person_name:
person_key = person_name.lower()
if person_key in people_map:
person_id = people_map[person_key]["people_id"]
else:
person_payload = {"name": person_name}

if not pd.isna(row.get("default_hourly_rate")):
person_payload["default_hourly_rate"] = float(row["default_hourly_rate"])

# Optional person start_date
if not pd.isna(row.get("person_start_date")):
person_payload["start_date"] = sanitize(row.get("person_start_date"))

if pd.notna(row.get("people_tags")):
tags = [t.strip() for t in str(row["people_tags"]).split(",") if t.strip()]
if tags:
person_payload["tags"] = [{"name": tag} for tag in tags]

person_payload = {k: v for k, v in person_payload.items() if v is not None}

if DRY_RUN:
print(f"[Dry-run] Creating person: {person_payload}")
person_id = 666000 + row_num
else:
resp = requests.post(PEOPLE_API_URL, json=person_payload, headers=HEADERS)
if resp.ok:
person = resp.json()
person_id = person["people_id"]
people_map[person_key] = person
else:
raise Exception(f"POST people failed: {resp.status_code} - {resp.text}")

# === TASK ===
task_notes = sanitize(row.get("task_notes"))
repeat_state = row.get("repeat_state")
repeat_end_date = sanitize(row.get("repeat_end_date"))

task_payload = {
"name": sanitize(row.get("task_name")),
"project_id": project_id,
"phase_id": phase_id,
"people_id": person_id,
"start_date": sanitize(row.get("start_date")),
"end_date": sanitize(row.get("end_date")),
"hours": float(row.get("hours")) if not pd.isna(row.get("hours")) else 0.0,
"notes": task_notes or None,
"repeat_state": int(repeat_state) if not pd.isna(repeat_state) else None,
"repeat_end_date": repeat_end_date or None,
}

task_payload = {k: v for k, v in task_payload.items() if v is not None}

if DRY_RUN:
print(f"[Dry-run] Creating task: {task_payload}")
else:
resp = requests.post(TASKS_API_URL, json=task_payload, headers=HEADERS)
if resp.ok:
if SHOW_SUCCESS_OUTPUT:
print(f"✅ Task created for Row {row_num}")
else:
raise Exception(f"POST tasks failed: {resp.status_code} - {resp.text}")

time.sleep(0.6)

except Exception as e:
error_msg = str(e)
print(f"❌ Error in Row {row_num}: {error_msg}")
log_error(row_num, error_msg)

# === Step 9: Wrap up ===
print("\n🎉 Import completed.\n")
print(f"📝 Errors (if any) logged to: {ERROR_LOG_FILE}")

# Append completion timestamps to error log
with open(ERROR_LOG_FILE, "a") as f:
f.write(
"\nImport completed: "
f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} (local), "
f"{datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC\n"
)

🏁 Run the Import

  1. Place your CSV and Python script in the same folder.

  2. Open Terminal or Command Prompt

python3 your_import_script.py

Use DRY_RUN = True for safe preview mode. Set to False to execute.


🚫 Handling Errors

  • The script logs clear error messages per row

  • Missing fields, bad dates, or duplicate project codes will be flagged

  • NaN or empty values are automatically sanitized, so nothing invalid is sent


🔗 Float API Documentation

For more technical details, check out our API reference:
👉 https://developer.float.com/


💬 Need Help?

If you have any questions or need help please reach out to support@float.com

Did this answer your question?