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.
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.
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.
Body max: 1 MB. Batch max: 20 entities. Query pagination sizes: 20, 50, or 100.
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.
Lists version, change type, type metadata, actor, request id, reason, and timestamp. It does not return entity data or diffs.
Returns the entity snapshot for a specific recorded version when you need the previous state.
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.
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.
Built-in primitive validations cover common checks like non-blank strings and email addresses. Tenant schemas can compose those primitives with standard Malli forms.
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.
Use persist-if-valid when valid data should be written immediately. Invalid data returns 422 with validation errors and is not stored.
What the API does
Store data, validate it, delete it, query it, and inspect its structure.
Check service health
Returns service status and deployed version. Add .json or .edn to choose the response format.
Create an entity
Stores one entity for the current tenant. Provide id, type, and data. If id is omitted, the server creates one.
Find one entity
Returns a single entity by id.
Find entities by type
Returns entities with a matching type using page, page-size, sort-by, and sort-direction.
Find entities by attributes
Finds entities whose data matches the supplied attributes.
Upsert an entity
Requires an id. Updates the entity when it exists, or creates it when it does not.
List primitive validations
Returns built-in primitive validation ids and descriptions, such as email-address and non-blank-string.
Validate with a primitive
Validates one supplied value against a built-in primitive validation.
List tenant validations
Returns active validation definitions for the authenticated tenant.
Define a validation
Stores a tenant-owned Malli schema and creates a validation version.
Validate a value
Validates a value against a stored tenant validation or a provided Malli schema.
Persist if valid
Validates first and only upserts the entity when the submitted value is valid.
Update an entity
Updates an active entity with the supplied id. Returns 404 when the entity does not exist.
Create multiple entities
Stores up to 20 entities. Defaults to one transaction, or set transaction false for per-entity success and failure results.
Delete an entity
Soft delete marks the entity as deleted without removing its history. Hard delete removes the entity but keeps its history.
Evict an entity
Permanently removes the entity and its full history.
List entity history
Returns append-only change metadata for one entity, including versions, change types, reasons, and request ids.
List entity changes
Alias for entity history. Use it when change language fits the calling system better than history language.
Get one history version
Returns the entity snapshot for one recorded version.
Get recent entities by type
Returns the 20 most recently updated entities for a type.
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.
Inspect one stored entity
Fetches one entity by id and reports the inferred structure for its stored data.
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.
curl https://shipdeploy.work/health.jsonCreate 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.
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.
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.
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.
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.
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.
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.
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.
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"]]]]}'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.
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.
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"}}]}'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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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}'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.
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"]}}'(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.
Missing or invalid API key.
Read-only API key attempted a write operation.
Route or entity was not found.
Entity id already exists. Pick a new id or query the existing record.
Request body exceeded the 1 MB limit. For batch creation, reduce the number of entities or the size of each entity.