Documentation
Documentation

Overview

Spore docs

Start here

Getting startedInstall the CLIAuthentication and keys

Build

Search and filtersSegments and the loopEnrichment lifecycle

Recipes

Recipe: search to a live SegmentRecipe: enrich without a segmentRecipe: land your own research

Connect

Events

Account

Credits and billing

Reference

Conventions
API reference

Enrichment lifecycle

What a launch does after you start it: lifecycle, the five outcomes, polling, partial failure, cancellation, and automatic refunds.

Everything that costs compute is asynchronous. POST /api/v1/enrich answers with the canonical Job resource — { job, quote, targeted, refused }. job is the authoritative intent, scheduling, economics, capacity, and Run-progress projection. quote separates Run compute from permanent Legal entity unlocks, and job.quotedCredits equals quote.total. Keep job.id as the handle for GET /api/v1/enrichments/{enrichmentId}: a Job borrows the ID of the act that created it, so the same ID reads the launch's own per-target projection.

Lifecycle and outcome

The poll body carries two independent fields:

  • lifecycle describes where the run is: waiting | queued | running | completed | completed_with_errors | failed | cancelled.
  • outcome describes what a terminal run produced: found | nothing_found | couldnt_look | failed | cancelled, or null while it is waiting, queued, or running.

The projection is a pure function of the stored run:

LifecycleOutcomeMeaning
waiting, queued, runningnullAdmission or execution is still in progress
completedfoundThe tool looked and produced an answer
completednothing_foundThe tool looked and definitively found nothing
completedcouldnt_lookThe world stopped the tool from looking (an unreachable or blocked site, unreadable pages)
failedfailedSpore's fault: a thrown error, a supplier outage, or Spore's own cost cap or output ceiling
cancelledcancelledCancellation ended the run

Do not collapse these fields into a single status. The accepted response's job.lifecycle uses the customer Job vocabulary, while the preserved v1 launch.lifecycle and launch.outcome describe launch progress; each child poll has its own lifecycle and outcome.

Polling a launch

curl https://www.spore.ee/api/v1/enrichments/$LAUNCH_ID \
  -H "Authorization: Bearer $SPORE_API_KEY"

The response below shows the lifecycle fields an explicit-Company enrichment preserves. Enrichments now target Company ids directly; live Segment membership is read first and is not stored as a Segment target on the launch.

{
	"executionId": "51e40960-d578-42c6-812c-7c2e21cfc540",
	"workflow": "domain-finder",
	"lifecycle": "completed",
	"outcome": "nothing_found",
	"message": "No candidate domains found.",
	"input": {
		"tool": "domain-finder",
		"intent": "Find the domain for Bolt",
		"input": {},
		"priority": "normal",
		"maxCredits": 2
	},
	"output": null,
	"billing": {
		"creditsCharged": 0
	},
	"startedAt": null,
	"completedAt": "2026-08-12T09:27:59.265Z",
	"createdAt": "2026-08-12T09:27:37.564Z",
	"launch": {
		"id": "51e40960-d578-42c6-812c-7c2e21cfc540",
		"tool": "domain-finder",
		"executor": "spore",
		"launchKind": "full",
		"priority": "normal",
		"lifecycle": "completed",
		"outcome": "nothing_found",
		"targeted": 1,
		"completed": 1,
		"failed": 0,
		"canceled": 0,
		"cancelRequestedAt": null,
		"error": null,
		"createdAt": "2026-08-12T09:27:37.564Z"
	}
}
FieldMeaning
executionIdthe id you polled
workflowthe tool that ran
lifecycle, outcome, messagewhere the run is and what it produced
inputon a launch, the stored envelope whose nested input holds shared knobs and context references naming Catalog fields; on a child Run, those knobs plus server-written core and per-target resolved knowledge; targets are not stored in this envelope
outputflat tool output, or null before completion and on non-success terminal states
billing.creditsChargedwhat this row was charged at trigger time
startedAt, completedAtnull until they happen
createdAtwhen the row was written
launchpresent only on a launch row; per-target progress for the work it dispatched
launch.prioritythe scheduling tier, read from the canonical Job row
jobthe accepted POST /api/v1/enrich response; the canonical Job projection
job.intent, job.kindthe durable purpose and workflow kind
job.lifecyclethe customer Job lifecycle, including completed_with_issues
job.prioritybackground, normal, or urgent, read from the Job row
job.occupiedSlotsthe Runs of this Job currently holding a slot
job.targetCount, job.runs, job.runTotaldeclared target count and aggregate Run counts
job.quotedCredits, job.maxCreditsthe Job's quote and hard credit ceiling
quote.runs, quote.unlocks, quote.totalaccepted POST Run compute, permanent Legal entity unlocks, and their total; job.quotedCredits equals quote.total
targeted, refusedadmitted target count and targets refused for missing Core; the Job fields are authoritative after admission

