FirstSpirit
The FirstSpirit loader imports content documents from the FirstSpirit CaaS (Content as a Service) platform into your warehouse. Each CaaS collection you select becomes its own table: the FirstSpirit system fields land as columns, and the complete JSON document lands in a data column you can unnest downstream.
Use it to bring released page, dataset and media metadata into the same warehouse as your customer data — so content can be joined to behaviour, modelled, and used in audiences and journeys.
This loader is the read-side counterpart of the FirstSpirit destination. Both use the same CaaS base URL, database and API key, so a single CaaS setup covers both directions.
Prerequisites
- A FirstSpirit CaaS Platform instance and its REST base URL (for FirstSpirit Cloud, your project’s CaaS endpoint).
- The CaaS database name — the tenant/project store that holds your content.
- The names of the CaaS collections you want to import. CaaS Connect creates them as
<project-uuid>.<state>.content(and<project-uuid>.<state>.filesfor media), so a typical value is<project-uuid>.release.content. - A CaaS API key whose permission allows GET on those collections (see Authentication).
- Content that has been published at least once from FirstSpirit — CaaS collections only exist after FirstSpirit has written into them.
- A connected warehouse to load the data into.
- CaaS Connect 3.83.9 or later is strongly recommended if you plan to use incremental sync. Earlier versions had a defect (CAAS-2536, fixed in 3.83.9) where the
changeInfofield on a CaaS document was not correctly updated even though the editorial content was current. Incremental sync keys offchangeInfo.revision, so on an older module a changed document can be missed. Check your module version with your FirstSpirit administrator, or use Always Full Refresh.
Authentication
Zeotap authenticates with a CaaS API key, sent as a Bearer token on every request.
- In CaaS, open the
apikeyscollection — in the CaaS admin database for a global key, or in your own database for a local key. - Create (or pick) a key for Zeotap.
- Make sure its permission covers the collections you want to import, and that the permission’s
methodslist includesGET. - In Zeotap, add a new FirstSpirit loader and paste the key into the API Key field.
The key must be readable, not just writable
This is the one thing that differs from the FirstSpirit destination. A destination only ever writes, so a key with PUT/PATCH/DELETE in its methods is enough. A loader reads, so:
- A write-only key (no
GETinmethods) returns a 4xx on every request and the connection test fails. - A key scoped with
permissionMode: PREFIXto a specific collection path can read that collection but cannot list the database’s collections. That is expected and supported: the connection test tolerates a 403 on the database root and verifies read access on each configured collection instead, and collection auto-discovery is best-effort only. You always name your collections explicitly.
The key is stored encrypted and is only used to read from the configured database.
Configuration
| Field | Type | Required | Description |
|---|---|---|---|
| API Key | Secret | Yes | A CaaS API key whose permission includes GET on the target collections. |
| CaaS Base URL | Text | Yes | The base URL of your CaaS Platform REST interface, without a trailing path — e.g. https://your-project.caas.e-spirit.cloud. |
| Database | Text | Yes | The CaaS database (tenant/project store) to read from. The first path segment of the CaaS URL scheme. |
| Collections | Tag list | Yes | The CaaS collections to import — one table each. Type a name and press Enter to add it. See Choosing collections. |
| Page Size | Number | No | Documents fetched per CaaS request, 1–100. Defaults to 100. CaaS caps pagesize at 100 and silently reduces anything larger, so values above 100 are clamped to 100. |
| Always Full Refresh | Toggle | No | Disables incremental sync and reads every document on every run. See Sync Modes. Defaults to off. |
Choosing collections
CaaS Connect names the collections it maintains after the FirstSpirit project UUID plus the publishing state:
| Collection | Contains | Import it? |
|---|---|---|
<project-uuid>.release.content | Page references, datasets, GCA pages and project properties in the released state. | Yes — this is the normal choice. |
<project-uuid>.preview.content | The same content in the preview state. | Usually not. |
<project-uuid>.release.files | Metadata for released media (images, downloads). | Optional. |
<project-uuid>.preview.files | Metadata for preview media. | Usually not. |
.preview.contentholds unpublished drafts. Importing it loads content your editors have written but have not released — including copy, prices and offers that were never meant to be visible. If you use preview data for a content-QA table, keep it in a separate loader and do not model it alongside released content.
apikeys and gql-apps are reserved by CaaS and are never offered as streams; apikeys in particular holds credentials.
Copy collection names verbatim from CaaS. A misspelled name is not silently ignored — the run fails with a “collection does not exist” error, because for a loader an absent collection means there is nothing to read.
Available Streams
Each collection you configure becomes one stream and one table. If your API key is broad enough to list the database’s collections, Zeotap also suggests the ones it finds; if it is not (the common case for a scoped read key), only the collections you named are offered.
A CaaS collection is schemaless — page references, datasets, media and project properties live side by side, and your own FirstSpirit templates add arbitrary fields. So every collection lands with the same stable column set: a header of FirstSpirit system fields, plus the whole document.
| Column | Type | Description |
|---|---|---|
_id | String | The CaaS document id — the FirstSpirit element’s GID plus the locale (e.g. f6910b22-…-c7b364b3117a.en_GB). |
_etag | String | The document’s CaaS revision tag. |
fs_type | String | The FirstSpirit element type: PageRef, Dataset, Media, GCAPage, ProjectProperties, … |
identifier | String | The FirstSpirit identifier. |
name | String | The technical name. |
display_name | String | The editorial display name. |
uid | String | The FirstSpirit UID (page references, media, GCA pages). |
uid_type | String | The UID type, e.g. SITESTORE_LEAF. |
schema | String | For datasets: the FirstSpirit schema. |
entity_type | String | For datasets: the entity type. |
locale_identifier | String | The language abbreviation, e.g. EN. |
locale_language | String | The language code, e.g. en. |
locale_country | String | The country code, e.g. GB. |
route | String | The primary route/URL of the element. |
routes | String | All routes, as a JSON array in text form. |
change_revision | Integer | The FirstSpirit repository revision from changeInfo.revision — the incremental cursor. |
data | String | The complete CaaS document as JSON text. |
Every column is nullable. Which system fields a document carries depends on its type: a page reference has uid and route but no schema; a dataset has schema, entity_type and locale_* but no uid; media metadata has neither route nor locale. change_revision is nullable too — see Sync Modes.
The interesting content — formData, metaFormData, template, page, and everything your templates define — is in data. Extract it downstream with your warehouse’s JSON functions. For example, in BigQuery:
SELECT
_id,
display_name,
locale_language,
JSON_VALUE(data, '$.page.formData.pt_headline.value') AS headline,
route,
change_revision
FROM <your_collection_table>
WHERE fs_type = 'PageRef'routes is JSON text as well, so it unnests the same way (JSON_QUERY_ARRAY(routes) in BigQuery, PARSE_JSON(routes) in Snowflake, routes::jsonb in Postgres).
Sync Modes
| Mode | Supported |
|---|---|
| Full refresh | Yes |
| Incremental | Yes — see the caveat below |
How incremental sync works
FirstSpirit stamps every CaaS document with changeInfo.revision: the repository-wide revision number, which increases monotonically as content is published. It is the cursor:
- Each run reads the revision it stopped at last time.
- It asks CaaS only for documents whose
changeInfo.revisionis greater than that value — so a document read once is never read again. - When the run finishes, the new high-watermark is the highest revision it saw.
A single FirstSpirit publish bumps the repository revision once, which means many documents share one revision value. Zeotap therefore keeps the filter fixed for the whole run and only records the new watermark once every page of a collection has been read. That is deliberate: recording a partial position mid-run could leave the rest of a shared revision permanently filtered out.
The caveat: documents without changeInfo
changeInfo is optional on a CaaS document. A document that does not carry it can never satisfy a “greater than” comparison, so it is invisible to incremental sync — permanently, not just for one run.
This matters in three situations:
- Documents written to CaaS by something other than CaaS Connect (a custom importer, a migration script, a GraphQL app).
- Collections created before
changeInfowas populated. - CaaS Connect older than 3.83.9, where
changeInfowas not always updated even for current content (CAAS-2536).
Handle it one of two ways:
- Turn on Always Full Refresh and set the loader’s sync mode to Full refresh. Every run reads every document and replaces the table. This is the safe default when you are not sure. Stream discovery then stops offering a cursor field — but this does not change the loader’s own sync mode, and it does not delete a cursor already stored. Set the loader’s sync mode to Full refresh as well. If you leave it on Incremental with this toggle on, no watermark is ever recorded and every run appends another complete copy of the collection.
- Run incremental, plus a periodic full refresh. Keep incremental for freshness and schedule a second, full-refresh loader (weekly, say) over the same collections as a reconciliation pass.
Deletions are also worth noting: an element removed in FirstSpirit disappears from CaaS, and there is no tombstone document, so an incremental run cannot see the removal. Only a full refresh drops the row.
How It Works
- Paging. Zeotap reads each collection page by page (up to 100 documents per request, the CaaS maximum), sorted by revision and then document id so page boundaries are stable. It stops when a page comes back empty or shorter than the page size.
- Batching and checkpointing. Each page is written to your warehouse as a batch, and the incremental watermark for a collection is recorded once that collection has been read in full. Streams are read independently, so one collection’s failure does not lose another’s progress.
- Restarts. If a run fails partway, the next run restarts that collection from the last recorded watermark. Incremental runs append, so a restart can re-deliver rows that were already written — deduplicate on
_id(andchange_revision) in your models if that matters. - Sorting needs an index. The read sorts and filters on
changeInfo.revision. On a large collection without a MongoDB index on that field, CaaS may answer408(see Troubleshooting).
Rate Limits
CaaS does not publish a rate limit, because it is normally deployed per customer or per tenant — the throughput you get depends on your own instance’s sizing, and on whether it is FirstSpirit Cloud or self-hosted.
In practice:
- The loader issues one request per 100 documents, so a 50,000-document collection is ~500 requests per full refresh. Incremental runs are far smaller.
- If your instance is shared with the live storefront or a rendering front end, schedule the loader outside peak hours, or ask your administrator whether a dedicated CaaS read endpoint is available.
- A
429response is surfaced with a clear message; reduce the schedule frequency, or lower Page Size if your instance struggles with large pages. - Ask your FirstSpirit administrator for the limits that apply to your deployment before scheduling a frequent full refresh over a large collection.
Table names
The warehouse table is derived from the collection name, so 3ddbe70b-1c6c-….release.content becomes _3ddbe70b_1c6c_…_release_content — dots and hyphens become underscores, and a name starting with a digit gains a leading underscore. Check the table list after the first run rather than guessing the name.
Troubleshooting
| Issue | Resolution |
|---|---|
| ”authentication failed … HTTP 401” | The API key was not accepted. Check the value, and confirm the key still exists in the apikeys collection. |
| ”denied access … HTTP 403” | The key is valid but its permission does not cover this read. Add GET to the permission’s methods, and check that the permission’s url and permissionMode (e.g. a PREFIX scope) actually cover the collection path. A write-only key — one built for the FirstSpirit destination — always fails here. |
| ”collection does not exist … HTTP 404” | The collection name is wrong, or FirstSpirit has never published into it. Copy the name verbatim from CaaS: CaaS Connect uses <project-uuid>.release.content for released content and <project-uuid>.release.files for media. Unlike the destination, a loader cannot create the collection — there would be nothing to read. |
| ”database does not exist … HTTP 404” | Check the CaaS Base URL and the Database name. The database is the first path segment of the CaaS URL scheme. |
| ”CaaS rejected the request … HTTP 400” | CaaS reports 400 for a malformed API key or an invalid query. Re-copy the key, making sure no whitespace was included. |
| ”CaaS timed out answering the query … HTTP 408” | The filter and sort on changeInfo.revision exceeded your instance’s query-performance threshold — this happens on large collections where that field is not indexed. Ask your FirstSpirit administrator to add a MongoDB index on changeInfo.revision for the collection. As a workaround, turn on Always Full Refresh, or lower Page Size. |
| ”HTTP 429” | Your CaaS instance is rate limiting or overloaded. CaaS limits are deployment-specific: reduce the schedule frequency, lower Page Size, or ask your administrator about the limits on your instance. |
| Changed content is not picked up | Incremental sync only sees documents whose changeInfo.revision increased. If a document carries no changeInfo at all it is invisible to incremental sync forever. Confirm CaaS Connect is 3.83.9 or later (CAAS-2536 fixed changeInfo not being updated for current content) and run a periodic full refresh, or turn on Always Full Refresh. |
| Deleted pages are still in the table | CaaS leaves no tombstone when FirstSpirit removes an element, so an incremental run cannot see the deletion. Use a full refresh to drop removed rows. |
| Unpublished content in the table | You are importing a .preview.content collection, which holds drafts. Switch to .release.content. |
| Table is empty after a successful run | The collection exists but the filter matched nothing. On an incremental run this is normal when nothing was published since the last run. On a first run, check that the collection actually holds documents and that your key can read them — an over-narrow PREFIX permission can allow the collection path but exclude the documents. |
| Many columns are NULL | Expected. A CaaS collection mixes element types and each type carries a different subset of the system fields. Filter on fs_type, and read everything else out of data. |
change_revision is NULL for some rows | Those documents carry no changeInfo. They will be loaded by a full refresh but never by an incremental run. |
| No collections are suggested when configuring the loader | Collection listing requires a key that can read the database root, which a PREFIX-scoped read key cannot. This is harmless — type the collection names in yourself. |
Next Steps
- Create a Model over the loaded content table.
- Build an Audience that joins content to customer behaviour.
- Push audiences or content back with the FirstSpirit destination.