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_intentWhat 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_domainand/oremail_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 |
|
|
Body | JSON wrapper containing base64 CSV bytes plus a | Plain JSON list of row objects with fixed key names. No metadata block required. |
Custom attributes | Listed explicitly in | 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/newURL 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 viacompany_domainand/oremail_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
Open Factors and ensure the project switcher (top-left) is on the project you want to upload into.
Go to Settings → Integrations.
In the "Data to Factors" category, open the card named Factors API.
If no key exists yet, click Generate API Key. If one already exists, click Copy API Key.
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_idand theagent_uuidof 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 Unauthorizedbefore 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 |
| Yes | Array of intent row objects. 1 to 10,000 rows per request. |
| No | Optional label for the upload; used to build the internal |
| Yes | Date the intent signal occurred. Format: |
| Yes | Intent source/category (free-form string). Stored as account property |
| Yes | The event name created and tracked in Factors for this row. |
| Conditional | Plain domain (e.g., |
| Conditional | Valid contact email address. Required if |
| 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 |
| Yes | Original filename. Must end in |
| Yes | Raw CSV bytes, encoded as a standard base64 string. |
| Yes | The header name in your CSV that holds the intent date. |
| Yes | The header name in your CSV that holds the intent source. |
| Yes | The header name in your CSV that holds the intent name (the event name). |
| Conditional | The header name for the company domain column. Optional only if |
| Conditional | The header name for the user email column. Optional only if |
| 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
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 headerWithin the data rows, every row must contain at least one valid domain or email. If a row leaves both identity cells empty, a
400error 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.)If the domain cell is empty but the email is present, Factors automatically derives the domain from the email value during processing.
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).
The mapped
intent_namecolumn 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 format |
|
Domain format | Plain domain: no protocols ( |
Email format | Must be a structurally valid email string when provided. Invalid formats trigger a |
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 |
| Missing |
401 |
| Header present but not structured in |
401 |
| Token is unrecognized, mismatched, or revoked. |
400 |
| Malformed JSON body submitted to the endpoint. |
400 |
| Completely empty data array or empty CSV structure payload. |
400 |
| Request payload contained greater than 10,000 records. |
400 |
| Exceeded 22 custom properties or missing crucial required metadata parameters. |
400 |
| The mandatory event identifier |
400 |
| Missing date entry on row N (JSON context). |
400 |
| Missing source string category on row N (JSON context). |
400 |
| Neither domain nor email context was provided on row N (JSON context). |
400 |
| Neither identity column had content on row N (CSV context). |
400 |
| The metadata definition omitted both identity mappings (CSV context). |
400 |
| Mismatched structure/invalid email text encountered during identity parsing. |
400 |
| 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
Factors validates the format, stores the upload archive safely, and instantly responds back with a tracking
file_reference.The asynchronous background intent enrichment worker detects the file package on its upcoming routine loop and cycles through each entry row by row.
Identity Resolution: The account match framework locates (or provisions) the corresponding target company account based on the configured
company_domain. Ifcompany_domainwas omitted in an upload row, it is dynamically derived from the suppliedemail_id.User & Contact Attribution: If an
email_idis 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).
Event Representation: The processed record event name matches your defined row value for
intent_name, timestamped exactly at the declareddate_of_intent(not the ingestion time).Property Processing (V1 Behavior):
The
intent_sourcevalue 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.
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 LikeLinkedin Post PraiseLinkedin Post AppreciationLinkedin Post EmpathyLinkedin Post InterestLinkedin Post EntertainmentLinkedin Post CommentLinkedin Post Create
Recognized LinkedIn Comment events
Linkedin Comment LikeLinkedin Comment PraiseLinkedin Comment AppreciationLinkedin Comment EmpathyLinkedin Comment InterestLinkedin 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 |
| Old JSON | Always requires |
| Old CSV | Metadata payload permits only |
