Skip to main content

REST API (rest-1.v1)

Overview​

rest-1.v1 is the general-purpose data API. Every asset, every attribute and every past version of both is addressable by URL. GET reads, POST writes.

It is a data API, not a task API: rather than a fixed catalogue of endpoints, it exposes one URL shape that is parameterised by asset type, so it grows automatically as the schema does. Ask the Meta API what asset types and attributes exist, then address them here.

Prerequisites​

  • A running Agility instance and its application path, for example http://localhost/VersionOne.
  • Credentials. Unlike meta.v1, rest-1.v1 requires authentication, and every request is subject to the calling member's permissions. See Security & errors.
  • A working understanding of assets, attributes and moments.

The URL Shape​

All requests are made against a single template:

~/rest-1.v1/{QueryType}/{AssetType}/{AssetID}/{Moment}/{AttributeName}?{QueryString}
SegmentRequiredMeaning
QueryTypeyesData, Hist or New — see below.
AssetTypeyesThe asset type token, for example Story, Defect, Member.
AssetIDnoInteger identifying one asset. Omit it to address the whole list.
MomentnoInteger identifying one past version of that asset.
AttributeNamenoAn attribute token, to address a single attribute rather than the whole asset.
QueryStringnoSelection, filtering, sorting, paging, search and operations.

Each segment narrows the result. /Data/Story is every Story you can see; /Data/Story/1364 is one Story; /Data/Story/1364/903 is that Story as it was at moment 903; /Data/Story/1364/903/Name is a single attribute of that version.

The Three Namespaces​

QueryTypePurposeResponse root
DataCurrent data. Query with GET, create and update with POST.Assets
HistHistorical data. Every version of every matching asset, read-only.History
NewA template for a new asset, pre-populated with defaults and rules.Asset

New is worth knowing about: rather than guessing which attributes a new asset needs, GET /rest-1.v1/New/Story?ctx=Scope:1000 returns a skeleton asset already filled in for that context. See New asset templates.

Query String Parameters​

ParameterPurpose
selWhich attributes to return.
selectUndocumented alias for sel. Takes precedence when both are sent.
whereFilter a list of assets.
withBind variables referenced by where.
sortOrder a list of assets.
pagePage size and start position, in that order — page=<size>,<start>.
findFree-text search across attributes.
findinRestrict which attributes find searches.
asofReturn data as it existed at a given moment or date and time.
deletedBoolean. Include deleted assets in the result.
needTotalBoolean. Compute the total count. The default comes from the ApiDefaultNeedTotal instance setting.
opExecute an operation. POST only, with an AssetType and AssetID.
AcceptOverride content negotiation when you cannot set headers. See Content types.
Not every parameter applies to every request shape

On a single-asset request in the Data namespace such as /Data/Story/1364, the parameters that only make sense for a list — where, sort, page and find — are accepted and silently ignored. No error is raised, so a query that looks like it filtered may simply have returned the asset unchanged. sel, with, asof, deleted and needTotal are applied on that same URL, so this is not a case of "list parameters don't work here" — some work and some are dropped.

This is a property of the Data namespace, not of single-asset URLs in general: /Hist/Story/1364 is also a single-asset request, and there where, sort, page and find all take effect. History requests also gain an implicit ascending Moment sort when sort is absent.

The query grammar reference publishes the full applicability matrix along with the syntax of each parameter.

note
needTotal and paging

If you drive a pager off the total attribute of the response, set needTotal=true explicitly rather than relying on the instance default. When needTotal is off, a paged response omits total entirely — it is not written as -1 or any other sentinel. (Unpaged responses always carry a real total.) Because the default comes from the instance-configurable ApiDefaultNeedTotal setting, a pager that works against one instance can silently break against another.

Reading Data​

Ask for a list, choose your attributes, and filter:

Stories with no remaining effort, owned by Member 20
curl -u "$V1_USER:$V1_PASSWORD" \
"~/rest-1.v1/Data/Story?sel=Name,ToDo&where=ToDo='0';Owners='Member:20'"
<Assets total="1" pageSize="2147483647" pageStart="0">
<Asset href="/VersionOne/rest-1.v1/Data/Story/1364" id="Story:1364">
<Attribute name="Name">Company History</Attribute>
<Attribute name="ToDo">0</Attribute>
</Asset>
</Assets>

Every Asset element carries an href you can follow and an id that is the asset's OID.

The same read as JSON (Accept: application/json) wraps every value in an Attributes map, renders a relation as a Relation whose value is one { "idref": … } or a list of them, and renders AssetState as a number (see AssetState values). Selecting a relation also brings its display attributes along uninvited - Owners.Name and Owners.Nickname beside Owners, Status.Name beside Status - as ordinary attributes, whether or not you asked for them:

…&sel=Name,Owners,AssetState — what comes back
{
"_type": "Assets",
"total": 1,
"pageSize": 1,
"pageStart": 0,
"Assets": [
{
"_type": "Asset",
"href": "/VersionOne/rest-1.v1/Data/Story/1364",
"id": "Story:1364",
"Attributes": {
"Name": { "_type": "Attribute", "name": "Name", "value": "Company History" },
"Owners": {
"_type": "Relation",
"name": "Owners",
"value": [{ "_type": "Asset", "href": "…/Member/20", "idref": "Member:20" }]
},
"Owners.Name": { "_type": "Attribute", "name": "Owners.Name", "value": ["Alfred Smith"] },
"Owners.Nickname": { "_type": "Attribute", "name": "Owners.Nickname", "value": ["alfred"] },
"AssetState": { "_type": "Attribute", "name": "AssetState", "value": 128 }
}
}
]
}

