AvalloneAPI

API design guidelines

How every endpoint of the Avallone API works, and how we change and retire endpoints.

These are the rules every endpoint in the Avallone API follows. Once you know them, you can predict how an unfamiliar endpoint behaves: its path, how it identifies records, what it returns, and what its errors look like.

The rules follow common practice from larger API providers. They draw on Google's API Improvement Proposals, the Microsoft REST API Guidelines and the Zalando RESTful API Guidelines.

Scope

The guidelines cover every endpoint of the API, including bulk endpoints such as PUT /sync.

  • Every new endpoint follows them from its first release.
  • Some existing endpoints don't follow every rule yet. They are brought in line over time, and each change is announced in the changelog.
  • Deprecated endpoints are not brought in line. They are retired through the deprecation process instead.

Basics

  • The base URL is https://api.avallone.io, and every path starts with /api/v1.
  • The API is only served over HTTPS, with TLS 1.2 or later.
  • Request and response bodies are JSON, encoded as UTF-8.
  • Every request needs a Bearer token in the Authorization header. See Authentication and permissions.

Paths

  • Resources are plural nouns in lowercase kebab-case: /companies, /related-companies, /company-document-types.
  • A single item sits under its collection: /companies/{companyId}.
  • A resource that only exists as part of another one is nested under it: /companies/{companyId}/documents/{documentId}.
  • Data that you read, but that isn't a field of the record, gets a sub-path named after what it returns. You read it with GET, for example GET /companies/{companyId}/documents/{documentId}/content.
  • An action that isn't create, read, update or delete gets a sub-path named after the action, as a kebab-case verb. You call it with POST. For example, an action that archives a company would be POST /companies/{companyId}/archive.
  • Path parameters are named after their resource, with an Id suffix: {companyId}, {officerId}, {documentId}, {relationId}. On by-ref paths, the parameter is {referenceId}.
  • Query parameters are camelCase, like field names.

Identifiers

Every record has an internal ID. It is a UUID that Avallone generates, and it never changes.

Companies and officers can also have a reference ID. That is the ID from your own system, which you send when you create or sync the record.

  • A reference ID is 1 to 255 characters long. Any character is allowed except control characters.
  • It is stored as you sent it, but compared without regard to case: ACME-1 and acme-1 are the same reference ID.
  • It is unique per resource type within your tenant. Once a record is deleted, its reference ID can be used again.
  • You can change it with PATCH. From then on, the old reference ID no longer finds the record.
  • Don't put personal data, such as names, email addresses or national ID numbers, in a reference ID.

In paths:

  • {companyId}, {officerId}, {relationId} and so on are always internal IDs.
  • To address a record by your reference ID, use the by-ref form of the path: /companies/by-ref/{referenceId} or /officers/by-ref/{referenceId}. Every operation on the internal-ID path is also available on its by-ref form, including the sub-resources below it.
  • A path uses by-ref at most once, for the top-level record. Anything below it is addressed by internal ID: /companies/by-ref/{referenceId}/documents/{documentId}. There is no path such as /companies/by-ref/{referenceId}/officers/by-ref/{officerReferenceId}.
  • URL-encode a reference ID when you put it in a path.
  • An internal ID in a path that isn't a valid UUID returns 400.

In request bodies, when a body points at another record, you can identify it in either of two ways:

  • {resource}Id, the internal ID, for example companyId;
  • {resource}ReferenceId, your reference ID, for example companyReferenceId.

Send at least one. If you send both, they must point at the same record. Otherwise you get 400.

If the record you point at doesn't exist, you get 404, with a not_found entry in errors that points at the field. See Telling 404s apart.

In responses, every record includes its id. Records that can have a reference ID also include referenceId, which is null when none is set.

Methods

MethodOn a collectionOn an item
GETLists the itemsReturns the item
POSTCreates one or more items—
PUTReplaces the whole collection, where offeredReplaces the item, where offered
PATCH—Changes only the fields you send
DELETE—Deletes the item

A method an endpoint doesn't support returns 405.

Every method except POST is idempotent: sending the same request twice leaves the same result as sending it once. POST becomes safe to repeat when you send an Idempotency-Key.

