Skip to main content

Intent Upload API

Push account intent signals into Factors from any external system.​

1. What this API does

The Intent Upload API lets any system you own — a data warehouse export, a third-party intent provider, a webhook from your marketing automation, or even a one-off script — push account-level intent signals into a Factors project. No one has to open the Factors UI for it to work.

The recommended and current endpoints use the /new paths, allowing granular configuration of specific intent event names and contact identities.

Each signal answers four questions:

  • When did it happen?date_of_intent

  • What kind of signal was it?intent_source (e.g., "G2 Visit", "LinkedIn Ad View")

  • What event was performed?intent_name (becomes the specific event name written in Factors)

  • Who did it relate to? — Identity via company_domain and/or email_id (at least one is required)

You can attach any number of extra attributes — industry, employee count, region, or custom campaign tags. In V1, these extra fields are processed and applied as account/domain properties rather than properties on the intent event itself.

Mental Model: Think of this API as: "write me an intent event named intent_name on this account (and optionally this email/user), dated this day, from this source." The company domain can be supplied directly or automatically derived from the provided email address.

2. Two endpoints, same outcome

There are two ways to send the data. Internally, Factors treats them identically and runs them through the same enrichment job — pick whichever is easier for your source system to emit.

Feature

CSV Endpoint

JSON Endpoint

When to use

You already have a CSV: a manual export, a file dropped in S3, or a spreadsheet from a vendor.

Your system emits row objects: webhooks, programmatic integrations, or Zapier/n8n/Workato flows.

Path

POST /open/v1/intent/uploadcsv/new

POST /open/v1/intent/upload/new

Body

JSON wrapper containing base64 CSV bytes plus a metadata block mapping columns to fixed fields.

Plain JSON list of row objects with fixed key names. No metadata block required.

Custom attributes

Listed explicitly in metadata.additional_properties.

Any key beyond the fixed identity and required fields is auto-treated as a custom attribute.

3. Using it as a webhook destination

The JSON endpoint was designed to be webhook-friendly:

  • One row per request is fully supported — just send "data": [{ ... one row }].

  • Most webhook senders (Zapier, n8n, Workato, or in-house code) can post a JSON body to a URL with a single header. That's all this endpoint needs.

  • No signing secret, no callback handshake, and no setup on the Factors side — point the webhook at the /open/v1/intent/upload/new URL and start sending.

Volume and Budget Guidance:

  • Batching: Each request creates a separate stored upload internally. High cadences (more than a handful of requests per minute per project) waste storage and slow down enrichment. For high-volume sources, batch on your side and collect intent events over a few minutes to post them as a single request.

  • Column Budget: A single request can carry up to 10,000 rows and up to 22 custom properties beyond the fixed fields (date_of_intent, intent_source, intent_name, and identity via company_domain and/or email_id).

  • De-duplication: There is no internal de-duplication; re-sending the same row creates another intent event. Configure your webhook to fire once per signal.

4. Getting your API token

Every request needs an API token. The token identifies which Factors project the data belongs to, and who is uploading it.

Where to find it in Factors

  1. Open Factors and ensure the project switcher (top-left) is on the project you want to upload into.

  2. Go to SettingsIntegrations.

  3. In the "Data to Factors" category, open the card named Factors API.

  4. If no key exists yet, click Generate API Key. If one already exists, click Copy API Key.

  5. Store the key securely (a secrets manager or environment variable). Anyone holding it can write intent data to your project.

Rotating a key: If a key is ever leaked, the same page has a Regenerate API Key button. Regenerating immediately invalidates the previous key; you'll need to roll the new one to every system using it.

5. Authentication

Send the token as a Bearer header on every request:

Authorization: Bearer <your-api-key> Content-Type: application/json

Server-Side Rules and Privileges

  • The token is matched against the project-agent token table. A valid token resolves to a project_id and the agent_uuid of the user who generated it.

  • Privilege Guardrail: The uploading agent must possess the Real-Time Alerts write privilege (PrivRealTimeAlertsWrite). Requests submitted by an agent without this privilege will be rejected by the privilege middleware.

  • Feature Gate: The project must have the Event-Based Alerts feature enabled. If it is not, the request is rejected by a feature-gate middleware.

  • Missing, malformed, or revoked tokens are rejected with a 401 Unauthorized before the upload handler runs.

