Back to home
Entity API docs

Store data. Validate it. Query it. Inspect its structure. Review changes.

An HTTP API for storing JSON or EDN data, validating tenant-owned entity shapes, querying data by id, type, or attributes, inspecting inferred structure, and reviewing every change.

JSON and EDN are both supported natively. Clojure services can exchange native data structures directly - including maps, keywords, vectors, booleans, and deeply nested data - and request EDN responses without needing to convert data to JSON first.

Auth

Include x-api-key in tenant-scoped requests. The key identifies the tenant and limits access to its data, validations, and schema inspection. Keys can be read-write or read-only; read-only keys can query and validate data but cannot store, delete, or manage validations.

Formats

EDN is supported alongside JSON for Clojure applications. Send content-type: application/edn or content-type: application/json with request bodies. Add .edn or .json to choose the response format.

Limits

Body max: 1 MB. Batch max: 20 entities. Query pagination sizes: 20, 50, or 100.

History

Track every change

Every create, update, upsert, and delete operation is recorded automatically. Even after an entity is soft-deleted or hard-deleted, its history and version snapshots can still be queried for auditing and traceability.

Evict permanently removes everything for that entity: the entity itself and all associated history records.

POST /entities/history

Lists version, change type, type metadata, actor, request id, reason, and timestamp. It does not return entity data or diffs.

POST /entities/history/version

Returns the entity snapshot for a specific recorded version when you need the previous state.

Tenant validations

What an account gets

Each account can have one or more tenants. Each tenant gets its own validation catalog, entity records, API keys, and validation version history. The x-api-key decides which tenant is being used, so callers do not pass tenant ownership in the body.

A read-write key can define validations and persist entities. A read-only key can list and run validations, but cannot change definitions or write entity data.

Validation catalog

Store reusable Malli schemas by validation id, such as person, supplier, order, or inspection-result. Updating a validation creates the next version instead of overwriting the audit trail.

Primitive validators

Built-in primitive validations cover common checks like non-blank strings and email addresses. Tenant schemas can compose those primitives with standard Malli forms.

Validate only

Use validate when an app needs a yes/no answer and error details before deciding what to do next. Nothing is stored by validation-only requests.

Persist if valid

Use persist-if-valid when valid data should be written immediately. Invalid data returns 422 with validation errors and is not stored.

Core endpoints

What the API does

Store data, validate it, delete it, query it, and inspect its structure.

GET /health

Check service health

Returns service status and deployed version. Add .json or .edn to choose the response format.

POST /entities

Create an entity

Stores one entity for the current tenant. Provide id, type, and data. If id is omitted, the server creates one.

POST /queries/find-entity-by-id

Find one entity

Returns a single entity by id.

POST /queries/find-entities-by-type

Find entities by type

Returns entities with a matching type using page, page-size, sort-by, and sort-direction.

POST /queries/find-entities-by-attributes

Find entities by attributes

Finds entities whose data matches the supplied attributes.

POST /entities/upsert

Upsert an entity

Requires an id. Updates the entity when it exists, or creates it when it does not.

GET /validations/primitives

List primitive validations

Returns built-in primitive validation ids and descriptions, such as email-address and non-blank-string.

POST /validations/primitives/{primitive-id}/validate

Validate with a primitive

Validates one supplied value against a built-in primitive validation.

GET /validations

List tenant validations

Returns active validation definitions for the authenticated tenant.

POST /validations

Define a validation

Stores a tenant-owned Malli schema and creates a validation version.

POST /validations/validate

Validate a value

Validates a value against a stored tenant validation or a provided Malli schema.

POST /entities/persist-if-valid

Persist if valid

Validates first and only upserts the entity when the submitted value is valid.

POST /entities/update

Update an entity

Updates an active entity with the supplied id. Returns 404 when the entity does not exist.

POST /entities/batch

Create multiple entities

Stores up to 20 entities. Defaults to one transaction, or set transaction false for per-entity success and failure results.

POST /entities/delete

Delete an entity

Soft delete marks the entity as deleted without removing its history. Hard delete removes the entity but keeps its history.

POST /entities/evict

Evict an entity

Permanently removes the entity and its full history.

POST /entities/history

List entity history

Returns append-only change metadata for one entity, including versions, change types, reasons, and request ids.

POST /entities/changes

List entity changes

Alias for entity history. Use it when change language fits the calling system better than history language.

POST /entities/history/version