POST creates

  • POST creates and never updates. If the record already exists, for example with the same reference ID, you get 409 and nothing changes.
  • A body with one object creates one record. The response is 201, with the record in the body and its path in the Location header.
  • A body with a list of objects creates several records. The response is 201, with the created records under results.

PATCH updates

PATCH is the way to update a record. Every item that can be changed has a PATCH.

The body follows JSON Merge Patch (RFC 7396). Send it as application/merge-patch+json; application/json is accepted too.

  • Fields you leave out keep their value.
  • To clear a field, send null.
  • A nested object is merged field by field, by the same rules.
  • A list is replaced as a whole. To add one item to a list, send the complete new list.

PATCH is safe even when we add fields you don't know about yet, because it only touches what you send.

PUT on an item replaces it

New items get PUT only when it is useful to mirror a complete record from your own system. Otherwise, use PATCH. Where an item has PUT, it works like this:

  • The body has the same fields as the GET response, so a record you read can be sent back as it is.
  • Writable fields you leave out are cleared. Sending null clears a field too.
  • Sub-resources that have their own path are not touched. A PUT on a company doesn't change its documents or its relations. Change those through their own paths.
  • PUT never creates. A PUT to a record that doesn't exist returns 404, also on a by-ref path. This way a typo in a reference ID can't create a duplicate. To create or update in one call, use a bulk endpoint such as /sync.

PUT has a cost when we add a field. A client that doesn't know the new field drops it when it reads the record, so its next PUT clears it. If-Match doesn't prevent this. It only stops you from overwriting a change someone else made (see Concurrency). If you use PUT, keep the fields you don't know when you read a record, and send them back unchanged. Better still, use PATCH.

PUT on a collection replaces the collection

Some collections, such as a company's relations, offer PUT. Only collections nested under a record do, such as /companies/{companyId}/related-companies. A top-level collection, such as /companies, never offers PUT. To write many top-level records in one call, use a bulk endpoint.

A collection PUT makes the stored collection match your request:

  • Each item you send is matched to an existing item. It matches by id if you send one. Otherwise it matches by the key named on the endpoint's page.
  • Matched items are updated in place and keep their id.
  • New items are created.
  • Existing items you didn't send are deleted.
  • Sending [] empties the collection.

A collection PUT deletes whatever you leave out, so a list that was cut short on your side deletes data. Send If-Match with the ETag from your last GET of the collection.

DELETE

  • DELETE returns 204 when it deleted something.
  • If nothing matches the ID, it returns 404, also on a repeated call. So you can always tell a real delete from a call that changed nothing.

Writes are all or nothing

A request either applies completely or changes nothing. This includes requests that touch several items, such as a POST with a list or a collection PUT. If one item fails, nothing is saved, and the error points at the item that failed.

Bulk endpoints are the one exception.

Requests

Content:

  • Request bodies are JSON, sent as application/json. PATCH also accepts application/merge-patch+json.
  • A POST that creates several items, or a PUT that replaces a collection, sends the items as a bare JSON array. Response lists are wrapped in an object so they can gain fields later. These requests don't need that room, because their options go in query parameters. Bulk endpoints such as /sync have their own body shape, described on their page.
  • File uploads use multipart/form-data. Each file comes with a metadata entry:
    • By default, entries are matched to files by position. The first entry describes the first file, and so on.
    • To match by name instead, add fileName to each entry, set to the filename of its file part. When fileName is present, it takes precedence over position.
    • Either every entry has fileName or none does. A fileName that matches no file, or more than one, returns 400.
  • Text is stored exactly as you send it. We don't trim it, change its case or normalize its Unicode, unless the field's description says so. For example, email addresses are trimmed and stored in lowercase.

Limits:

  • A JSON request body can be up to 1.5 MB.
  • Each endpoint's page names its other limits, such as the largest file it accepts or the most items a list can have.
  • A body over the limit returns 413. A list with too many items, or text that is too long, returns 400.

Fields:

Every field has one of these behaviours, shown on the endpoint's page and in the OpenAPI specification:

BehaviourMeaning
RequiredYou must send it when you create the record.
OptionalYou can leave it out.
Read-onlyAvallone sets it, for example id, url and createdAt. It is ignored in requests, so a record you read can be sent back as it is.
Create-onlyYou can set it when you create the record, but not change it later. Sending a different value later returns 400.
  • A field the endpoint doesn't know returns 400. The error names the field. This catches typos, and fields that were removed.
  • A query parameter the endpoint doesn't know also returns 400.
  • An empty string is a value, not "no value". To clear a field, send null. A required text field rejects an empty string.