6. JSON endpoint — request body

Path: POST /open/v1/intent/upload/new

Example Body

{
"file_name":"intent_upload.json",
"data":[
{
"date_of_intent":"11/20/2025",
"intent_source":"G2 Visit",
"intent_name":"Product Interest",
"company_domain":"acme.com",
"industry":"SaaS",
"employee_count":"500-1000"
},
{
"date_of_intent":"11/21/2025",
"intent_source":"LinkedIn Ad View",
"intent_name":"Ad Engagement",
"email_id":"user@example.com",
"industry":"FinTech",
"region":"US"
}
]
}

Note: file_name is optional. If omitted, the server defaults to a reference name such as intent_upload.csv for storage.

Field Table

Field

Required

Description

data

Yes

Array of intent row objects. 1 to 10,000 rows per request.

file_name

No

Optional label for the upload; used to build the internal file_reference.

data[].date_of_intent

Yes

Date the intent signal occurred. Format: MM/DD/YYYY.

data[].intent_source

Yes

Intent source/category (free-form string). Stored as account property $intent_source.

data[].intent_name

Yes

The event name created and tracked in Factors for this row.

data[].company_domain

Conditional

Plain domain (e.g., acme.com), no protocol or path. Required if email_id is missing or empty.

data[].email_id

Conditional

Valid contact email address. Required if company_domain is missing or empty. If the domain is omitted, it is derived from the email string (user@acme.comacme.com).

data[].<any other key>

No

Custom attribute → stored as an account/domain property (not an event property). Max 22 distinct keys.

Identity Rule

For each individual row, either email_id or company_domain (or both) must be provided. If both are missing, the endpoint returns a 400 Bad Request containing an error message like: either "email_id" or "company_domain" is required in row N.

How non-string JSON values are handled

  • String: Unchanged

  • Number: Converted to its string representation

  • Boolean: Converted to "true" or "false"

  • null: Stored as an empty string for custom properties; rejected if passed into required fields

  • Array or object: Serialized into a compact JSON string

7. CSV endpoint — request body

Path: POST /open/v1/intent/uploadcsv/new

Example Body

{
"file_name":"intent_upload.csv",
"payload":"<base64-encoded CSV bytes>",
"metadata":{
"date_intent":"Date of Intent",
"intent_type":"Intent Source",
"intent_name":"Intent Name",
"domain":"Company Domain",
"email_id":"Email Id",
"additional_properties":[
"Industry",
"Employee Count"
]
}
}

Field Table

Field

Required

Description

file_name

Yes

Original filename. Must end in .csv. Used to generate the internal storage reference.

payload

Yes

Raw CSV bytes, encoded as a standard base64 string.

metadata.date_intent

Yes

The header name in your CSV that holds the intent date.

metadata.intent_type

Yes

The header name in your CSV that holds the intent source.

metadata.intent_name

Yes

The header name in your CSV that holds the intent name (the event name).

metadata.domain

Conditional

The header name for the company domain column. Optional only if email_id is mapped and present.

metadata.email_id

Conditional

The header name for the user email column. Optional only if domain is mapped and present.

metadata.additional_properties

No

Header names from the CSV to preserve as custom properties. Maximum of 22.

Matching CSV for the Example Above

Date of Intent,Intent Source,Intent Name,Company Domain,Email Id,Industry,Employee Count 11/20/2025,G2 Visit,Product Interest,acme.com,,SaaS,500-1000 11/21/2025,LinkedIn Ad View,Ad Engagement,,user@example.com,FinTech,100-500

