Two APIs.One is the graph.
One holds the guest graph and answers questions about it. One fills it.
The engine is the API you build against: you send what a system knows about a person, and it answers with the person. A connector is a client of that API with a small surface of its own, the one an operator uses to watch it and to start a run. Every call below is generated from the service's own OpenAPI document at the commit this page pins, so a path here is a path that exists. Those rows are in the one language the documents are written in; the rest of this page is bilingual.
There is no instance to call. GuestGraph is Apache-2.0 and you run it yourself; the
paths below are relative to your own deployment, and a running instance serves its complete document at
/api-docs with no key.
One fills it
connector-apaleoA connector is a client like any other: it reads one source system and submits what it finds. Its own surface is what an operator calls, not what an integration calls, and it answers to a bearer token rather than to a tenant's key.
POST/Receive one Apaleo delivery, or its reachability checkapaleo/ events/ {secret} The secret names the connection: the delivery is stored under the connection whose webhook secret hash matches and answered at once; processing happens on the worker. A second delivery of the same id is acknowledged and ignored. An empty body is Apaleo's reachability check and is answered with 200. A secret that matches no connection is 404, indistinguishable from an unknown path.
Request
ApaleoEventidstring requiredtopicstring requiredtypestring requiredaccountIdstring requiredpropertyIdstring requiredtimestampstring requiredclientIdstringsubjectIdstringdataobject
Responses
200Reachability check answered202Delivery stored404Unknown path
GET/Is this connector fine, connection by connectionstatus Responses
200The status documentStatusconnectionsConnectionStatus[] required
401Missing or wrong token ·unauthorized
POST/Start a full sync of one connection's properties and bookingsconnections/ {connectionId}/ sync/ full Responses
202StartedRunStartedrunIdstring required
401Missing or wrong token ·unauthorized404No such connection ·not-found409A full sync is already running on this connection ·run-in-progress
POST/Start a reconciliation from one connection's sync pointsconnections/ {connectionId}/ sync/ reconcile Responses
202StartedRunStartedrunIdstring required
401Missing or wrong token ·unauthorized404No such connection ·not-found409A run is already in progress on this connection ·run-in-progress
POST/Re-read one connection's held guest ids and apply the integrator ruleconnections/ {connectionId}/ refresh Responses
202StartedRunStartedrunIdstring required
401Missing or wrong token ·unauthorized404No such connection ·not-found
GET/One run's progress and outcomeconnections/ {connectionId}/ runs/ {runId} Responses
200The runRunidstring requiredkindstring requiredstartedAtstring requiredfinishedAtstringoutcomestringreservationsSeeninteger requiredversionsSubmittedinteger requiredrecordsSubmittedinteger requiredduplicatesinteger requiredflaggedForReviewinteger requirederrorsinteger requiredlastErrorstring
401Missing or wrong token ·unauthorized404No such connection or run ·not-found
PUT/Create this connection's webhook subscription againconnections/ {connectionId}/ subscription The same act the connector performs for every connection when it starts: creates the subscription, or replaces one whose endpoint, events or properties differ. Idempotent, and the way back from a removal without editing configuration or restarting.
Responses
200A subscription names this connectionSubscriptionStatestatestring requiredidstringeventTypesstring[] requiredcheckedAtstring
401Missing or wrong token ·unauthorized404No such connection ·not-found502Apaleo refused or could not be reached; nothing was created ·apaleo-unreachable
DELETE/Remove this connection's webhook subscription in Apaleoconnections/ {connectionId}/ subscription Deletes the subscription whose endpoint is this connection's, and nothing else in the Apaleo account, so two connections sharing an account are independent. A statement of the end state rather than of an act: asking twice, or asking when none exists, succeeds and says so in `removed`. The absence lasts until the connector is restarted or the subscription is restored; a restart subscribes every configured connection again.
Responses
200No subscription now names this connectionSubscriptionRemoved401Missing or wrong token ·unauthorized404No such connection ·not-found502Apaleo refused or could not be reached; nothing was removed ·apaleo-unreachable
What arrives
engineA system registers itself, then sends what it knows about a person. Resolution happens in the same request, so the answer names the guest the record turned out to belong to.
POST/Register a source system for the caller's tenantsource-systems Request
codestring requirednamestring required
Responses
201RegisteredSourceSystemidstring requiredcodestring requirednamestring requiredcreatedAtstring required
400invalid-request401unauthorized409Code already registered in this tenant ·conflict
POST/Ingest guest record(s); resolves synchronouslyrecords Accepts a single record or a batch (max 1000). Each record is stored immutably and resolved independently; one outcome is returned per record. Malformed-but-parseable records are stored flagged needs_review — never dropped. Only an unparseable request body is rejected as a whole.
Request
IngestRecord one or a list of themsourceSystemstring requiredexternalKeystring requiredrecordTimestampstringpayloadobject required
Responses
200Per-record resolution outcomes (batch is never atomic)resultsIngestResult[] required
400invalid-request401unauthorized
What came out
engineOne person, assembled from every record that turned out to be them, with the evidence for it. A guest id you store keeps answering after a merge.
GET/Golden profile + identifiers, or where a retired id wentguests/ {guestId} Responses
200The guest, or the resolution of a retired id — told apart by status401unauthorized404not-found
GET/The guest's source records, as originally receivedguests/ {guestId}/ records Responses
200Source recordsrecordsSourceRecord[] required
401unauthorized404not-found410The guest id was retired by a merge or a split. The problem details name the current guest or guests; read GET /guests/{guestId} on the retired id for the full resolution. ·guest-retired
GET/Why these records are one guest — the full merge-event chainguests/ {guestId}/ explain Responses
200Decision chain, oldest first (includes events of absorbed guests)guestIdstring requiredeventsMergeEvent[] required
401unauthorized404not-found410The guest id was retired by a merge or a split. The problem details name the current guest or guests; read GET /guests/{guestId} on the retired id for the full resolution. ·guest-retired
GET/The guest's current business-object associationsguests/ {guestId}/ timeline Associations the guest currently holds, ordered by business start and falling back to the observation timestamp when the submitter supplied no business dates. An association is current while the object's newest version roster places this guest in that role. With includePast=true, associations the guest no longer holds are also returned with status ENDED, naming the successor when exactly one guest holds that role now.
Responses
200Associations pageitemsAssociation[] requirednextCursorstring
401unauthorized404not-found410The guest id was retired by a merge or a split. The problem details name the current guest or guests; read GET /guests/{guestId} on the retired id for the full resolution. ·guest-retired
GET/Look up guests by identifier valueguests The value is normalized before matching. A miss returns an empty list. Without an explicit type, every type whose normalization accepts the value is tried — except ID_DOCUMENT, which is only matched when type=ID_DOCUMENT is given (pass the value as TYPE:NUMBER; it is hashed before matching).
Responses
200Matching guests (usually 0 or 1; >1 after a rejected shared-identifier review)guestsGuest[] required
400invalid-request401unauthorized
GET/A business object's current roster and observation historysource-objects/ {sourceSystem}/ {objectType}/ {objectId} The object as the graph knows it: the newest version's roster — who is on it now, in which role, resolved to which guest — and every observation of the object in version order, including superseded ones and those belonging to guests no longer on it. The history is a property of the object, not of any one guest, because a reassignment spans two of them.
Responses
200The object, its current roster, and its observationssourceSystemstring requiredobjectTypestring requiredobjectIdstring requiredcurrentVersionstring requiredbusinessStartstringbusinessEndstringrosterRosterEntry[] requiredobservationsObjectObservation[] required
401unauthorized404not-found
When it is wrong
engineEvery merge the engine makes can be taken back, and a steward's decision outlives the next import rather than being overwritten by it.
POST/Detach wrongly linked record(s) and replay resolutionguests/ {guestId}/ unmerge Removes the resolution links of the given source records, records an UNMERGE event (with an exclusion so identical re-ingest does not silently recreate the wrong merge), re-resolves the detached records, and recomputes the remaining guest's profile and identifiers. Source records are never altered.
Request
sourceRecordIdsstring[] required
Responses
200Outcome of the replayunmergeEventIdstring requiredremainingGuestIdstringdetachedRecordsobject[] required
400E.g. record not linked to this guest, or guest has a single record ·invalid-unmerge401unauthorized404not-found410The guest id was retired by a merge or a split. The problem details name the current guest or guests; read GET /guests/{guestId} on the retired id for the full resolution. ·guest-retired
GET/Pending uncertain matchesmatch-reviews Responses
200Review queue entries, oldest firstreviewsMatchReview[] requiredtotalinteger requirednextCursorstring
401unauthorized
POST/Confirm or reject a queued match (exactly once)match-reviews/ {reviewId} Request
decisionstring required
Responses
200Decision applied; on CONFIRM the merge executed and was recordedMatchReviewidstring requiredstatusstring requiredsourceRecordIdstring requiredcandidateGuestIdstring requiredidentifierobjectreasonstring requiredmatcherNamestringconfidencenumbercreatedAtstring requireddecidedAtstringdecisionEventIdstring
401unauthorized404not-found409Review already decided; the first decision stands ·review-already-decided
GET/Do-not-merge rules (written by unmerge and review rejections)negative-rules Responses
200Rules, newest firstrulesNegativeRule[] requiredtotalinteger requirednextCursorstring
401unauthorized
DELETE/Lift a do-not-merge rule (steward act) — the pair may merge on next evidencenegative-rules/ {ruleId} Responses
204Lifted401unauthorized404not-found
What the tenant decides
engineThresholds and identifier trust are per tenant, because one brand's shared front-desk number is another brand's real one.
GET/The tenant's matching thresholdsconfig/ matching Responses
200Current configurationMatchingConfigautoMergeThresholdnumber requiredreviewFloornumber requiredreviewThresholdinteger required
401unauthorized
PUT/Update the tenant's matching thresholds (transactional — all or nothing)config/ matching Request
MatchingConfigautoMergeThresholdnumber requiredreviewFloornumber requiredreviewThresholdinteger required
Responses
200Applied; effective for subsequent resolutions, no restartMatchingConfigautoMergeThresholdnumber requiredreviewFloornumber requiredreviewThresholdinteger required
400Invalid combination (e.g. reviewFloor > autoMergeThreshold, values outside 0..1) ·invalid-request401unauthorized
GET/Identifier quality rules — tenant-added plus built-in defaultsconfig/ identifier-rules Responses
200Rules; built-ins carry builtin=true and cannot be deletedrulesIdentifierRule[] required
401unauthorized
POST/Add a tenant identifier quality ruleconfig/ identifier-rules Request
identifierTypestring requiredmatchKindstring requiredvaluestring requiredrulestring requirednotestring
Responses
201Created; effective for subsequent resolutionsIdentifierRuleidstring requiredidentifierTypestring requiredmatchKindstring requiredvaluestring requiredrulestring requirednotestringbuiltinboolean requiredcreatedAtstring
400invalid-request401unauthorized409Equivalent rule already exists ·conflict
DELETE/Delete a tenant rule (built-ins are not deletable)config/ identifier-rules/ {ruleId} Responses
204Deleted401unauthorized404Unknown rule id — including built-ins, which carry no id and therefore cannot be addressed for deletion ·not-found
When a call is refused
Both services answer a refusal the same way: an RFC 9457 problem detail whose
type is a section of the problems page. Follow it and you land on what that
type means and what to do about it, rather than on a status code you have to interpret. Two refusals belong to every call and so appear on none of the rows: a malformed actor claim, which the key filter raises before an operation runs, and a body over the service's size cap.
- What each refusal type means — each with the service that answers it
- The documents themselves — the engine's and the connector's, which this page is built from
- How identity is decided — what makes two records one person, and what a stored guest id means later
Generated from guestgraph/engine@c8b205c and guestgraph/connector-apaleo@65d24cc, the documents those commits hold.