API v3 documentation

Requests use your account’s Aha! domain and the permissions of the user associated with your API key or OAuth token.

Download the OpenAPI specification

JSON REST API for the Aha! product development platform.

Choosing an API version

We recommend API v3 for new projects wherever it supports the functionality you need. It provides more consistent conventions across endpoints, richer filtering, and efficient pagination for working with large collections of records.

API v3 offers:

  • More flexible filtering. Combine filters on supported record fields, related records, and custom fields to retrieve the data you need and reduce filtering in your own code.
  • Efficient retrieval of large datasets. Default list requests use cursor-based pagination, letting you work through large collections without increasingly expensive page offsets.
  • More control over responses. Request specific fields and supported related records to keep responses focused and reduce unnecessary data transfer.
  • A consistent interface for developers and AI tools. Shared conventions for requests, responses, filtering, pagination, and errors reduce endpoint-specific rules. Machine-readable OpenAPI documentation helps development tools and LLMs understand the available operations and construct requests.

API v3 does not yet cover every operation available in API v1. Use v1 for functionality that is not available in v3. You can use both versions within the same integration, with the same API key, and adopt v3 incrementally.

Authentication

Authenticate with an API key or OAuth2 access token in the Authorization header:

Authorization: Bearer <your_api_key_or_oauth_access_token>

Generate an API key in Settings > Personal > Developer > API key. See the API key documentation for details.

To authorize an application to access Aha! on a user's behalf, register your application and follow the OAuth2 authorization flow.

Requests use the permissions of the user associated with the key or token, who must have access to the account identified by the URL's subdomain.

Fields

Responses include a small set of default fields when fields is omitted. Pass a comma-separated fields query parameter to return only the fields you need: /api/v3/features/PROJ-1?fields=id,name,status. Include default to keep the default fields and add others, for example fields=default,assigned_to_user.

Select fields within an associated record or collection using dot notation or bracket shorthand. For example, fields=id,status.id,status.name and fields=id,status[id,name] are equivalent. The same syntax works for collections, such as fields=id,requirements[id,name]. Use default within brackets to include the associated resource's defaults plus other fields: requirements[default,assigned_to_user].

Collections can only be selected on the top-level resource. Selecting a collection on a nested resource, such as fields=feature[requirements] or fields=feature[tags], returns a fields.nested_collection error. Unknown fields return a fields.unknown error.

Custom fields are omitted unless requested. Select individual custom fields by API key with fields=default,custom_fields.priority or fields=default,custom_fields[priority,effort]. Request bare custom_fields to return all supported custom fields.

Create and update responses honor fields and also include fields submitted in the record body. Submitted association IDs return the corresponding embedded association, and submitted custom field keys are returned without separately requesting them in fields.

Filtering

Use filter.{operator}[{field}]=value query parameters on list endpoints. Multiple filters combine with AND logic. Each endpoint lists its filterable fields and the operators supported for each field type.

For example, /api/v3/features?filter.contains[name]=login&filter.eq[status.name]=In%20progress returns features whose names contain "login" and whose status is "In progress".

Use comma-separated values for in and not_in, and two comma-separated bounds for between (inclusive). Pass true for empty and not_empty. Fields with a fixed set of choices accept their documented labels.

Sub-attribute filtering

Filter on attributes of associated records using dot notation: filter.{operator}[field.attribute]=value. For example, filter.eq[status.name]=In%20progress or filter.in[tags.name]=Backend,API.

Supported operators depend on the subfield's type and filterability. Compare associated records using subfields such as status.id or status.name. Bare associations support emptiness checks, not equality comparisons.

Custom field filtering

Use the custom field's API key: filter.{operator}[custom_fields.{key}]=value. Text, URL, number, date, and checkbox custom fields support operators for their corresponding field type. Other custom field types are not supported.

Pagination

List endpoints return records and a pagination object. Use per_page to set the page size (default 25, maximum 200).

When pagination.has_more is true, follow pagination.next_url or pass pagination.next_cursor as the after query parameter with the same filters and sort. Treat cursors as opaque values. When has_more is false, next_cursor and next_url are null.

Default order and retrieving all records

Without a sort parameter, records are returned by ID in descending order. This mode supports pagination through all matching records without a total record limit.

Sorting and the record limit

Use sort.asc={field} or sort.desc={field} to sort by a field listed by the endpoint. Supplying either parameter, including an explicit sort by ID, limits results to the first 1,000 records across all pages.

If more records exist beyond this limit, the final page has pagination.truncated: true, an explanatory pagination.truncated_message, and pagination.has_more: false. Attempting to paginate beyond the limit returns pagination.max_records_exceeded.

To retrieve all matching records, remove sort.asc and sort.desc and restart without an after cursor. Then follow the pagination links in the default descending ID order.