Admin Ingestion
The admin app exposes a set of Sanctum-protected endpoints for external ingestion agents (such as n8n) to submit content payloads and for curators to manage the review workflow.
Auth
Source-management and submission endpoints require a Sanctum service token with the appropriate ability and Spatie permission. For n8n-managed sources provisioned from the admin app, that token is issued per source and injected into the cloned workflow automatically. Review endpoints also work for authenticated curator/admin browser sessions.
Base URLs
| Environment | Base URL |
|---|---|
| Local | https://localhost/admin/api |
| Production | TBD |
Authentication
Service-account tokens can be created manually by an admin and scoped to the minimum abilities needed. For n8n-managed sources, the app issues a dedicated per-source token, injects it into the cloned workflow, scopes it to that source’s source_url, and revokes it when the source is deleted.
| Ability | Required for |
|---|---|
ingestion:view | List sources |
ingestion:create | Submit payloads, create sources |
Spatie permission checks (ingestion.view, ingestion.create) are enforced server-side in addition to ability checks. A token with the correct ability but belonging to a user without the matching Spatie permission is rejected with 403.
Ingestion Sources
List sources
GET /admin/api/ingestion-sourcesRequires ingestion:view ability and ingestion.view permission.
Response 200 OK — Laravel paginator envelope with source objects.
Create a source
POST /admin/api/ingestion-sourcesRequires ingestion:create ability and ingestion.create permission.
Request body
{
"name": "Tourism Board Feed",
"source_url": "https://example.com/tourism/events",
"ingestion_type": "api",
"provider": "example.com",
"external_source_id": "tourism-board-events",
"source_priority": 10,
"is_active": true
}| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | Human-readable source label |
source_url | string | Yes | Unique canonical URL for this feed |
ingestion_type | string | Yes | api | scrape | manual | csv |
provider | string | No | Required when external_source_id is given |
external_source_id | string | No | Unique within the provider |
source_priority | integer | No | Defaults to 0 |
is_active | boolean | No | Defaults to false |
approval_mode is server-managed — change it through the admin UI.
Response 201 Created — { "data": { ...source } }
Submitting payloads
POST /admin/api/ingest/{type}{type} is event, place, business, cultural, or local.
Requires ingestion:create ability and ingestion.create permission. For source-scoped n8n tokens, every submitted item must use the bound source_url or the request is rejected with 403.
Single item
{
"item": {
"external_id": "evt-001",
"source_url": "https://example.com/events",
"name": "Carnival Street Jump",
"external_url": "https://example.com/events/evt-001"
}
}Batch
{
"items": [
{
"external_id": "evt-001",
"source_url": "https://example.com/events",
"name": "Carnival Street Jump"
},
{
"external_id": "evt-002",
"source_url": "https://example.com/events",
"name": "Beach Lime"
}
]
}Response 202 Accepted
{ "data": { "queued": 2, "source_id": null } }Items are processed asynchronously. Per-item validation, deduplication, staged record creation, and approval-mode handling happen after the response.
Local payloads
local submissions use the same envelope as the other ingestion targets. Payloads can include editorial fields such as local_subtype, dob, dod, known_for, photo_url, tags, and image/gallery URLs. When the raw payload omits local_subtype, the normalizer can infer it from source fields such as person_type, role, occupation, or type and preserve that inferred value through promotion.
Review workflow
Curators review staged items in the Filament admin panel. The same state transitions are available via API:
| Action | Endpoint | Required status |
|---|---|---|
| Approve | POST /admin/api/ingested-items/{item}/approve | pending or processing |
| Reject | POST /admin/api/ingested-items/{item}/reject | pending or processing |
| Request changes | POST /admin/api/ingested-items/{item}/request-changes | pending or processing |
| Retarget | POST /admin/api/ingested-items/{item}/retarget | any non-merged status |
| Promote to canon | POST /admin/api/ingested-items/{item}/promote | approved |
Retargeting clears stale canonical links, recalculates hash-based duplicate markers for the new target type, and queues the embedding-based duplicate check again.
Promoting an item creates or updates the matching canonical record and marks the ingested item as merged.
Item lifecycle
submitted → processing → pending → approved → merged
↘ rejectedAuto-approval sources skip the pending step:
submitted → processing → approved → mergedPromotion targets
| Payload type | Promotes to |
|---|---|
event | Event record |
place | Place record |
business | Place with place_kind = business |
cultural | Story record |
local | Local record |
For cultural payloads, include culture_topic_id, culture_topic_slug, or culture_topic_name so curators can promote the item into the correct story topic.
For local payloads, gallery media is attached to the gallery collection and treated as the canonical primary media collection for import and re-import.
Error reference
| Status | Meaning |
|---|---|
422 | Malformed submission or invalid state transition |
401 | Missing or invalid Sanctum token |
403 | Token ability, permission, or source-scoped access denied |
404 | Item not found |