Responses

Shape:

  • Successful responses are JSON, sent as application/json.
  • A single record is a JSON object, with no wrapper.
  • A list is always a JSON object with the items under results, never a bare array. A list that can grow without limit is also paginated, with its paging details under metadata. A JSON object can gain new fields later; a bare array can't.
  • POST, PUT and PATCH return the records as they are after the change, in the same shape as GET.

Fields every record has:

FieldDescription
idThe internal ID
urlThe path where you can GET the record, relative to the base URL, for example /api/v1/companies/3f2a1c40-9c1e-4c7f-9c6b-2b0f7a5d1e88
createdAtWhen the record was created
updatedAtWhen the record last changed

Records that belong to a tenant also include tenantId: the ID of the tenant the record belongs to. It is always your own tenant. tenantId is read-only. It is ignored in requests, so a record you read can be sent back as it is.

Every field in a record's schema is present in every response. A field without a value is null, or [] for a list. It is never left out.

Headers:

  • Every response carries an X-Request-Id header. Error bodies repeat it as requestId. Quote it when you contact support.
  • Responses are compressed with gzip when the request sends Accept-Encoding: gzip.

File downloads:

A content sub-path, such as GET /companies/{companyId}/documents/{documentId}/content, returns the file itself:

  • A record that has a file gives this path in contentUrl, next to its own url. Follow it rather than building the path yourself.
  • The file is streamed in the response body. There is no redirect to another URL.
  • Content-Type is the file's media type, and Content-Length its size in bytes.
  • Content-Disposition: attachment carries the file name.
  • The whole file is always returned. Range requests aren't supported.

Status codes

CodeWhen
200 OKA read or update succeeded. The body holds the result.
201 CreatedA POST created something. The body holds what was created.
204 No ContentA DELETE succeeded. There is no body.
207 Multi-StatusA bulk endpoint finished, and at least one item failed. The body holds a result per item.
400 Bad RequestThe request is invalid: a malformed ID, a missing required field, a wrong type, an unknown field or parameter, an unknown enum value, or conflicting identifiers.
401 UnauthorizedThe token is missing, malformed, expired or revoked. The response carries WWW-Authenticate: Bearer.
403 ForbiddenThe token is valid, but it lacks the permission the endpoint needs, or its API user is deactivated.
404 Not FoundThe path matches no endpoint, the record in the path doesn't exist, or a record named in the body doesn't exist. The error says which.
405 Method Not AllowedThe endpoint doesn't support the method. The Allow header lists the methods it supports.
406 Not AcceptableThe Accept header doesn't allow any type the endpoint can return.
409 ConflictThe request clashes with stored data: a duplicate, or a relation that would create an ownership cycle. Also returned, with Retry-After, while an earlier request with the same Idempotency-Key is still running.
410 GoneThe endpoint has been removed. The error names its replacement.
412 Precondition FailedThe If-Match header doesn't match the record's current ETag. Someone else changed it.
413 Content Too LargeThe request body is over the endpoint's size limit.
415 Unsupported Media TypeThe Content-Type isn't one the endpoint accepts.
422 Unprocessable ContentAn Idempotency-Key was reused with a different request body.
429 Too Many RequestsYou went over a rate limit.
500 Internal Server ErrorSomething failed on our side.
502, 503, 504We are briefly unable to serve the request. Retry with backoff.

Errors

Every error response we send uses the Problem Details format (RFC 9457), sent as application/problem+json:

{
  "type": "https://api.avallone.io/problems/validation_failed",
  "title": "Validation failed",
  "status": 400,
  "detail": "2 fields are invalid.",
  "code": "validation_failed",
  "requestId": "0b8f7c2e-5d1a-4f0e-9a43-6c2d1e7b9f10",
  "errors": [
    {
      "pointer": "/incorporationCountryCode",
      "code": "invalid_format",
      "detail": "Must be an ISO 3166-1 alpha-2 country code."
    },
    {
      "pointer": "/name",
      "code": "required",
      "detail": "Must not be empty."
    }
  ]
}
FieldDescription
typeA URI that identifies the kind of problem. It always ends in the code.
titleA short summary of the kind of problem. It is the same every time the problem occurs.
statusThe HTTP status code, repeated in the body.
detailWhat went wrong in this request, written for people.
codeA stable, machine-readable code. See the table below.
requestIdThe ID of the request. Quote it when you contact support.
errorsOne entry per input that has a problem, such as an invalid field or a record named in the body that doesn't exist. [] when the problem isn't tied to a specific input.