CSV Identity & Column Rules

  1. At least one identity column (Company Domain or Email Id) must exist based on your metadata mapping. If neither is defined in the metadata block, the request fails with a 400: either "Email Id" or "Company Domain" column must be present in CSV header

  2. Within the data rows, every row must contain at least one valid domain or email. If a row leaves both identity cells empty, a 400 error is returned: either "Email Id" or "Company Domain" is required in row N (Note: Row numbers are 1-indexed including the header, meaning the first data row is evaluated as row 2.)

  3. If the domain cell is empty but the email is present, Factors automatically derives the domain from the email value during processing.

  4. If only one identity column is provided in the uploaded file, the missing identity column may be auto-added during system processing (e.g., an email-only file will generate a domain column mapped from those emails).

  5. The mapped intent_name column must exist, and every row within it must contain a non-empty string value.

8. Limits & validation

These core validation principles apply universally to both endpoints.

Rule

Limit / Format Description

Maximum rows per request

10,000

Maximum custom properties

22 (beyond the core fixed fields)

Required fields

date_of_intent, intent_source, intent_name, and identity via (company_domain and/or email_id)

Date format

MM/DD/YYYY (e.g., 11/20/2025)

Domain format

Plain domain: no protocols (http://), no paths, no trailing slashes, and no whitespace.

Email format

Must be a structurally valid email string when provided. Invalid formats trigger a 400 error.

Empty request

Rejected immediately.

9. Response & errors

Both endpoints return the identical schema structure upon successful upload registration:

{   "file_reference": "f1f9a3c0-2d3e-4f5a-9b01-123456789abc_intent_upload_csv" }

The file_reference is the explicit token visible in the intent upload logs inside Factors and is referenced by status check utilities.

Error Messages Matrix

Status

Body / Error Excerpt

Meaning / Resolution

401

{"error":"private token not provided."}

Missing Authorization header.

401

{"error":"invalid private token format."}

Header present but not structured in Bearer <token> syntax.

401

{"error":"invalid private token."}

Token is unrecognized, mismatched, or revoked.

400

Failed to decode payload

Malformed JSON body submitted to the endpoint.

400

The file uploaded is invalid…

Completely empty data array or empty CSV structure payload.

400

invalid csv file: row limit exceed

Request payload contained greater than 10,000 records.

400

invalid csv file: columns limit exceed

Exceeded 22 custom properties or missing crucial required metadata parameters.

400

missing required attribute "intent_name" in row N

The mandatory event identifier intent_name was blank (JSON context).

400

missing required attribute "date_of_intent" in row N

Missing date entry on row N (JSON context).

400

missing required attribute "intent_source" in row N

Missing source string category on row N (JSON context).

400

either "email_id" or "company_domain" is required in row N

Neither domain nor email context was provided on row N (JSON context).

400

either "Email Id" or "Company Domain" is required in row N

Neither identity column had content on row N (CSV context).

400

either "Email Id" or "Company Domain" column must be present…

The metadata definition omitted both identity mappings (CSV context).

400

invalid email "…" in row N

Mismatched structure/invalid email text encountered during identity parsing.

400

{"message":"Invalid CSV","error_list":[...]}

Internal cell formatting failures (such as structural domain anomalies).

10. Worked examples

10.1 cURL — JSON Endpoint (Recommended for Webhooks)

curl -X POST "https://api.factors.ai/open/v1/intent/upload/new" -H "Authorization: Bearer
${FACTORS_API_KEY}" -H "Content-Type: application/json" -d '{
"data": [
{
"date_of_intent": "11/20/2025",
"intent_source": "G2 Visit",
"intent_name": "Product Interest",
"company_domain": "acme.com",
"industry": "SaaS"
}
]
}'

Email-Only Alternate Payload Example

{
"data":[
{
"date_of_intent":"11/20/2025",
"intent_source":"G2 Visit",
"intent_name":"Product Interest",
"email_id":"user@acme.com"
}
]
}

10.2 cURL — CSV Endpoint

