Bulk APIs and subscribe widget
Available on Starter plan and above (subscribe widget is free); bulk APIs require Starter+ for contacts scope
Ingest contacts at scale, sync tags from an external system, and embed a hosted subscribe form on any third-party site.
Bulk contact upsert
POST /api/contacts/bulk — designed for nightly syncs from a CRM, one-time migrations from another tool, and batch-imports of trade-show badge scans.
Request shape
POST /api/contacts/bulk
X-Api-Key: fli_...
Content-Type: application/json
{
"contacts": [
{ "email": "alice@example.com", "firstName": "Alice", "company": "Acme" },
{ "email": "bob@example.com", "firstName": "Bob", "jobTitle": "CTO" }
],
"tags": ["import:2026-q1", "newsletter"]
}
Behaviour
- Upsert by email — existing contacts are updated, new ones are created.
- Null fields = leave alone — partial updates from a CRM nightly sync don't accidentally clear fields that weren't pushed.
- Top-level
tags— applied platform-wide to every row in the batch (created or updated). Useful for stamping the whole import. - Hard cap 10,000 contacts per request — beyond that, the request pipeline starts to time out under realistic SQL load. Larger imports should chunk client-side.
- 500-row chunks internally — EF's change tracker stays bounded.
- One pre-load query — existing contacts are fetched in a single
WHERE Email IN (...)trip.
Response
{
"totalSubmitted": 2,
"created": 1,
"updated": 1,
"skipped": 0,
"errors": []
}
Per-row failures (validation errors etc.) appear in errors; the rest of the batch still processes.
Bulk tag add/remove
PATCH /api/contacts/bulk/tags — add or remove tags across many contacts in one call.
PATCH /api/contacts/bulk/tags
X-Api-Key: fli_...
Content-Type: application/json
{
"contactIds": ["...", "..."],
"emails": ["alice@example.com"],
"addTags": ["vip"],
"removeTags": ["churn-risk"]
}
- Match contacts by
contactIdsand / oremails(results unioned, owner-filtered server-side). addTagsand / orremoveTags— at least one must be non-empty.- The add path uses
EnsureTagsAsync(find-or-create the tag rows in one batch) + pre-filtered inserts to avoid noisy unique-constraint exceptions. - The remove path uses
ExecuteDeleteAsync— one DELETE per tag, not per row.
Response:
{ "matched": 247, "tagsAdded": 247, "tagsRemoved": 12 }
Subscribe widget
A hosted JavaScript widget you drop on any third-party site to collect newsletter signups directly into a contact group. No CSS conflicts, no build step.
Embed
<script src="https://flnk.it/js/subscribe-widget.js"
data-group="GROUP_ID"
data-theme="light"
data-show-name="true"
data-button-text="Subscribe"
data-success-message="Thanks for subscribing!"></script>
Data attributes
| Attribute | Default | Description |
|---|---|---|
data-group |
required | The contact-group GUID — get it from /contacts/groups/{id} URL bar |
data-theme |
light |
light or dark |
data-show-name |
false |
Add an optional name field |
data-button-text |
Subscribe | Button label |
data-success-message |
Thanks for subscribing! | Shown after a successful submit |
data-email-placeholder |
your@email.com | Email input placeholder |
data-name-placeholder |
Your name | Name input placeholder |
data-mount |
(auto) | CSS selector for an explicit mount point — defaults to a <div> inserted right after the script tag |
data-api |
https://flnk.it | Override for self-hosted FastLinkIt deploys |
How it works
- The widget reads its config from data-attrs on its own
<script>tag. - Renders a self-contained form with inline styles (won't fight host CSS).
- On submit, POSTs to
/api/subscribewithContent-Type: text/plainso the request stays a CORS "simple request" — no preflight needed. - Server side:
IContactService.UpsertFromSourceAsyncupserts the contact with asource:subscribe:<groupName>tag, fires thecontact.createdwebhook for new arrivals, and adds the contact to the group (idempotent). - Server validates the email shape and rate-limits 1 signup per (groupId, email) per 60 seconds.
- On success, the form swaps for a check-mark message; on error, shows the message inline and re-enables submit.
Where the GROUP_ID comes from
Go to /contacts/groups, click the group you want signups to land in. The URL is /contacts/groups/{id} — that GUID is what you put in data-group. Group GUIDs are 32 bytes of entropy — non-enumerable and safe to be public.
Activity API
GET /api/contacts/{id}/activity?skip=0&take=50 — returns the unified timeline aggregating mailings, payments, bookings, donations, tickets, and inbox messages for one contact. Same shape as the contact 360° UI.
Per-tenant authorisation: refuses if the contact doesn't belong to the caller (otherwise a guessed contact id could enumerate another tenant's history).
Worked example — nightly CRM sync
Goal: a Salesforce nightly export pushes 5,000 updated contacts into FastLinkIt at 2 AM.
- Set up the export job in Salesforce.
- Generate a FastLinkIt API key with
contactsandmailingscopes. - The export hits
POST /api/contacts/bulkwithtags: ["crm-sync:2026-05-14"]. - Response confirms 4,800 updated, 200 created, 0 errors.
- A morning report can run
GET /api/contacts/bulk/tags(PATCH for tag add) to set acrm-sync:currenttag on the imported batch and remove it from yesterday's batch. - Smart segments built on the
crm-sync:currenttag pick up exactly today's import.
Related
- Webhooks for contacts and mailing — listen for events from the upserts above
- Smart segments — segment over imported tags
- Custom contact fields — fields you can populate via bulk upsert
- API Keys — generate a key with the right scopes