title and detail are in English.

Each entry in errors says where the problem is and what it is:

FieldDescription
pointerWhere the problem is in the request body, as a JSON Pointer (RFC 6901). In a request with several items, it starts with the item's index, for example /2/companyId.
parameterThe path or query parameter with the problem. An entry has either pointer or parameter.
codeWhat is wrong: required, invalid_type, invalid_format, invalid_value, out_of_range, too_long, too_many_items, unknown_field, create_only or not_found. We may add codes.
detailWhat is wrong, written for people.

Branch on code. Show detail to people, but don't parse it: its wording can change. The codes are:

codeStatusMeaning
validation_failed400Some input is invalid. errors says which.
unauthenticated401The token is missing, malformed, expired or revoked.
permission_denied403The token lacks the permission, or its API user is deactivated.
endpoint_not_found404The path matches no endpoint. detail names the method and path you sent.
not_found404A record in the path or body doesn't exist. detail names it, and for a record in the body, errors points at the field.
method_not_allowed405The endpoint doesn't support the method.
not_acceptable406The endpoint can't return a type the Accept header allows.
already_exists409A record with the same unique value already exists.
ownership_cycle409The relation would make a company own itself, directly or indirectly.
idempotency_key_in_use409An earlier request with the same Idempotency-Key is still running.
gone410The endpoint has been removed. detail names the replacement.
precondition_failed412The record changed since you read it.
content_too_large413The request body is over the size limit.
unsupported_media_type415The Content-Type isn't accepted.
idempotency_key_reused422The Idempotency-Key was used before with a different body.
rate_limited429You went over a rate limit.
internal500Something failed on our side.
unavailable503We are briefly unable to serve the request.

A 502 or 504 can come from the network in front of the API, without a problem body. Handle those by their status code.

Telling 404s apart

Every 404 has a problem body, so a mistyped path never looks like a missing record. Check code to see what wasn't found:

What wasn't foundcodeExample detail
The endpoint. The path is wrong, for example a typo.endpoint_not_foundNo endpoint matches GET /api/v1/compnies.
The record in the path. The path is right, but no record has that ID.not_foundCompany 3f2a… doesn't exist.
A record named in the body. errors points at the field.not_foundCompany 3f2a… doesn't exist.

Lists

Pagination

Paginated lists are split into numbered pages. The query parameters are:

ParamDefaultDescription
page0The page to return. The first page is 0. A negative value returns 400.
limit15Items per page, from 1 to 100. A value outside that range returns 400.

A paginated response:

{
  "results": [],
  "metadata": {
    "currentPage": 0,
    "totalPages": 5,
    "limit": 15,
    "nextPageUrl": "/api/v1/activities?page=1&limit=15"
  }
}
  • nextPageUrl is null on the last page.
  • nextPageUrl repeats your filters and sort. Follow it rather than building the next URL yourself.
  • If you build the URL yourself, send the same filters and sort on every page.
  • A page after the last one returns 200 with an empty results.
  • A list with no matches returns 200 with an empty results. It never returns 404 or 204.
  • Pages are counted from the start of the list each time you ask for one. If records are added or deleted while you page through, the rest shift. A record can then appear on two pages, or on none.

Filtering

  • Filters are query parameters named after the field they filter on: ?incorporationCountryCode=DK.
  • Values of the same filter are combined with OR. Repeat the parameter to send several: ?incorporationCountryCode=DK&incorporationCountryCode=SE returns companies in Denmark or Sweden.
  • Different filters are combined with AND. ?incorporationCountryCode=DK&incorporationCountryCode=SE&type=parent returns parent companies in Denmark or Sweden.
  • There is no OR across different filters. To get companies in Denmark or of type parent, make two requests.
  • An unknown value for an enum filter returns 400. It never silently returns nothing.
  • A filter on a record that doesn't exist, such as an unknown companyId, returns an empty list, not 404.