CSV_BASE64=$(base64 < intent_upload.csv | tr -d '
')
curl -X POST "https://api.factors.ai/open/v1/intent/uploadcsv/new" -H "Authorization:
Bearer ${FACTORS_API_KEY}" -H "Content-Type: application/json" -d "{
"file_name": "intent_upload.csv",
"payload": "${CSV_BASE64}",
"metadata": {
"date_intent": "Date of Intent",
"intent_type": "Intent Source",
"intent_name": "Intent Name",
"domain": "Company Domain",
"email_id": "Email Id",
"additional_properties": ["Industry", "Employee Count"]
}
}"

10.3 JavaScript (Webhook Handler Context)

await fetch("https://api.factors.ai/open/v1/intent/upload/new",
{
"method":"POST",
"headers":{
"Authorization":"`Bearer ${process.env.FACTORS_API_KEY}`",
"Content-Type":"application/json"
},
"body":"JSON.stringify("{
"data":[
{
"date_of_intent":"11/20/2025",
"intent_source":"G2 Visit",
"intent_name":"Product Interest",
"company_domain":"acme.com",
"industry":"SaaS"
}
]
}")"
});

11. What happens after a successful upload

  1. Factors validates the format, stores the upload archive safely, and instantly responds back with a tracking file_reference.

  2. The asynchronous background intent enrichment worker detects the file package on its upcoming routine loop and cycles through each entry row by row.

  3. Identity Resolution: The account match framework locates (or provisions) the corresponding target company account based on the configured company_domain. If company_domain was omitted in an upload row, it is dynamically derived from the supplied email_id.

  4. User & Contact Attribution: If an email_id is supplied in the row:

    • A contact/user identity is resolved or initialized for that email address.

    • This user profile is explicitly mapped to the corresponding company domain group.

    • The logged intent event is written directly to that individual user timeline (otherwise, if email is missing, it logs globally on the fallback account/domain user placeholder).

  5. Event Representation: The processed record event name matches your defined row value for intent_name, timestamped exactly at the declared date_of_intent (not the ingestion time).

  6. Property Processing (V1 Behavior):

    • The intent_source value is committed as a permanent domain/account-level property under the key name $intent_source.

    • All extra keys mapped or captured as custom attributes are written as domain/account properties, rather than attaching to the discrete event record.

  7. Once enrichment is complete, these events become visible inside account timelines, customer segment rules, alert conditions, and dashboard queries.

Processing Latency: Enrichment processing executes on a batch schedule, not in real-time. Plan for your events to show up in Factors search indexes within the normal cycle intervals of the processing engine rather than instantaneously upon receipt.

12. LinkedIn engagement events: icon mapping for intent_name

intent_name is a free-form string — any value you send is accepted and written as the event name in Factors (see §6/§7). Factors additionally recognizes a fixed set of LinkedIn engagement event names: if intent_name exactly matches one of the values below, Factors renders that event in the UI with a dedicated LinkedIn icon instead of the generic default. This is purely cosmetic, recognition only changes which icon is shown, never whether the row is accepted.

Recognized LinkedIn Post events

  • Linkedin Post Like

  • Linkedin Post Praise

  • Linkedin Post Appreciation

  • Linkedin Post Empathy

  • Linkedin Post Interest

  • Linkedin Post Entertainment

  • Linkedin Post Comment

  • Linkedin Post Create

Recognized LinkedIn Comment events

  • Linkedin Comment Like

  • Linkedin Comment Praise

  • Linkedin Comment Appreciation

  • Linkedin Comment Empathy

  • Linkedin Comment Interest

  • Linkedin Comment Entertainment

Any other intent_name — whether it's a typo of one of the values above, a value from a different source entirely (e.g. "G2 Comparison Page View"), or anything else you choose — is still accepted and stored exactly as sent; Factors just displays it with a generic default icon rather than a LinkedIn-specific one. intent_name is never restricted to this list; the list only controls icon choice.

13. Appendix: Legacy endpoints

The following old paths are maintained exclusively for backwards compatibility but are deprecated and not recommended for greenfield implementations.

Legacy Path

Format

Structural Differences

POST /open/v1/intent/upload

Old JSON

Always requires company_domain directly; does not support intent_name or email_id. All events are recorded as a fixed Third Party Intent Visit ($third_party_intent_visit). Custom attributes write directly to event schemas; source maps to $intent_category.

POST /open/v1/intent/uploadcsv

Old CSV

Metadata payload permits only date_intent, intent_type, and domain along with additional_properties. Lacks support for intent_name or email_id.

Did this answer your question?