Skip to content

Pull jobs

A pull job asks the Unreal plugin to bring props into a project with every dependency, laid out under a target folder. You create the job; an Unreal editor open on that project claims it, downloads the files, imports them and reports back.

Method and path Scope Who calls it
POST /pull-jobs write You: create a job
GET /pull-jobs read You or the editor: list jobs
GET /pull-jobs/{job_id} read Job detail and report
PATCH /pull-jobs/{job_id} with cancelled write You: cancel a queued job
POST /pull-jobs/{job_id}/claim write The editor
POST /pull-jobs/{job_id}/heartbeat write The editor
GET /pull-jobs/{job_id}/closure read The editor
PATCH /pull-jobs/{job_id} with running, done, failed write The editor
Terminal window
curl -X POST -H "Authorization: Bearer $PH_KEY" -H "Content-Type: application/json" \
-d '{"target_project": "ColdStorage", "target_root": "/Game/Prophouse", "entries": [{"entry_id": 412}, {"entry_id": 88, "object_id": 1502}]}' \
https://api.prophouse.dev/api/v1/pull-jobs
Field Type Description
target_project string, required The Unreal project name the job is for (up to 200 characters). Only an editor open on a project with this exact name can claim it.
target_root string Destination folder: /Game or a folder below it. Default /Game/Prophouse.
entries array [{entry_id, object_id?}], up to 1,000. object_id pins a specific version from the entry’s lineage; omit it for the current version.
collection_id integer Pull every entry in a collection instead. Send exactly one of entries or collection_id.
delivery string manual (default): the editor picks the job up on its next check or when someone clicks to check now. agent: delivered automatically to your own editors that have agent imports turned on. See Editor sessions.

The full dependency closure is resolved and saved when the job is created, so later pushes do not change what a queued job delivers. Creating a job marks its entries as pulled (they can no longer be hard-deleted) and records download.pull activity. A new target_project name is added to projects.

201 returns the job detail plus warnings, for example {"type": "deprecated_entry", ...} for deprecated entries (they are still pulled).

Error Cause
404 ENTRY_NOT_FOUND, 404 OBJECT_NOT_FOUND, 404 COLLECTION_NOT_FOUND Unknown id.
409 LINEAGE_CONFLICT A pinned object_id is not in that entry’s lineage.
422 VALIDATION Bad target_root; the closure includes files without a /Game path (source files cannot be pulled into Unreal); the closure has more than 5,000 files; two versions of one prop would land in the same place.
{
"id": 57,
"target_project": "ColdStorage",
"target_root": "/Game/Prophouse",
"requested_by": 7,
"claimed_by": null,
"status": "queued",
"delivery": "manual",
"failure_reason": null,
"engine_version": null,
"entry_count": 2,
"object_count": 9,
"total_bytes": 18874368,
"created_at": "2026-07-25T12:00:00Z",
"updated_at": "2026-07-25T12:00:00Z"
}

status is queued, claimed, running, done, failed or cancelled. engine_version is the claiming editor’s engine version.

The detail (GET /pull-jobs/{job_id}) adds entries: [{entry_id, object_id, name, entry_type, status}] and report_json (the editor’s report once finished, otherwise null).

Parameter Type Description
target_project string Exact project name.
status string One status value.
requested_by integer Library user id of the requester.
limit, cursor Opaque cursor pagination, newest first.

Returns {"jobs": [job], "next_cursor"}.

{ "status": "cancelled" }

PATCH /pull-jobs/{job_id}. Only the requester or an admin (403 otherwise), and only while the job is queued (409 JOB_STATE otherwise).

  1. POST /pull-jobs/{job_id}/claim with {"project_key": "<target_project>", "engine_version": "5.8.0"}. Exactly one claimant wins. The response is the job detail plus a secret claim_token and warnings (for example older_engine for files saved by an older engine). Claims are leases of 60 minutes.
  2. GET /pull-jobs/{job_id}/closure returns the saved file list: {"job_id", "objects": [{object_id, content_hash, lineage_id, asset_class, asset_name, origin_path, blob_size, engine_version}], "total_objects", "total_bytes"}. Download each through Downloads.
  3. PATCH /pull-jobs/{job_id} with {"status": "running", "claim_token"}.
  4. POST /pull-jobs/{job_id}/heartbeat with {"claim_token"} during long work. Returns {"job_id", "status", "lease_expires_at"}.
  5. Finish with PATCH {"status": "done", "claim_token", "report_json"} or {"status": "failed", "claim_token", "failure_reason", "report_json"?}.

Claim errors: 409 CONFLICT when project_key is not the job’s target_project; 409 JOB_STATE when the job is no longer queued; 409 ENGINE_TOO_OLD when some files were saved by a newer engine than yours (details.objects, the job stays queued). If a lease expires, the job returns to queued and the old claim_token stops working (409 JOB_STATE).

The report lists every closure file exactly once:

{
"added": [{ "asset_name": "SM_Barrel_Rusty", "destination": "/Game/Prophouse/Meshes/SM_Barrel_Rusty", "content_hash": "9f2c...e41a", "lineage_id": "guid:6F96...64FF", "action": "add" }],
"skipped_identical": [],
"conflicts": [],
"failed": []
}

Every item has exactly asset_name, destination, content_hash and lineage_id, plus: action (add or safe_overwrite) on added items; conflict items carry resolution (keep_local, overwrite or skip); failed items carry error. A done report must account for every closure file and contain no failed items. Reports are limited to 5,000 items and 2 MiB, and unknown fields are rejected (422).