These naming rules apply to lists of records. The activity log is a query over events, which have nested fields such as actor.userId and resources[].type, so its page names its own filters.

Time ranges use from and to:

  • from is inclusive and to is exclusive. Back-to-back windows such as from=09:00&to=10:00, then from=10:00&to=11:00 never return the same record twice.
  • Both take an RFC 3339 timestamp, such as 2026-03-01T09:00:00Z, or a calendar date. A date means midnight UTC at the start of that day. So from=2026-03-01&to=2026-04-01 covers all of March.
  • When a list can filter on more than one time field, the parameters are prefixed with the field: createdFrom, createdTo, updatedFrom, updatedTo.

Finding changes. Lists of records that can change accept updatedFrom and updatedTo, and can be sorted by updatedAt. To keep your system in sync, ask for the records updated since your last run, instead of reading everything again. Sort by updatedAt in ascending order.

Warning

Paging through changes isn't guaranteed to return every record. If a record changes while you page through, it moves to the end of the list and the records after it shift back by one. One of them can then be skipped.

Deleted records don't appear in lists, so updatedFrom can't tell you about them. To find deletions, read the activity log. It records them as activity types such as company-deleted and company-document-deleted.

Search. Where a list offers free-text search, the parameter is q: ?q=acme. The endpoint's page says which fields it searches.

Sorting

  • Every paginated list has a fixed default order, named on the endpoint's page. Records that tie on it are ordered by id, so the order is always stable.
  • Where an endpoint lets you choose the order, use sort with a field name: ?sort=createdAt. Prefix it with - for descending order: ?sort=-createdAt. To sort by several fields, repeat the parameter.
  • Sorting on a field the endpoint doesn't list returns 400.

Field names and formats

KindFormatExample
Field namescamelCaseincorporationCountryCode
Abbreviations in namesWritten as a wordurl, lei, nextPageUrl
ListsPlural nameaddresses, roles
Yes/no fieldsBoolean, named is… or has…isListed, hasLicenses
References to other records{resource}Id and {resource}ReferenceIdcompanyId, companyReferenceId
Internal IDsUUID3f2a1c40-9c1e-4c7f-9c6b-2b0f7a5d1e88
Points in timeRFC 3339 timestamp2026-03-14T09:21:44.512Z
Calendar datesISO 8601 date, no time2026-03-14
CountriesISO 3166-1 alpha-2DK
PercentagesNumber from 0 to 100, at most 4 decimals51.25
Enum valueslowercase snake_caseparent, related_company
  • Responses give timestamps in UTC, with milliseconds.
  • A timestamp in a request must have seconds, and must end with Z for UTC or an offset such as +01:00, as RFC 3339 requires. A timestamp with neither returns 400. It is never assumed to be UTC, because the API can't know which time zone the client meant.
  • Use a calendar date for things that happen on a day, such as a date of birth, a role start date or an expiry date. Use a timestamp for things that happen at an instant, such as createdAt.

Enum values are lowercase snake_case. Error codes are enum values too, so they follow the same format.

  • An enum value is a stable code, not display text. Where people need a readable name, the response has a …Label field next to it, for example legalForm: "limited_liability_company" and legalFormLabel: "Limited Liability Company". The label's wording can change; the value can't.
  • The activity log is the exception. Its activity types and resource types come from Avallone's internal activity log and keep that format, such as document-uploaded and CompanyDocument. Its page lists them.

Concurrency

Two clients can change the same record at the same time. To stop one from silently overwriting the other, records carry a version tag, the ETag.

  • GET on a record returns an ETag header. So does GET on a collection that offers PUT.
  • Send that value back in an If-Match header on PUT, PATCH or DELETE. If the record changed since you read it, you get 412 and nothing changes. Read it again, reapply your change, and retry.
  • If-Match is optional. We recommend it for every PUT, because a PUT built from an old copy overwrites newer changes.

Note

Caching with the ETag isn't part of the API. Sending it in If-None-Match to get 304 Not Modified isn't supported, so don't rely on it. We may add it in the future.

Retries and idempotency