Get one history version

Returns the entity snapshot for one recorded version.

POST /queries/recent-entities-by-type

Get recent entities by type

Returns the 20 most recently updated entities for a type.

POST /schema

Inspect one object structure

Accepts a JSON or EDN object and reports top-level fields and inferred types. This endpoint does not require an API key.

POST /schema/by-entity-id

Inspect one stored entity

Fetches one entity by id and reports the inferred structure for its stored data.

POST /schema/sample-by-type

Inspect stored data by type

Samples recent entities of a type and shows common and uncommon fields.

Check health

Use this to confirm the service is running and to see the deployed version.

Example
curl https://shipdeploy.work/health.json

Create an entity

The most common way to store data. If id is omitted, the server uses a random UUID string. Schema inspection can show the structure of the stored data.

Example
curl -X POST https://shipdeploy.work/api/v1/entities.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"id":"person-ada","type":"person","data":{"name":"Ada","active":true,"department":"research"}}'

Find one entity

Use this when you already know the id. If the entity belongs to another tenant, it behaves the same as not found.

Example
curl -X POST https://shipdeploy.work/api/v1/queries/find-entity-by-id.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"id":"person-ada"}'

Find entities by type

Allowed page sizes are 20, 50, and 100. Supported sort fields are id, type, created-at, and updated-at.

Example
curl -X POST https://shipdeploy.work/api/v1/queries/find-entities-by-type.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"type":"person","page":1,"page-size":20,"sort-by":"updated-at","sort-direction":"desc"}'

Find entities by attributes

The attributes must match values in the entity data. This is useful for simple status, department, owner, or category filters.

Example
curl -X POST https://shipdeploy.work/api/v1/queries/find-entities-by-attributes.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"attributes":{"department":"research","active":true},"page-size":20}'

Upsert by id

Use upsert when you already have the id and want one request to insert or replace the current entity data.

Example
curl -X POST https://shipdeploy.work/api/v1/entities/upsert.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"id":"person-ada","type":"person","data":{"name":"Ada Lovelace","active":true}}'

Define a validation

Tenant validations are Malli schemas. They can compose built-in primitive validations and are versioned by validation id.

Example
curl -X POST https://shipdeploy.work/api/v1/validations.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"validation-id":"person","name":"Person","schema":["map",["first-name","non-blank-string"],["last-name","non-blank-string"],["age",["and","int",[">",18],["<",65]]]]}'

Validate a value

Use this when you need validation without storing an entity.

Example
curl -X POST https://shipdeploy.work/api/v1/validations/validate.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"validation-id":"person","value":{"first-name":"Ada","last-name":"Lovelace","age":36}}'

Persist if valid

This endpoint replaces the old validate-then-persist coordinator flow: it validates first, then writes through the normal entity upsert path and records history. The validation schema should match the submitted entity envelope, including id, type, and data.

Example
curl -X POST https://shipdeploy.work/api/v1/validations.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"validation-id":"person-entity","name":"Person entity","schema":["map",["id","non-blank-string"],["type",["=","person"]],["data",["map",["first-name","non-blank-string"],["last-name","non-blank-string"],["email","email-address"]]]]}'
Example
curl -X POST https://shipdeploy.work/api/v1/entities/persist-if-valid.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"validation-id":"person-entity","entity":{"id":"person-ada","type":"person","data":{"first-name":"Ada","last-name":"Lovelace","email":"ada@example.com"}}}'

Update by id

Use update when the entity already exists. The id is required, and missing or soft-deleted entities return 404.

Example
curl -X POST https://shipdeploy.work/api/v1/entities/update.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"id":"person-ada","type":"person","data":{"name":"Ada Lovelace","active":true}}'

Create multiple entities

Send 1 to 20 entities. By default the server stores them in one transaction. If any insert fails, none are saved. The 1 MB request limit applies to the whole batch, so large entities may need smaller batches. Set transaction to false to process each entity independently and receive per-entity success and failure results.

Example
curl -X POST https://shipdeploy.work/api/v1/entities/batch.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"entities":[{"id":"person-ada","type":"person","data":{"name":"Ada"}},{"type":"person","data":{"name":"Grace"}}]}'
Example
curl -X POST https://shipdeploy.work/api/v1/entities/batch.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"transaction":false,"entities":[{"id":"person-ada","type":"person","data":{"name":"Ada"}},{"id":"person-grace","type":"person","data":{"name":"Grace"}}]}'