Read the launch's frozen ordered target receipt at GET /api/v1/jobs/{jobId}/targets. Its pages carry { items: [{ targetIndex, companyId }], nextCursor, watermarks: { postgres, generations } }. watermarks.generations maps each Jurisdiction the Commons served at admission to the Generation this Job read, as in { "EE": "20260905_023200" }, and is null when none was named. That receipt and its watermarks do not change when a target is later deleted or merged.

Polls are free and set no Retry-After — see the async pattern.

The launch block

targeted is the declaration: how many targets the launch was created for. completed, failed and canceled are derived from the children that have arrived, never from a stored counter — so a launch nobody is working on simply shows no progress rather than a comforting lie.

For Spore-executed launches, each admitted Company target has one child row. A launch's lifecycle and outcome summarize those child rows.

A launch goes terminal when every child has settled and either the target count was reached or cancellation was requested. Its terminal lifecycle is then:

Childrenlaunch.lifecyclelaunch.outcome
at least one completed and one failedcompleted_with_errorsfailed
any failed, but none completedfailedfailed
otherwise, any cancelled or fewer children than targetedcancelledcancelled
otherwisecompletedfound if any child found, else nothing_found if any found nothing, else couldnt_look

The launch's own projection spells the mixed terminal state completed_with_errors. The canonical job.lifecycle calls the same aggregate state completed_with_issues; use the Job field when deciding whether the intent completed cleanly. A child settles with completed, failed, or cancelled lifecycle.

Reading the results

The launch is progress, not payload. Each child holds its own run, and each child is a pollable enrichment in its own right:

{
	"executionId": "ad3c513b-67c8-43a1-b65e-455dca4be04b",
	"workflow": "domain-finder",
	"lifecycle": "completed",
	"outcome": "found",
	"message": "Completed successfully.",
	"input": {
		"core": {
			"companyId": "8a7a0f13-fce0-4d97-a5f8-1950a49e7a63",
			"name": "Bolt Technology OÜ",
			"country": "EE",
			"activity": {
				"code": "62.01",
				"label": "Ride-hailing and micromobility platform"
			},
			"identifier": "12417834"
		},
		"knowledge": []
	},
	"output": {
		"domain": "bolt.eu",
		"confidence": "high",
		"responsive": true,
		"linkedin": {
			"url": "https://www.linkedin.com/company/bolt-eu",
			"confidence": "high"
		}
	},
	"billing": {
		"creditsCharged": 2
	},
	"startedAt": "2026-08-12T09:27:38.847Z",
	"completedAt": "2026-08-12T09:27:59.252Z",
	"createdAt": "2026-08-12T09:27:38.694Z"
}

output above is abbreviated: the real domain-finder body also carries the enriched domain, bot-protection details, the Company's brand name, and activity-match percentage. No launch key, because a child is not a launch.

You do not have to keep the child ids. The work lands as knowledge on the company, so the durable read is the dossier: GET /api/v1/company?ids={companyId} returns the company projection and its knowledge fields. Each value carries observedAt, stale, and sourceRefs — the enrichment ids that produced it, which you can poll for the full run.

