When you create, update, delete, or refund an event, the API responds immediately and performs the follow-up work asynchronously: recurrence fan-out, ticket structure setup, and (for Shopify-installed companies) Shopify product mirroring. This endpoint reports the status of that work as an operation.
Create, update, and refund return the operation id on the X-Event-Operation-ID response header. Delete (DELETE /events/{id}) returns the operation as its response body. If your integration needs to wait for the work to finish before proceeding (for example, a bulk import that creates an event and then immediately imports tickets), poll this endpoint until the operation reaches a terminal status (completed or failed).
The operation model
A save or delete operation is an aggregate. It does no work itself; it owns a tree of phases that perform the work, and its own status is a roll-up of theirs. A refund operation is a single leaf with no phases.
The phases present depend on the merchant type and the event, so do not assume a fixed set. See Phases.
Breaking change Aggregates (save, delete) pass through a running status between pending and the terminal completed / failed. If your integration treats “any status other than pending” as done, it will stop too early, while work is still in flight. Treat only completed and failed as terminal. (refund never reports running.)
Get operation
GET /events/{event_id}/operations/{id}
curl -X GET \
https://app.guestmanager.com/api/public/v2/events/71390/operations/4291 \
-H 'Authorization: Token abcdefg' \
-H 'Content-Type: application/json'
Sample response — Shopify
savein progress
{
"id": 4291,
"action": "save",
"event_id": 71390,
"status": "running",
"progress": { "percent": 33, "completed": 2, "total": 6 },
"error": null,
"created_at": "2026-05-18T11:00:00Z",
"updated_at": "2026-05-18T11:00:05Z",
"phases": [
{ "id": 4292, "name": "tickets", "status": "completed", "progress": { "percent": 100, "completed": 8, "total": 8 }, "error": null },
{ "id": 4293, "name": "catalog", "status": "running", "progress": null, "error": null },
{ "id": 4294, "name": "inventory", "status": "completed", "progress": { "percent": 100, "completed": 3, "total": 3 }, "error": null },
{ "id": 4295, "name": "metafields", "status": "pending", "progress": null, "error": null },
{ "id": 4296, "name": "delivery", "status": "pending", "progress": null, "error": null },
{ "id": 4297, "name": "publish", "status": "pending", "progress": null, "error": null }
]
}
Here the local ticket structure is already built (tickets is completed), so the registration types and products are usable, while the Shopify-side phases finish. See Detecting partial readiness.
Sample response —
savefailed (fatal phase)
{
"id": 4291,
"action": "save",
"event_id": 71390,
"status": "failed",
"progress": { "percent": 50, "completed": 3, "total": 6 },
"error": { "code": "INVENTORY_RECONCILE_RAISED", "message": "Shopify rejected the inventory adjustment.", "phase": "inventory" },
"created_at": "2026-05-18T11:00:00Z",
"updated_at": "2026-05-18T11:00:08Z",
"phases": [
{ "id": 4292, "name": "tickets", "status": "completed", "progress": { "percent": 100, "completed": 8, "total": 8 }, "error": null },
{ "id": 4294, "name": "inventory", "status": "failed", "progress": null, "error": { "code": "INVENTORY_RECONCILE_RAISED", "message": "Shopify rejected the inventory adjustment." } },
{ "id": 4297, "name": "publish", "status": "failed", "progress": null, "error": { "code": "UPSTREAM_FAILED", "message": null } }
]
}
The aggregate’s error.phase names the originating phase (matching one of the phases[].name). Phases that could not run because a prerequisite failed are failed with error.code: "UPSTREAM_FAILED".
Sample response —
savecompleted with a non-fatal phase failure
{
"id": 4291,
"action": "save",
"event_id": 71390,
"status": "completed",
"progress": { "percent": 100, "completed": 6, "total": 6 },
"error": null,
"created_at": "2026-05-18T11:00:00Z",
"updated_at": "2026-05-18T11:00:11Z",
"phases": [
{ "id": 4292, "name": "tickets", "status": "completed", "progress": { "percent": 100, "completed": 8, "total": 8 }, "error": null },
{ "id": 4297, "name": "publish", "status": "completed", "progress": null, "error": null },
{ "id": 4295, "name": "metafields", "status": "failed", "progress": null, "error": { "code": "METAFIELDS_RAISED", "message": "Metafield write timed out." } }
]
}
A completed aggregate does not guarantee every phase completed. The trailing phases metafields and delivery are best-effort: a failure stays visible in the tree (and is reported to our error tracking) but does not fail the operation, because the products are already built, stocked, and published by the time those run. If you need to assert that every step succeeded, check the individual phases statuses, not just the aggregate.
Sample response — non-Shopify
save
{
"id": 4310,
"action": "save",
"event_id": 71401,
"status": "completed",
"progress": { "percent": 100, "completed": 1, "total": 1 },
"error": null,
"created_at": "2026-05-18T12:00:00Z",
"updated_at": "2026-05-18T12:00:02Z",
"phases": [
{ "id": 4311, "name": "tickets", "status": "completed", "progress": { "percent": 100, "completed": 4, "total": 4 }, "error": null }
]
}
Sample response —
delete
{
"id": 4330,
"action": "delete",
"event_id": 71390,
"status": "running",
"progress": { "percent": 0, "completed": 0, "total": 2 },
"error": null,
"created_at": "2026-05-18T13:00:00Z",
"updated_at": "2026-05-18T13:00:00Z",
"phases": [
{ "id": 4331, "name": "tickets", "status": "running", "progress": null, "error": null },
{ "id": 4332, "name": "catalog", "status": "running", "progress": null, "error": null }
]
}
Response fields
Top node (the operation):
| Field | Type | Description |
|---|---|---|
id |
integer |
Operation ID. Matches the X-Event-Operation-ID header from the originating request. |
action |
string |
save, delete, or refund. |
event_id |
integer |
The event this operation acts on. Stable across its lifetime. |
status |
string |
pending, running, completed, or failed. Poll until completed or failed. See Status. |
progress |
object / null |
{ percent, completed, total } or null. See Progress. |
error |
object / null |
null until failed; then { code, message, phase }. See Errors. |
phases |
array |
The phase tree (empty [] for refund). See Phases. |
created_at |
string |
ISO 8601 timestamp of creation. |
updated_at |
string |
ISO 8601 timestamp of the last status / progress change. |
Phase node (an entry in phases):
| Field | Type | Description |
|---|---|---|
id |
integer |
Phase ID. |
name |
string |
The phase name. See the Phases enum. |
status |
string |
pending, running, completed, or failed. |
progress |
object / null |
{ percent, completed, total } or null when the phase has no determinate metric. |
error |
object / null |
null, or { code, message } when this phase failed. |
Phase nodes are intentionally slim: no event_id (identical to the operation), no timestamps, and no nested phases (the tree is always two levels).
Status
| Status | Terminal? | Meaning |
|---|---|---|
pending |
no | Planned but not started (waiting on a prerequisite phase, or queued). |
running |
no | In flight. Aggregates pass through it; refund never reports it. |
completed |
yes | Finished successfully. On an aggregate, every fatal phase completed (a non-fatal phase may still have failed). |
failed |
yes | Finished unsuccessfully. error carries the root cause. On an aggregate this means a fatal phase failed. |
Progress
progress is { percent, completed, total } or null.
- On an aggregate (
save/delete) it is a phase-count roll-up:completedandtotalcount terminal phases against total phases, so the bar reaches 100% once every phase is terminal (even if a non-fatal phase failed). It isnullbefore any phase is planned. - On a phase it is the work count when the phase has a determinate batch (
tickets,inventory,metafields,delivery), andnullotherwise (catalog,schedule, and any phase that has not started).
Treat progress as nullable everywhere, and use status (not progress) to decide whether a node is done.
Phases
phases is a flat list of the work the aggregate planned. Ordering is not significant and dependencies between phases are not exposed; track each phase by its status.
Stable The phase name values and the operation action values are a stable enum. New names may be added as the pipeline grows, but existing names will not change meaning. (The error.code values, by contrast, are an open set; see Errors.)
A phase’s name is the subsystem it touches; whether the phase builds or tears down that subsystem is determined by the operation’s action (save builds/syncs, delete tears down). There is no per-phase action field. So tickets builds the product/ticket structure under a save and removes it under a delete; catalog mirrors the metaobject graph under a save and deletes it under a delete.
name |
Subsystem | Fatal to the operation? |
|---|---|---|
schedule |
The child date occurrences of a recurring event. | yes |
tickets |
The local ticket / registration structure (and Shopify products, when connected). Carries determinate progress. Under delete, the product/event teardown. |
yes |
catalog |
The event’s catalog metaobject graph in Shopify (bulk operations under save; removed under delete). |
save: yes · delete: no (best-effort) |
inventory |
Shopify inventory levels, reconciled against ticket availability. | yes |
metafields |
Product / variant metafields in Shopify. | no (best-effort) |
delivery |
Digital delivery configuration for the Shopify products. | no (best-effort) |
publish |
Storefront publication of the products (after catalog and inventory). |
yes |
Which phases appear
The planned set depends on the merchant type and the event. Phases that do not apply are never planned and never appear.
| Merchant / event | save phases |
|---|---|
| Non-Shopify, single-date | tickets |
| Non-Shopify, recurring | schedule, tickets |
| Shopify (custom app), single-date | tickets, inventory, metafields, delivery, publish |
| Shopify (standard app), single-date | tickets, catalog, inventory, metafields, delivery, publish |
| Shopify (standard app), recurring | adds schedule to the front of the standard set |
delete plans tickets (the event/product teardown) plus catalog for Shopify standard-app events (the metaobject-graph teardown). refund plans no phases (phases is []).
Detecting partial readiness
Because save is an aggregate, the merchant’s “I can work with this now” moment arrives before the operation finishes. When the tickets phase reaches completed while the operation is still running, the local ticket structure and (for Shopify) the products exist and are editable; the remaining phases are Shopify-side finishing work. If your integration only needs the tickets/products to exist, you can proceed at that point.
Per-operation summary
action |
Status path | phases |
|---|---|---|
save |
pending → running → completed / failed |
the phase tree (varies) |
delete |
running → completed / failed (returned as running) |
tickets (+ catalog on Shopify) |
refund |
pending → completed / failed |
[] (leaf, no phases) |
Errors
error is null until a node fails, then { code, message } (plus phase on an aggregate). code is a stable, machine-readable string; message is human-readable copy that may change without notice. Always switch on code, not message. On an aggregate, error carries the root-cause phase’s code and names it in phase.
Preview The error.code set is open and additive. New, more specific codes will be introduced as failure modes become worth distinguishing; existing codes will not change meaning. Match coarsely and treat any unrecognized code as a generic failure.
| Code | Meaning |
|---|---|
FAILED |
Generic failure: the underlying batch chain exhausted retries. See message for detail. |
UPSTREAM_FAILED |
A prerequisite phase failed, so this phase never ran. Look at the other phases for the real cause. |
*_RAISED |
A specific phase raised while running (e.g. INVENTORY_RECONCILE_RAISED, METAFIELDS_RAISED, SESSION_BUILD_RAISED, DELIVERY_RAISED). |
| catalog-specific | Shopify catalog phase failures, e.g. BULK_OP_FAILED, METAFIELDS_ROW_ERRORS, RESULT_FILE_ROW_ERRORS. |
Polling guidance
- Start polling with a short delay. Simple
dateevents typically finish in 1 to 5 seconds; recurring events with many occurrences and Shopify catalog mirroring take longer. - Use exponential backoff: 1s, 2s, 4s, 8s, capped at 15s.
- Stop polling only when
statusiscompletedorfailed.pendingandrunningare both non-terminal. - A
404 Not Foundmeans the operation id does not exist, does not belong to the event in the URL, or is a phase id rather than an operation id. Poll with the id fromX-Event-Operation-ID, not aphases[].id. - The underlying records are retained for the lifetime of the operation, so you can fetch the final status long after completion.
Deprecated aliases
For a transition window, the pre-rename path and header continue to work and are equivalent to the canonical ones above. Migrate to the canonical names; the aliases will be removed on or after September 4, 2026, as announced in the changelog.
| Deprecated | Canonical |
|---|---|
GET /events/{id}/sync_jobs/{id} |
GET /events/{id}/operations/{id} |
X-Event-Sync-Job-ID header |
X-Event-Operation-ID header |
What happens for non-Shopify customers
The pipeline runs the same regardless of merchant type. Every company gets the tickets phase (and schedule for recurring events). The Shopify phases (catalog, inventory, metafields, delivery, publish) are planned only for Shopify-installed companies and are simply absent otherwise. The operation completes normally either way.