Idempotency-Key makes a POST safe to retry. Without it, a POST that timed out might have created the record, and retrying could create a second one. The header follows the IETF Idempotency-Key draft.

  • Send a unique value, such as a new UUID, in the Idempotency-Key header of each POST. Use the same value when you retry that POST.
  • We remember the key, per tenant, for 24 hours.
  • A retry with the same key and the same body gets the original response again, with the same status and body. It also carries the header Idempotent-Replayed: true.
  • The same key with a different body returns 422.
  • If the first request is still running, a retry returns 409 with a Retry-After header. Wait that long, then retry.

When to retry:

ResponseWhat to do
429, 503Wait for the time in Retry-After, then retry.
502, 504, a timeout or a dropped connectionRetry with exponential backoff and random jitter.
500Retry once or twice with backoff. If it keeps failing, contact support with the requestId.
Any other 4xxDon't retry. Change the request first.

Only retry a request that is safe to repeat: GET, PUT, PATCH, DELETE, and POST with an Idempotency-Key. Because writes are all or nothing, a request that got an error response changed nothing. A request that timed out, or whose connection dropped, may or may not have been applied.

A retried DELETE can return 404 if the first attempt succeeded but its response was lost. Treat that 404 as success.

Bulk endpoints

A bulk endpoint, such as PUT /sync, is a dedicated endpoint that takes many items in one request and handles each one on its own. One item failing doesn't stop the others. This is the only kind of write that isn't all or nothing.

A collection PUT or a POST with a list also writes several items, but it isn't a bulk endpoint. It follows the usual rule: if one item fails, nothing is saved.

  • If the request as a whole is invalid, for example it isn't valid JSON, you get 400 and nothing is processed.
  • Otherwise every item gets a result. The response is 200 if every item succeeded, and 207 if at least one failed.
  • Results are in the same order as the items you sent:
{
  "results": [
    {
      "index": 0,
      "referenceId": "acme-inc",
      "status": "created",
      "id": "3f2a1c40-9c1e-4c7f-9c6b-2b0f7a5d1e88",
      "error": null
    },
    {
      "index": 1,
      "referenceId": "globex",
      "status": "failed",
      "id": null,
      "error": {
        "type": "https://api.avallone.io/problems/validation_failed",
        "title": "Validation failed",
        "status": 400,
        "detail": "1 field is invalid.",
        "code": "validation_failed",
        "errors": [
          { "pointer": "/name", "code": "required", "detail": "Must not be empty." }
        ]
      }
    }
  ]
}
FieldDescription
indexThe item's position in the request, from 0
referenceIdThe item's reference ID, if it has one
statuscreated, updated, unchanged, deleted or failed
idThe internal ID of the record, or null if the item failed before a record existed
errorWhy the item failed, in the error format, with pointers relative to the item. null when it succeeded.

Rate limits

Requests are limited per tenant.

  • Every response carries these headers:

    HeaderDescription
    X-RateLimit-LimitHow many requests the current window allows
    X-RateLimit-RemainingHow many are left in the current window
    X-RateLimit-ResetWhen the window resets, in seconds since the Unix epoch
  • If you go over the limit, you get 429 with a Retry-After header saying how many seconds to wait.

The IETF is working on standard RateLimit headers, but the draft still changes between revisions. We will add them once it is final, alongside these.

Authentication and permissions

  • Every request needs a Bearer token: Authorization: Bearer <token>. See Authentication.
  • Never send a token in a query string. URLs end up in logs.
  • Treat a token as an opaque string. Don't decode it or rely on what it contains.
  • Each endpoint needs one permission, named on its page. A token without that permission gets 403.
  • Tokens expire after 1 year by default. When you create a token, you can choose an expiry from 3 months to 3 years instead.
  • You can have several active tokens. To rotate one without downtime, create the new token, switch your client to it, then revoke the old one.

Note

How a token gets its permissions, and when a removed permission stops working, is still being decided. Until then, see Authentication for how it works today.

Versions and changes

The major version is in the path: /api/v1. We evolve v1 in place, under the rules below, instead of starting a v2.

Every release has a version number in the changelog, and the OpenAPI specification shows the current one in info.version.

These changes are non-breaking. We make them without notice and list them in the changelog:

  • new endpoints;
  • new optional request fields (for items you replace with PUT, see what a new field means for PUT);
  • new response fields;
  • new enum values;
  • new error codes;
  • changes to the wording of title, detail and descriptions.

Your client should ignore response fields it doesn't know. It should also handle an enum value or error code it doesn't know without failing. For an unknown error code, fall back to the HTTP status.