Nothing found and couldn't look

A tool that looked and definitively found nothing settles with lifecycle: "completed" and outcome: "nothing_found". A tool the world stopped from looking — the site was unreachable or blocked, or its pages could not be read — settles completed with outcome: "couldnt_look". Either way the tool's own reason becomes message, rather than a generic "no result".

Nothing found stays charged in full: "we looked, there is nothing" is a usable answer, and paying for it once is cheaper than paying to re-ask. Couldn't look refunds the part no provider work consumed.

Refunds

Credits are charged by the child's first durable workflow step. What a child keeps follows from its outcome alone, refunded automatically and idempotently:

OutcomeCharge
foundkept in full
nothing_foundkept in full
couldnt_lookrefunds the part no provider work consumed
failedrefunds everything still held — Spore's fault costs you nothing
cancelledrefunds the part no provider work consumed; a child cancelled while still queued refunds in full

A refund is a positive workflow_refund row in GET /api/v1/wallet/transactions referencing the enrichment it returned, so a batch that broke halfway is auditable from the ledger alone.

Cancelling

curl -X POST https://www.spore.ee/api/v1/enrichments/$LAUNCH_ID/cancel \
  -H "Authorization: Bearer $SPORE_API_KEY" \
  -H "Idempotency-Key: cancel-$LAUNCH_ID-1"

Cancelling is free and answers 200 with { "launch": … } — the same launch resource the poll returns. A Spore launch records the request and in-flight children observe it on their next checkpoint. Pending children are refunded per the rules above.

A repeat cancel on a settled cancelled launch answers 200 with { "launch": … }, even with a new Idempotency-Key. This is idempotent: the requested cancellation has already succeeded. It returns the unchanged launch, including its original cancelRequestedAt, rather than requesting cancellation again.

A settled launch whose lifecycle is not cancelled answers 409 with already_terminal: its lifecycle is completed, completed_with_errors, or failed. Cancellation may have been requested and some children may have been cancelled; child failures take precedence when the launch settles. This code never belongs to a launch that has not settled.

Reusing the same Idempotency-Key and input after a successful cancellation replays its stored answer.

A later retry after a refused cancel is evaluated again, even with the same key and input: a refusal does not retain the idempotency claim. If the launch has moved, the answer can change.

{
	"error": "already_terminal",
	"message": "Execution is already completed.",
	"current": "completed"
}

A repeat cancel on a launch whose cancellation is already requested and not yet settled is 409 cancel_already_requested. Its current is the launch's own lifecycle, which is still non-terminal — that is the point of the separate code:

{
	"error": "cancel_already_requested",
	"message": "Cancellation is already requested for this execution; poll it until its lifecycle is terminal.",
	"current": "pending"
}

A launch that never had a cancellation requested and has no cancellable work left — its children have stopped, its own settlement has not landed yet — is 409 nothing_cancellable. Both answers mean the same next step: keep polling until the lifecycle is terminal.

A launch whose executor cannot be cancelled answers 409 with executor_not_cancellable. An id that is absent — or belongs to another organization — is a 404.

A cancel that is recorded but cannot yet reach every running child answers 503 service_unavailable with retryAfter (and Retry-After). The launch is not cancelled yet, so there is no launch in the answer. Retry after the wait, with the same Idempotency-Key or a new one: the retry itself performs the rest of the cancel and answers 200 with the launch, as a cancel that reached every child does, or 409 already_terminal if the launch settled first. Nothing else finishes it for you.

Segments and the loop

Name a live filter over your Companies, read its members, refine it, and use what you learn.

Recipe: search to a live Segment

Search the Commons, unlock the population, then read your Companies and a live Segment for free.

On this page

Lifecycle and outcomePolling a launchThe launch blockReading the resultsNothing found and couldn't lookRefundsCancelling