400 or rejected template
Read the response body. Check that column_map_str is a JSON-encoded CSV-header-to-contact-field map, that it includes conflict_on, and that every mapped destination is valid and unique. Confirm that the template is active.
Public documentation
Create or update contacts from CSV files, control list membership through import templates, and verify each import. Use an ownership-only template when you need to reassign existing contacts.
Start here
This API handles contact imports and list membership for Trellus Calling. For call records, transcripts and reporting, use the Call Data API. To ask an AI assistant about your calls, use the MCP guide.
Use a server-side environment with Bash, curl and jq. Set your key privately and run the examples in the same Bash session so the returned connection, template and ingest variables stay available. Keep keys and returned secrets out of browser code, logs and chat.
export VCC_BASE_URL='https://vcc.trellus.ai'
export CSV_FILE='contacts.csv'
# Set TRELLUS_API_KEY privately in this server-side environment.
# Use a key belonging to an authorized team admin.
Send Authorization: Bearer $TRELLUS_API_KEY to https://vcc.trellus.ai/resolve-path. The response provides path and secret for subsequent requests. An optional x-team-id header selects a team you are already authorized to administer.
set -euo pipefail
: "${VCC_BASE_URL:?Set VCC_BASE_URL first}"
: "${TRELLUS_API_KEY:?Set TRELLUS_API_KEY privately first}"
WORKER_JSON=$(curl --fail-with-body --silent --show-error \
--request POST "$VCC_BASE_URL/resolve-path" \
--header "Authorization: Bearer $TRELLUS_API_KEY")
VCC_WORKER_URL=$(jq -er '.path | strings | select(length > 0)' <<<"$WORKER_JSON")
VCC_WORKER_SECRET=$(jq -er '.secret | strings | select(length > 0)' <<<"$WORKER_JSON")
# For an authorized team override, add this header to the resolver request:
# --header "x-team-id: $TEAM_ID"
# Keep the returned secret private.
At the returned URL, send the raw API key as Authorization: $TRELLUS_API_KEY and the returned secret as x-worker-secret. Keep that connection paired with the team for which it was resolved.
Template and ingest endpoints require team-admin access. Append their paths to the resolved URL. Use the exact returned presigned URL only for the CSV upload.
| Method and path | Destination | Result |
|---|---|---|
POST/resolve-path | Resolver | Get the authorized team’s request URL and secret. |
GET/upload-templates | Resolved URL | Read existing import templates. |
POST/upload-templates | Resolved URL | Create a template. The response is its template ID as plain text. |
POST/ready-ingest-upload | Resolved URL | Request an ingest ID and a presigned CSV upload URL. No request body. |
PUT<returned presigned URL> | Returned upload URL | Upload raw CSV bytes with the returned headers. Do not send Trellus credentials. |
POST/ingest-uploaded | Resolved URL | Queue the uploaded CSV with its ingest ID, type and template ID. HTTP 200 means queued. |
GET/ingests?limit=50&offset=0 | Resolved URL | Read recent imports and find the exact ingest ID to inspect its results. |
Choose an existing upload template or create one for the data you plan to send. CSV headers are yours: column_map_str maps each CSV header to one contact field. Its value is a JSON-encoded string inside the template JSON.
curl --fail-with-body --silent --show-error \
"$VCC_WORKER_URL/upload-templates" \
--header "Authorization: $TRELLUS_API_KEY" \
--header "x-worker-secret: $VCC_WORKER_SECRET"
# To reuse a template, set TEMPLATE_ID to its template_id.
Save this sample as contacts.csv and replace it with your intended contact data before uploading. Save the template as template.json. The sample matches on xref_contact_id and updates only a single matching existing contact.
external_id,first_name,last_name,phone,email
EXAMPLE-001,Ada,Lovelace,+12025550123,ada@example.invalid
{
"label": "Contact upsert — external ID",
"conflict_on": "xref_contact_id",
"column_map_str": "{\"external_id\":\"xref_contact_id\",\"first_name\":\"first_name\",\"last_name\":\"last_name\",\"phone\":\"number_1\",\"email\":\"email\"}",
"include_incomplete_parse": false,
"can_insert": true,
"can_update": true,
"update_resolver": "SINGLES",
"add_to_lists_str": null,
"remove_from_lists_str": null,
"list_resolver": null,
"add_resets_lists": false
}
contact_id, number_1, xref_contact_id and email. Include the chosen match field in the map.can_insert and can_update control whether the import can create contacts or update existing ones. SINGLES updates only when a match identifies one contact; FIRST updates the first match and ALL updates every matching contact.null to leave memberships unchanged.For list changes, add_to_lists_str and remove_from_lists_str contain JSON-encoded arrays of list IDs. A nonempty list change requires list_resolver. Existing-contact list changes currently follow update_resolver; setting list_resolver does not independently override that matching behavior.
add_resets_lists: true clears entire lists named in add_to_lists_str before processing rows. It is not a reset for only the contacts in your CSV. The examples keep it false.: "${VCC_WORKER_URL:?Resolve the team connection first}"
: "${VCC_WORKER_SECRET:?Resolve the team connection first}"
: "${TRELLUS_API_KEY:?Set TRELLUS_API_KEY privately first}"
# Save the template JSON above as template.json.
# The successful response is plain text, not a JSON object.
TEMPLATE_ID=$(curl --fail-with-body --silent --show-error \
--request POST "$VCC_WORKER_URL/upload-templates" \
--header "Authorization: $TRELLUS_API_KEY" \
--header "x-worker-secret: $VCC_WORKER_SECRET" \
--header 'Content-Type: application/json' \
--data-binary @template.json)
: "${TEMPLATE_ID:?The template response was empty}"
The successful template response is an ID as plain text. If you reuse a template, set TEMPLATE_ID to its existing template_id.
The ready response contains an ingest_id, ingest_type, upload url, method and headers. Use those returned values together. The current CSV upload uses PUT and Content-Type: text/csv; its presigned URL expires after five minutes.
Send raw file bytes to the upload URL without a Trellus authorization header or worker secret. Confirm only after that upload succeeds.
set -euo pipefail
: "${TRELLUS_API_KEY:?Set TRELLUS_API_KEY privately first}"
: "${VCC_WORKER_URL:?Resolve the team connection first}"
: "${VCC_WORKER_SECRET:?Resolve the team connection first}"
: "${TEMPLATE_ID:?Set TEMPLATE_ID first}"
: "${CSV_FILE:?Set CSV_FILE first}"
READY_JSON=$(curl --fail-with-body --silent --show-error \
--request POST "$VCC_WORKER_URL/ready-ingest-upload" \
--header "Authorization: $TRELLUS_API_KEY" \
--header "x-worker-secret: $VCC_WORKER_SECRET")
INGEST_ID=$(jq -er '.ingest_id | strings | select(length > 0)' <<<"$READY_JSON")
INGEST_TYPE=$(jq -er '.ingest_type | select(. == "UPLOADED_CSV")' <<<"$READY_JSON")
UPLOAD_URL=$(jq -er '.url | strings | select(length > 0)' <<<"$READY_JSON")
UPLOAD_METHOD=$(jq -er '.method | strings | select(length > 0)' <<<"$READY_JSON")
UPLOAD_CONTENT_TYPE=$(jq -er '.headers["Content-Type"] | strings | select(length > 0)' <<<"$READY_JSON")
# This request goes to the returned presigned URL.
# Do not send a Trellus API key or worker secret.
curl --fail-with-body --silent --show-error \
--request "$UPLOAD_METHOD" "$UPLOAD_URL" \
--header "Content-Type: $UPLOAD_CONTENT_TYPE" \
--data-binary "@$CSV_FILE"
# Confirm only after the upload succeeds.
jq -n \
--arg ingest_id "$INGEST_ID" \
--arg ingest_type "$INGEST_TYPE" \
--arg template_id "$TEMPLATE_ID" \
'{ingest_id: $ingest_id, ingest_type: $ingest_type, template_id: $template_id}' |
curl --fail-with-body --silent --show-error \
--request POST "$VCC_WORKER_URL/ingest-uploaded" \
--header "Authorization: $TRELLUS_API_KEY" \
--header "x-worker-secret: $VCC_WORKER_SECRET" \
--header 'Content-Type: application/json' \
--data-binary @-
Poll at most once every five seconds. Find the ingest_id returned by ready-upload. If it is not in the newest 50 results, increase OFFSET by 50 until you find it or reach the end of the history.
OFFSET=0
curl --fail-with-body --silent --show-error \
"$VCC_WORKER_URL/ingests?limit=50&offset=$OFFSET" \
--header "Authorization: $TRELLUS_API_KEY" \
--header "x-worker-secret: $VCC_WORKER_SECRET" |
jq --arg ingest_id "$INGEST_ID" '.[] | select(.ingest_id == $ingest_id)'
# Wait at least five seconds before polling again.
# If the ingest is not in this page, increase OFFSET by 50.
For a CSV import, wait for row_count, error_count and the other result statistics to be populated; zero is a valid value. processed_at is written before CSV processing starts. Read inserted, updated, skipped, list and error counts together.
| Field | How to read it |
|---|---|
ingest_id | The identifier returned by ready-ingest-upload. Match this exact value. |
row_count | Rows seen in the uploaded data. A populated value is part of the completion check; zero is a valid count. |
contact_insert_count / contact_update_count | Contacts inserted and updated. Read these alongside row and skip counts. |
skip_count | Rows skipped, including duplicate-key rows. A skip is not necessarily a parsing error. |
list_add_count / list_remove_count | List membership changes made by the template. |
error_count / error_sample_str | Error count and a JSON-encoded array of up to five truncated examples. |
created_at / processed_at | Unix microseconds. For CSV imports, processed_at records the processing start, not completion. Convert with new Date(value / 1000). |
Partial imports can change some rows while others fail or are skipped. Zero errors does not prove every row changed. error_sample_str contains a JSON-encoded array of up to five truncated examples to help identify mapping or parsing problems.
Use a separate ownership-only template when you want to update existing owners without inserting contacts or replacing list memberships. Prepare the intended contact-to-agent assignments in advance; the CSV is the reassignment plan.
{
"label": "Ownership updates — existing contacts only",
"conflict_on": "contact_id",
"column_map_str": "{\"contact_id\":\"contact_id\",\"owner_user_id\":\"owner_user_id\"}",
"include_incomplete_parse": false,
"can_insert": false,
"can_update": true,
"update_resolver": "SINGLES",
"add_to_lists_str": null,
"remove_from_lists_str": null,
"list_resolver": null,
"add_resets_lists": false
}
contact_id,owner_user_id
CONTACT_UUID,AGENT_UUID
/upload-templates, then use its ID as TEMPLATE_ID.CSV_FILE=ownership.csv and follow the same upload, confirmation and result checks above.owner_user_id values before upload when your intent is reassignment. Ownership belongs to the contact across campaigns, so the change can affect every campaign using that contact’s lists.This template does not clear DNC status, final outcomes, retry state, callback timestamps, list memberships or agent skill grants. Exclude DNC, final-outcome and manual-calendar-callback rows from your reassignment file when those records should retain their owners. An included callback keeps its callback time but receives the new owner.
Read the response body. Check that column_map_str is a JSON-encoded CSV-header-to-contact-field map, that it includes conflict_on, and that every mapped destination is valid and unique. Confirm that the template is active.
Check the key’s team-admin access and the selected team. Use Bearer authentication for resolve-path; the resolved URL takes the raw key in Authorization plus x-worker-secret. Selecting a team does not grant access to it.
Honor Retry-After before trying again. Do not continuously poll import history.
The presigned upload URL lasts five minutes. Request a fresh ready-ingest-upload response and use its new ingest ID, URL, method and headers together.
The confirmation only queues the import. Find its ingest ID and wait for row_count, error_count and the other result statistics to be populated. processed_at alone is not completion.
Compare inserted, updated and skipped counts. Duplicate match values and the selected conflict resolver can prevent a row from changing a contact.
If you need help, send Trellus support the endpoint, HTTP status, response text and relevant ingest or template ID. Keep API keys, worker secrets and contact details out of the message.