These changes are breaking:

  • removing or renaming an endpoint, a field or an enum value;
  • changing a field's type or format;
  • adding a required request field, or making an optional one required;
  • making a response field nullable that never was;
  • rejecting a value that used to be accepted, for example by adding a maximum length;
  • changing a default value, a page size or a sort order;
  • changing which status code or error code a situation returns;
  • changing what an operation does with the data you send;
  • requiring a different permission for an endpoint.

How we make a breaking change. We avoid breaking changes. When one is needed, it follows the deprecation process:

  1. We announce the change in the changelog, with the date it takes effect.
  2. We contact every tenant whose traffic it affects.
  3. The change takes effect at least 6 months after the announcement, or earlier if every affected tenant agrees to an earlier date.

We group breaking changes so they take effect together, on at most two dates a year. You update your integration once per date, instead of for each change.

On its effective date, a breaking change applies to the existing path. This is the only way an endpoint's behaviour changes under the same path, and it is always announced first.

Deprecation

We deprecate something when a better way exists and we want you to move to it. We can deprecate any of these:

  • an endpoint;
  • a request or response field;
  • an enum value;
  • a behaviour, such as an endpoint accepting a field it shouldn't.

A deprecated item works exactly as before until the date it is removed.

How you find out

When we deprecate something, we announce it in all of these places at once:

  • The changelog. The entry says what is deprecated, why, what replaces it, and the date it will be removed.
  • The OpenAPI specification. The item is marked deprecated. Its description names the replacement and the removal date.
  • Response headers, on every call to a deprecated endpoint:
    • Deprecation (RFC 9745): when the endpoint was deprecated.
    • Sunset (RFC 8594): when it will be removed.
    • Link with rel="deprecation": the changelog entry.
  • A direct message to every tenant that used it in the last 90 days.

The headers describe whole endpoints. A deprecated field, enum value or behaviour is announced through the changelog, the specification and direct messages.

We recommend logging the Deprecation and Sunset headers in your client, so a deprecation can't go unnoticed.

Timeline

  1. Announcement. The item is marked deprecated. It keeps working unchanged, and new features go to its replacement.
  2. Notice period. At least 6 months pass between the announcement and the removal date.
  3. Reminder. One month before the removal date, we contact every tenant that is still using it.
  4. Removal. This happens on or after the removal date, once the item has gone unused for 30 days. If a tenant is still using it, we contact them and agree on a new date.

Rules that apply while something is deprecated

  • If you send a deprecated field together with its replacement, both must have the same value. Otherwise you get 400, as with {resource}Id and {resource}ReferenceId.
  • A deprecated response field keeps being returned, with the same value, until it is removed.
  • A deprecated enum value is still accepted in requests. Responses switch to the replacement value on the removal date.

After removal

  • A removed endpoint returns 410 Gone. The error names the replacement.
  • A request that still sends a removed field with a value gets 400, with an unknown_field error naming it. We never silently ignore a value.
  • A removed field sent as null is ignored. A client that reads a record and sends it back in a PUT, with a model that still has the old field, keeps working.

OpenAPI specification

The specification is at https://api.avallone.io/docs/api, and as JSON at https://api.avallone.io/docs/api-json. It uses OpenAPI 3.1, and describes every endpoint with:

  • the base URL in servers, and the current release in info.version;
  • each endpoint grouped under a tag for its resource;
  • the property names exactly as they appear in the JSON;
  • each field's behaviour: required, readOnly, and create-only fields named in their description;
  • the request and response schemas, including the error schema for every error status;
  • every status code the endpoint can return, and the headers described on this page;
  • an example for every request and response, which passes the endpoint's own validation.

Names follow fixed patterns, so generated clients get predictable class and method names:

WhatPatternExamples
operationIdVerb and resource, camelCase. The verbs are list, get, create, update (PATCH), replace (PUT) and delete.listCompanies, getCompany, updateCompany
Record schemasThe resource, singular, PascalCaseCompany, RelatedCompany
Request schemasVerb and resource, with RequestCreateCompanyRequest, UpdateCompanyRequest
List schemasResource and ListCompanyList
Error schemaProblemProblem

If the specification and an endpoint's behaviour disagree, that is a bug. Please tell us.

On this page