Soft delete

Soft delete marks the entity as deleted without removing its history. Standard query endpoints exclude soft-deleted entities, but you can still list history metadata and fetch recorded version snapshots.

Example
curl -X POST https://shipdeploy.work/api/v1/entities/delete.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"id":"person-ada","mode":"soft"}'

Hard delete

Hard delete removes the entity but keeps its history. You can still query previous history and versions after the entity itself is gone.

Example
curl -X POST https://shipdeploy.work/api/v1/entities/delete.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"id":"person-ada","mode":"hard"}'

Evict

Evict permanently removes the entity and its full history. Use it only when both the current record and its audit history should be cleared.

Example
curl -X POST https://shipdeploy.work/api/v1/entities/evict.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"id":"person-ada"}'

List history

Every write records an append-only change. Soft-deleted and hard-deleted entities can still be queried here unless they have been evicted. The list endpoint returns metadata only, without entity data or diffs.

Example
curl -X POST https://shipdeploy.work/api/v1/entities/history.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"id":"person-ada","page":1,"page-size":20}'

List changes

This is the same response as history, exposed with change-oriented naming for systems that prefer it.

Example
curl -X POST https://shipdeploy.work/api/v1/entities/changes.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"id":"person-ada","page":1,"page-size":20}'

Get a version

Fetch a specific version when you need the entity snapshot as it existed after that change.

Example
curl -X POST https://shipdeploy.work/api/v1/entities/history/version.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"id":"person-ada","version":2}'

Get recent entities by type

This fixed query returns the latest 20 records for a type, sorted by updated-at descending.

Example
curl -X POST https://shipdeploy.work/api/v1/queries/recent-entities-by-type.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"type":"person"}'

Inspect one object

Use this before saving data, or when you want to understand the structure of one JSON or EDN object.

Example
curl -X POST https://shipdeploy.work/api/v1/schema.json \
  -H 'content-type: application/json' \
  -d '{"object":{"name":"Ada","age":37,"active":true,"tags":["research"]}}'

Inspect one stored entity

Use this when a record already exists and you want the schema for that entity's stored data.

Example
curl -X POST https://shipdeploy.work/api/v1/schema/by-entity-id.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"id":"person-ada"}'

Sample stored entities

Use this once records exist. The result shows common fields and exception fields across a recent sample.

Example
curl -X POST https://shipdeploy.work/api/v1/schema/sample-by-type.json \
  -H 'content-type: application/json' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{"type":"person","sample-size":20}'
For Clojure developers

Use EDN directly from Clojure

Ship Deploy supports EDN for both requests and responses so Clojure apps can send maps, keywords, vectors, booleans, and nested data without converting data to JSON first.

Use content-type: application/edn when the request body is EDN. Use accept: application/edn or a .edn extension when you want an EDN response.

Clojure clients can parse responses with clojure.edn/read-string.

Example
curl -X POST https://shipdeploy.work/api/v1/entities.edn \
  -H 'content-type: application/edn' \
  -H 'accept: application/edn' \
  -H 'x-api-key: replace-with-your-api-key' \
  -d '{:id "person-ada" :type "person" :data {:name "Ada" :active true :tags ["research"]}}'
Example
(import '[java.net URI]
        '[java.net.http HttpClient HttpRequest HttpRequest$BodyPublishers HttpResponse$BodyHandlers])
(require '[clojure.edn :as edn])

(def payload
  {:id "person-ada"
   :type "person"
   :data {:name "Ada" :active true}})

(def response
  (.send (HttpClient/newHttpClient)
         (-> (HttpRequest/newBuilder (URI/create "https://shipdeploy.work/api/v1/entities.edn"))
             (.header "content-type" "application/edn")
             (.header "accept" "application/edn")
             (.header "x-api-key" "replace-with-your-api-key")
             (.POST (HttpRequest$BodyPublishers/ofString (pr-str payload)))
             (.build))
         (HttpResponse$BodyHandlers/ofString)))

(edn/read-string (.body response))

Common responses

The API uses a small set of standard HTTP status codes.

401Unauthorized

Missing or invalid API key.

403Forbidden

Read-only API key attempted a write operation.

404Not Found

Route or entity was not found.

409Conflict

Entity id already exists. Pick a new id or query the existing record.

413Content Too Large

Request body exceeded the 1 MB limit. For batch creation, reduce the number of entities or the size of each entity.