Data sources
The inbound direction, and the one that needs no code from you. Give event67 a URL and a token, look at what comes back, drag the fields onto attendees or sessions, and let it run every hour. This is the path when a registration or ticketing tool has an API but no event67 integration.
What a data source is
A saved recipe for reading somebody else's JSON API and turning what it returns into records on your event. You build it in the organizer console under Integrations thenData sources, in four steps: connect, preview, map, schedule.
Each data source targets exactly one of three things.
- Attendees. Names, email addresses, company and job title.
- Sessions. Title, description and start and end times.
- Records. Nothing is mapped and every fetched record is stored raw and browsable. Use this when you want the data on hand before you know what to do with it.
Pulls create and update. They never delete. A record that disappears from the upstream API is left alone in event67, because "the API returned less than usual" and "these people cancelled" look identical from here and only one of them should empty your attendee list. Removing people stays a deliberate action in the console.
Nothing here sends email either. An attendee created by a data source is created silently, the same as an admin adding a row without inviting them. Inviting is always something a person chooses to do.
The connection
Everything about how to reach the upstream API. The console builds this for you; the shape is shown so you know exactly what is being asked of the other side.
{
"url": "https://api.registrations.example.com/v2/registrations",
"method": "GET",
"headers": { "Accept": "application/json" },
"query": { "event": "northwind-2026", "per_page": "200" },
"auth": { "type": "bearer" },
"recordsPath": "data.items",
"pagination": {
"type": "page",
"param": "page",
"start": 1,
"sizeParam": "per_page",
"size": 200,
"maxPages": 20
}
}| Field | What it does |
|---|---|
| url | An HTTPS URL on a public host. GET is the only method; there is no way to make event67 POST to a third party. |
| headers | Non-secret headers only. Authorization, Host andCookie are refused here so a credential cannot end up stored in the clear; credentials go in auth. |
| query | Fixed query parameters, added to every request. |
| auth | One of none, bearer, header with a header name, or basic with a username. The secret itself is entered separately, stored encrypted, and never returned by any endpoint or shown again. |
| recordsPath | Where the array of records lives in the response. Empty means the body is the array itself; otherwise a dot path such as data.items. |
| pagination | Either none, or page numbers: the parameter name, the number to start at, optionally a page-size parameter and size, and a hard stop of between 1 and 50 pages. |
If the upstream API paginates by cursor rather than by page number, ask it for the largest page it will serve and use a single page. Cursor pagination is not supported in this release.
Preview
Before anything is saved, the console fetches once and shows you what came back: how many records it found, the first twenty of them, and every field path it detected with a sample value. That list of paths is what you pick from in the next step, so a typo inrecordsPath shows up here as an empty preview rather than as a failed run at two in the morning.
Given a response like this:
{
"meta": { "page": 1, "pages": 4 },
"data": {
"items": [
{
"registration_id": "REG-88301",
"attendee": {
"given_name": " Dana ",
"family_name": "Okonkwo",
"emails": [{ "address": "Dana@Example.com", "primary": true }]
},
"employer": "Meridian Logistics",
"role": "Head of Operations"
}
]
}
}Preview reports 1 record and offers registration_id,attendee.given_name, attendee.family_name,attendee.emails[0].address, attendee.emails[0].primary,employer and role.
Mapping fields
A mapping is a list of rules, each one taking a path out of the upstream record and putting it into a field on the target, optionally passing it through one transform.
{
"externalIdPath": "registration_id",
"fields": [
{ "target": "firstName", "source": "attendee.given_name", "transform": "trim" },
{ "target": "lastName", "source": "attendee.family_name", "transform": "trim" },
{ "target": "email", "source": "attendee.emails[0].address", "transform": "lowercase" },
{ "target": "company", "source": "employer" },
{ "target": "jobTitle", "source": "role" }
]
}Target fields
| Target | Required | Optional |
|---|---|---|
| attendees | firstName, lastName, email | company, jobTitle |
| sessions | title, startAt, endAt | description |
| records | none | none. The raw record is stored as it arrived. |
firstName and lastName are write-direction only.event67 stores one name per attendee, so on save the two are joined with a single space intodisplayName. They exist as separate mapping targets because upstream systems almost always hold two columns, and the firstName and lastNametransforms below exist to split a single upstream name into them. Do not expect either back: the Partner API and every webhook returndisplayName and nothing else. A record mapped as Dana and Okonkwo reads back as "Dana Okonkwo".
Path syntax
Dot-separated names, with square brackets for array indexes:attendee.emails[0].address. A path that does not exist in a given record yields nothing, which is fine for an optional field and a validation failure for a required one.
Transforms
| Transform | What it does |
|---|---|
| trim | Removes leading and trailing whitespace. |
| lowercase | Lower-cases the value. Worth using on email, because it is what makes matching an existing attendee reliable. |
| firstName | Takes the first word of a single full-name field. |
| lastName | Takes everything after the first word of a single full-name field. |
| isoDate | Parses a date or date-time into ISO-8601 UTC. Use it on session start and end times unless the upstream API already returns them that way. |
The transform list is short on purpose. If the upstream data needs more shaping than this, the right tool is the Partner API with your own script, not a template language inside a form.
The external id
externalIdPath names the field that identifies a record in the upstream system, and it is what makes a second run an update rather than a duplicate. Point it at the most stable id the other side offers. If you leave it empty, event67 hashes the whole record instead, which works but means any change at all upstream looks like a brand-new record.
What a run does
Each fetched record is taken through the same four questions.
- Seen before, and identical? The record is counted as unchanged and nothing is written. This is why a second run over a stable list is cheap.
- Seen before, and different? The record it created or linked to is updated through the same code path the console uses, so every rule about what an attendee or a session may be still applies.
- New, but we already have somebody with that email? For the attendees target, the existing attendee is linked to this external id rather than duplicated. That is what lets you point a data source at an event you have already been running by hand.
- New? It is created, and counted as created.
A record that fails validation is counted as failed and the run carries on. The first twenty failures are kept with their position in the fetched list and the reason, which is usually enough to find the bad rows upstream.
{
"data": {
"id": "c0a7e934-51bd-4d82-b6f1-38e0492c7a5b",
"status": "partial",
"trigger": "manual",
"startedAt": "2026-09-09T08:00:01.204Z",
"finishedAt": "2026-09-09T08:00:14.887Z",
"fetched": 812,
"created": 45,
"updated": 12,
"unchanged": 753,
"failed": 2,
"errors": [
{ "index": 219, "message": "email must be a valid email address" },
{ "index": 604, "message": "firstName is required" }
]
}
}| Run status | Meaning |
|---|---|
| running | In progress. |
| succeeded | Everything fetched was applied. |
| partial | The fetch worked and at least one record failed validation, or the run hit its record or time ceiling. What did apply is applied; the next run picks up the rest. |
| failed | The fetch itself failed: the host did not answer, the response was not JSON, the credential was rejected. Nothing was applied. |
Schedules
A data source is manual, hourly or daily. Manual runs only when somebody presses Run now. Hourly runs when its last run was more than 55 minutes ago, daily when the last run was more than 23 hours ago; both are evaluated on a five-minute cadence, so treat them as "about every hour" and "about once a day" rather than as a cron expression.
Sources are run one at a time and a run is claimed before it starts, so a source cannot be running twice at once however many machines are handling schedules. A source can also be paused, which stops the schedule without deleting the configuration or the records it has already gathered.
Limits and safety
- 5,000 records or 60 seconds per run, whichever comes first. A run that stops at a ceiling is recorded as partial, and the next one continues.
- 20 seconds and 5 MB per HTTP request, and JSON only. A slow or enormous page fails the fetch rather than hanging the run.
GETonly, no redirects followed. Register the final URL.- The same URL rules as webhooks. HTTPS, a public hostname whose every resolved address is public, and no credentials in the URL. Checked when you save and again on every connection.
- Your credential is used and never shown. It is encrypted at rest, sent only as the
authyou configured, and the upstream response body is never written to our logs.
What event67 keeps
One row per external id per data source: the raw record exactly as it arrived, a hash of it, the first time it was seen, the last time it was seen, and which event67 record it created or linked to. The console lets you browse them, which is the fastest way to answer "where did this attendee's job title come from".
Run history is kept for 90 days. The raw records are kept for as long as the data source exists; deleting the data source removes them.
Every change to any of this is dated in the changelog.