Clicktag docs
Imports API
The Clicktag Imports API copies historical pageviews and events from Simple Analytics into a site in monthly background chunks, up to 1,826 days per job. Starting a job needs the site owner's Firebase ID token, and each owner can start 20 per rolling 24 hours.
Base URL: https://clicktag.io. Download OpenAPI or read this page as Markdown.
Access
Starting, canceling and retrying imports need a Firebase ID token in Authorization: Bearer <token>. Sign in, open Authentication, and use Copy access token. Reading sources and jobs also works with a management key that has the read scope; on the POST routes a management key gets 403 management keys cannot call this endpoint. Site API keys (tt_...) do not work here; SimpleAnalytics credentials are source configuration, not Clicktag authentication.
GET /api/import-sources accepts any signed-in account or management key with read. Every site-specific import endpoint requires the site's owner, even for public sites. Authentication is checked before the site lookup. Creating an import also requires domain verification; listing, inspecting, canceling and retrying jobs do not add a verification gate.
| Method | Route | Result |
|---|---|---|
| GET | /api/import-sources |
Supported sources and configuration fields |
| POST | /api/sites/{hostname}/imports |
Validate, create and queue a job |
| GET | /api/sites/{hostname}/imports |
Latest 50 jobs for the site |
| GET | /api/sites/{hostname}/imports/{jobId} |
One job's current state |
| POST | /api/sites/{hostname}/imports/{jobId}/cancel |
Cancel a queued or running job |
| POST | /api/sites/{hostname}/imports/{jobId}/retry |
Re-queue a failed or canceled job |
Responses are JSON with Cache-Control: no-store. Product API responses have no cross-origin CORS headers; call from a server or the Clicktag origin. Use the normalized destination hostname returned by the Sites API; path lookups do not lowercase it.
TypeScript examples run server-side on Node 20+: save a snippet as example.mts, set its environment variables, then run npx tsx example.mts. No example automatically retries a POST.
Available sources
GET /api/import-sources returns { "sources": [...] }. Each source has id, label, description and configFields. A configuration field has key, label, type (text, password or date), required, and optional placeholder and help strings.
The current source is simpleanalytics, labeled SimpleAnalytics:
| Configuration key | Type | Required | Meaning |
|---|---|---|---|
user_id |
text | Yes | User ID from SimpleAnalytics dashboard → Account → API |
api_key |
password | Yes | SimpleAnalytics API key |
source_hostname |
text | Yes | Hostname registered in SimpleAnalytics; may differ from the destination |
Start an import
POST /api/sites/{hostname}/imports requires source, config, start and end. Set source to simpleanalytics and supply all three configuration strings above. Strings are trimmed and truncated to 512 characters; unknown configuration keys are ignored. source_hostname is lowercased and must be a hostname containing a dot, without a scheme, port or path.
Dates are inclusive UTC dates in YYYY-MM-DD format. start must be on or after 2010-01-01, start <= end, and the difference end - start cannot exceed 1,826 days. There is no future-end cutoff. The dates are checked before the rest of the body, so a missing or non-JSON body also returns invalid date range.
The planner preserves the requested date boundaries. Each month has two chunks: pageviews, then events; the first chunk starts on start and the final month ends on end.
Before queueing, creation checks the daily limit and the one-active-job rule, then tests the source credentials with one request for a recent pageview export. The test gives up after 20 seconds, and a failed test returns 400. 201 means queued, not import finished. Check job progress and actual reports before considering the migration complete.
Read credentials without putting them in shell history. Export EA_HOSTNAME for your registered destination, SA_HOSTNAME for the source, and EA_IMPORT_START / EA_IMPORT_END for your chosen dates. These examples create a real job when used with valid credentials.
read -rsp "Clicktag access token: " TT_TOKEN; printf '\n'
read -rsp "SimpleAnalytics user ID: " SA_USER_ID; printf '\n'
read -rsp "SimpleAnalytics API key: " SA_API_KEY; printf '\n'
export TT_TOKEN SA_USER_ID SA_API_KEYThe curl example requires Bash and jq; jq --arg safely encodes values, including quotes in credentials.
set -o pipefail
jq -n \
--arg user_id "${SA_USER_ID:?Set SA_USER_ID}" \
--arg api_key "${SA_API_KEY:?Set SA_API_KEY}" \
--arg source_hostname "${SA_HOSTNAME:?Set SA_HOSTNAME}" \
--arg start "${EA_IMPORT_START:?Set EA_IMPORT_START}" \
--arg end "${EA_IMPORT_END:?Set EA_IMPORT_END}" \
'{source:"simpleanalytics",config:{user_id:$user_id,api_key:$api_key,source_hostname:$source_hostname},start:$start,end:$end}' |
curl --fail-with-body --silent --show-error --max-time 300 \
-X POST "https://clicktag.io/api/sites/${EA_HOSTNAME:?Set EA_HOSTNAME}/imports" \
-H "Authorization: Bearer ${TT_TOKEN:?Copy an access token}" \
-H 'Content-Type: application/json' --data-binary @-const {
TT_TOKEN,
EA_HOSTNAME,
SA_USER_ID,
SA_API_KEY,
SA_HOSTNAME,
EA_IMPORT_START,
EA_IMPORT_END,
} = process.env;
if (
!TT_TOKEN ||
!EA_HOSTNAME ||
!SA_USER_ID ||
!SA_API_KEY ||
!SA_HOSTNAME ||
!EA_IMPORT_START ||
!EA_IMPORT_END
) {
throw new Error(
"Set the token, destination, source credentials, hostname and dates",
);
}
const response = await fetch(
`https://clicktag.io/api/sites/${encodeURIComponent(EA_HOSTNAME)}/imports`,
{
method: "POST",
headers: {
Authorization: `Bearer ${TT_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
source: "simpleanalytics",
config: {
user_id: SA_USER_ID,
api_key: SA_API_KEY,
source_hostname: SA_HOSTNAME,
},
start: EA_IMPORT_START,
end: EA_IMPORT_END,
}),
signal: AbortSignal.timeout(300_000),
},
);
if (!response.ok) {
throw new Error(`Create import ${response.status}: ${await response.text()}`);
}
console.log(await response.json());The response is the job object directly. Save its id as EA_IMPORT_ID to read progress. At most 20 new jobs per owner can be created in a rolling 24-hour window across all sites and statuses; the limit returns 429. Only one queued or running job may exist per site; another creation returns 409. Retrying the same job is not a new creation.
Import creation has no request idempotency key. The sink preserves stable SA source IDs and skips existing source records across imports; overlapping live TT data has unrelated IDs and still requires a deliberate replacement cutoff. After a timeout or ambiguous response, inspect the site's jobs before attempting another POST.
Read progress
GET /api/sites/{hostname}/imports returns { "imports": [...] }, ordered by created_at descending, with at most 50 entries. There are no pagination, cursor or offset parameters.
GET /api/sites/{hostname}/imports/{jobId} returns the job object directly. To list jobs instead, remove /{jobId} from the request below. Source discovery uses the same bearer header with /api/import-sources.
curl --fail-with-body --silent --show-error --max-time 20 \
-H "Authorization: Bearer ${TT_TOKEN:?Copy an access token}" \
"https://clicktag.io/api/sites/${EA_HOSTNAME:?Set EA_HOSTNAME}/imports/${EA_IMPORT_ID:?Set EA_IMPORT_ID}"const { TT_TOKEN, EA_HOSTNAME, EA_IMPORT_ID } = process.env;
if (!TT_TOKEN || !EA_HOSTNAME || !EA_IMPORT_ID) {
throw new Error("Set TT_TOKEN, EA_HOSTNAME and EA_IMPORT_ID");
}
const response = await fetch(
`https://clicktag.io/api/sites/${encodeURIComponent(EA_HOSTNAME)}/imports/${encodeURIComponent(EA_IMPORT_ID)}`,
{
headers: { Authorization: `Bearer ${TT_TOKEN}` },
signal: AbortSignal.timeout(20_000),
},
);
if (!response.ok) {
throw new Error(`Import ${response.status}: ${await response.text()}`);
}
console.log(await response.json());Job object
Create, get, cancel and retry return this object. List entries use the same shape; owner_uid and updated_at are never returned.
| Field | Type | Meaning |
|---|---|---|
id |
string | UUID job identifier |
site |
string | Destination Clicktag hostname |
source |
string | Source ID, currently simpleanalytics |
source_label |
string | Source display label, currently SimpleAnalytics |
status |
string | queued, running, completed, failed or canceled |
range_start |
string | Requested inclusive start date, YYYY-MM-DD |
range_end |
string | Requested inclusive end date, YYYY-MM-DD |
chunks_total |
integer | Planned pageview and event chunks |
chunks_done |
integer | Fully checkpointed chunks |
rows_imported |
number | Records of checkpointed chunks sent to storage, including ones already stored: pageviews, events and duration/scroll append rows |
rows_skipped |
number | Checkpointed source records skipped for another hostname or an event without a name |
error |
string or null | Latest worker error, up to 500 characters; cleared when a chunk succeeds |
config |
object | Non-password fields: user_id and source_hostname; never api_key |
created_at |
string | ISO date-time of creation |
finished_at |
string or null | ISO date-time of completion, failure or cancellation; normally null while active |
Request and returned range dates use YYYY-MM-DD, for example 2026-08-01. Progress updates only after a whole chunk finishes. Partial writes may not appear in counters, and repeated source rows can be skipped at insertion: these counters are neither unique pageviews nor proof of exact storage totals.
A job may have an error while still queued or running as the worker retries: each chunk is tried up to 4 times, about a minute apart, before the job fails with the last error. A rejected API key, or any other SimpleAnalytics 4xx except 429, fails the job right away. Imported pageviews count toward visitors through their exported is_unique flags; no synthetic visitor identities are invented. Duration and scroll attach to their original pageview and inherit its date and bot status. Browser and OS labels use the same normalization as live collection; imported country retains SA's timezone-based value.
Cancel or retry
Both actions accept a POST with no body and return 200 with the job object.
| Action | Allowed state | Effect |
|---|---|---|
/cancel |
queued or running |
Sets status to canceled |
/retry |
failed or canceled |
Sets status to queued, clears the error and resumes from chunks_done |
The examples cancel a job. To retry an eligible job, replace /cancel with /retry after checking its state.
curl --fail-with-body --silent --show-error --max-time 20 \
-X POST \
-H "Authorization: Bearer ${TT_TOKEN:?Copy an access token}" \
"https://clicktag.io/api/sites/${EA_HOSTNAME:?Set EA_HOSTNAME}/imports/${EA_IMPORT_ID:?Set EA_IMPORT_ID}/cancel"const { TT_TOKEN, EA_HOSTNAME, EA_IMPORT_ID } = process.env;
if (!TT_TOKEN || !EA_HOSTNAME || !EA_IMPORT_ID) {
throw new Error("Set TT_TOKEN, EA_HOSTNAME and EA_IMPORT_ID");
}
const response = await fetch(
`https://clicktag.io/api/sites/${encodeURIComponent(EA_HOSTNAME)}/imports/${encodeURIComponent(EA_IMPORT_ID)}/cancel`,
{
method: "POST",
headers: { Authorization: `Bearer ${TT_TOKEN}` },
signal: AbortSignal.timeout(20_000),
},
);
if (!response.ok) {
throw new Error(`Cancel import ${response.status}: ${await response.text()}`);
}
console.log(await response.json());Cancellation does not roll back rows. A chunk still being exported is discarded; a chunk already being written finishes first, and the cancel request waits for it. A retry skips checkpointed chunks; the sink also skips stable SA records already inserted by a partial attempt. This does not reconcile unrelated live collector IDs or restore interrupted job counters exactly. It does not accept replacement credentials or dates; completed jobs cannot be retried. Failed and canceled jobs keep their api_key for 7 days, then it is deleted and retry returns 409; start a new import instead.
Cancel and retry responses use the prior job snapshot with status overrides, so finished_at and the counters can be stale. Poll the GET endpoint for the persisted state.
Errors and credentials
Errors use { "error": "message" }.
| Status | Message or condition |
|---|---|
| 400 | invalid date range, unknown import source, a missing field such as User ID is required, Source hostname is invalid, or a failed credential test such as SimpleAnalytics returned 401: <response text> |
| 401 | sign in required: missing, invalid or expired token; invalid or revoked management key |
| 403 | Install your tracking script to verify this site first: creating an import requires a verified site; management keys cannot call this endpoint on the POST routes; scope read required for a key without read |
| 404 | unknown site: hostname is absent or not owned by the caller; unknown import: job is absent or belongs to another site |
| 409 | an import is already running for this site on create or retry, import is not active for cancel, only failed or canceled imports can be retried, or The API key for this import was deleted. Start a new import. for a retry after 7 days |
| 429 | too many imports today, try again tomorrow: rolling 24-hour creation limit reached |
| 500 | internal error |
| 502 | import queue unavailable, please try again: the job could not be queued. Creation cancels the new job, which still counts toward the daily limit and can be retried; retry marks the job failed with error import queue unavailable |
Password configuration fields are never echoed in the job's config. Completion removes the stored api_key. Failed and canceled jobs keep it for 7 days so they can be retried, then it is deleted. The non-password fields remain visible. Error text may include upstream response snippets and is not generally secret-redacted.
After an enqueue error, check job state before taking another action. Compare actual data with the Stats API; use the SimpleAnalytics migration guide to plan the wider cutover.