An unset single relation renders "value": null; an empty multi-value one "value": []. total is present only when needTotal=true. Query examples works through thirteen progressively more involved queries, from a single asset to text search and history.

Writing Data​

Updates are a POST to the URL of the asset, with a body describing only what should change. Each change is marked with an act:

actApplies toEffect
setScalars, single relationsReplace the value.
addMulti-value relationsAdd a value to the collection.
removeMulti-value relationsRemove a value from the collection.
Update one attribute
curl -u "$V1_USER:$V1_PASSWORD" \
-H "Content-Type: text/xml; charset=utf-8" \
--data-binary '<Asset><Attribute name="Phone" act="set">555-555-1212</Attribute></Asset>' \
"~/rest-1.v1/Data/Member/20"
warning
act goes on the value element, and the wrong placement is silent

On a multi-value relation, act belongs on the Asset element inside the Relation, not on the Relation element itself:

<Asset><Relation name="Owners"><Asset idref="Member:20" act="add"/></Relation></Asset>

Putting it on the Relation — <Relation name="Owners" act="add"> — returns 200, consumes a moment, and adds nothing. The response looks like success, so read the relation back when a relation update matters.

The response echoes the saved asset at a URL that includes the moment the change created, giving you a permanent address for exactly this version:

<Asset href="/VersionOne/rest-1.v1/Data/Member/20/173" id="Member:20:173">
<Attribute name="Phone">555-555-1212</Attribute>
</Asset>

Creating an asset is the same request without an AssetID — POST to /rest-1.v1/Data/Story — and all required attributes must be supplied or the save fails. Update examples covers scalars, single-value relations, multi-value relations and creation.

Recording Why a Change Was Made​

Add a comment query-string parameter to any write to attach an audit comment to that change — the same field the UI records when a user edits an asset:

curl -u "$V1_USER:$V1_PASSWORD" -X POST -H "Content-Type: text/xml; charset=utf-8" --data-binary '<Asset><Attribute name="Estimate" act="set">5</Attribute></Asset>' "${AGILITY_BASE_URL}/rest-1.v1/Data/Story/1024?comment=Re-estimated%20after%20grooming"

It works on operations too — ?op=Inactivate&comment=…. The comment is not an attribute you set; it is read back from history as ChangeComment.

Operations​

Some state changes are not attribute edits. Closing, reopening, deleting and similar transitions are operations, invoked with op on a POST:

curl -u "$V1_USER:$V1_PASSWORD" -X POST \
"~/rest-1.v1/Data/Story/1021?op=Delete"

Which operations an asset type supports, and when each is valid, is described by meta.v1. See Operations.

A redundant operation is not reliably rejected

Some redundant invocations fail a validation rule — Inactivate on an already inactive asset is a 400 Violation'AssetCannotInactivateClosed — but others succeed: Reactivate on an Active asset returns 200 and writes a new moment every time, changing nothing but the history. A 200 from an operation proves the request was accepted, not that a transition happened; check the asset's state when it matters.

History​

Swap Data for Hist and you get every version of every matching asset instead of just the current one, with each version addressable by moment. Add asof to ask what things looked like at a point in time:

curl -u "$V1_USER:$V1_PASSWORD" \
"~/rest-1.v1/Hist/Story/1364?asof=2005-08-03T17:30:00.00&sel=Name,ToDo,ChangeDate"

Moments are global to the instance and therefore sparse per asset, which has real consequences for how you walk history. Moments & history explains the model in full.

Content Types​

XML is the canonical format: responses default to text/xml, and that is the format the in-product reference and every example in these pages uses.

The handler also negotiates other representations. Set an Accept header — or, if you cannot set headers, an Accept query string parameter — to one of:

AcceptResponse
text/xml, application/xml, */*XML (the default).
application/json, text/jsonJSON.
application/jsonxJSON with a more explicit, XML-shaped structure.

An unrecognised Accept value is rejected with 406 Not Acceptable.

POST bodies are read as XML unless the request's Content-Type begins with application/json, in which case a JSON payload of this shape is accepted:

JSON equivalent of the update above
{
"Attributes": {
"Phone": { "value": "555-555-1212", "act": "set" }
}
}

Relations take an OID token as their value, and multi-value relations take an array whose entries each carry their own act:

{
"Attributes": {
"Scope": { "value": "Scope:0", "act": "set" },
"Members": {
"value": [
{ "idref": "Member:1000", "act": "add" },
{ "idref": "Member:1001", "act": "remove" }
]
}
}
}
warning
The concise api/asset JSON is not accepted here

rest-1.v1 requires the Attributes wrapper shown above. The concise Bulk API shape — { "from": "Story:1207", "update": { … } } — is rejected with 400 Invalid request payload: missing Attributes. The two surfaces take different JSON: use Attributes on rest-1.v1, and the from/update form on api/asset. A payload written for one does not work on the other.

XML is better documented

The JSON request and response formats are implemented and supported, but the in-product API reference documents only XML, so XML is the safer choice when you need behaviour to be predictable across versions. Prefer XML unless you have a specific reason not to.