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.

bash
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_KEY

The curl example requires Bash and jq; jq --arg safely encodes values, including quotes in credentials.

curl
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 @-
typescript
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
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}"
typescript
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
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"
typescript
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.