Appearance
Migrating to v2
v2 addresses everything by uuid instead of the integer id. Authentication, pagination, filters and errors work exactly as in v1, so for most integrations the move is a matter of switching ids and a few field names.
v1 keeps working. It receives bug fixes but no new features: new endpoints — such as recurring work steps — and webhooks exist in v2 only. You can move one resource at a time; both versions read and write the same data.
In short
| v1 | v2 | |
|---|---|---|
| Base path | /v1/… | /v2/… |
| Key of a resource | id (integer) | uuid |
| Path parameter | /v1/assignment-events/4711 | /v2/assignment-events/01a14371-… |
| Reference to another resource | assignmentId: 12 | assignmentUuid: "01a1…" |
| List of references | assignedUsers: [3, 4] | userUuids: ["01a1…", "01a2…"] |
| Linked address, written | "company" or an object | { "link": "company" } or { "link": "custom", … } |
*.deleted webhook | — | { "uuid": "…" } |
Before you switch: collect the uuids
Every v1 resource already returns its uuid and can be filtered by it. If you store Artesa ids, store the uuid next to them while you are still on v1:
http
GET /v1/assignment-events?size=20&page=0json
{ "items": [{ "id": 4711, "uuid": "01a14371-cc14-7524-8225-672c59cef75a", "assignmentId": 12, … }] }Once every stored id has its uuid, the switch is a lookup instead of a migration. References to other resources (assignmentId, typeId, …) need the uuid of the referenced resource: read it from that resource's v1 list, or let v2 tell you once you read the referencing resource there.
You can also set the uuid yourself when creating a resource, in v1 and v2 alike. It has to be a UUIDv7; a generated key lets you retry a create without creating twice.
What changes
Identifiers
- The
idproperty is gone;uuidis the key, in responses and in paths. - Every reference ends in
Uuidand holds a uuid:assignmentId→assignmentUuid,typeId→typeUuid,groupId→groupUuid,userId→userUuid, and so on. - A reference to a record of another company, or to none at all, answers 400
Unknown <property> <uuid>, as an unknown id does in v1. - Filters follow the property names:
?assignmentId=12becomes?assignmentUuid=01a1…,id[$in]becomesuuid[$in].
Lists of assigned users and gangs
assignedUsers and assignedGangs become userUuids and gangUuids. They behave as before: a patch replaces the whole list, [] removes everyone, and a gang brings its members along.
Addresses
v1 took a linked address as a string and an own address as an object, but returned both as an object with link. v2 reads and writes the same shape:
json
// v1 — write
{ "addressStartFrom": "company", "addressDestination": { "street": "Hauptstr. 1", "city": "Berlin" } }
// v2 — read and write
{
"addressStartFrom": { "link": "company" },
"addressDestination": { "link": "custom", "street": "Hauptstr. 1", "city": "Berlin" }
}link is company, customer, assignment or custom. null still clears an address, and patching an own address with custom changes it in place. A plain string is rejected in v2.
Webhooks
Webhooks are only available in v2, managed at /v2/webhook-subscriptions. data is the resource as its v2 GET returns it, and *.deleted carries only the uuid.
Types are checked strictly
v1 converts a value to the documented type: "name": 1 is stored as "1", "isOutward": "true" as true. v2 answers 400 instead. Only dates are read from strings, as JSON has no dates: send them as ISO 8601.
What stays the same
- The API key and the
Authorization: Bearer …header. page,size,pagesanditemsof every list, and the filter syntax.- Error responses and status codes.
externalIdon every resource that has one.completedAtandconfirmedAttake a date,null, ortruefor now andfalsefor none.
Resource by resource
assignment-events
| v1 | v2 |
|---|---|
id | uuid |
assignmentId | assignmentUuid |
typeId | typeUuid |
groupId | groupUuid |
assignedUsers | userUuids |
assignedGangs | gangUuids |
addressStartFrom, addressDestination, addressReturnTo | same names, written as { "link": … } |
| — | seriesUuid, seriesOccurrenceDate (new) |
An event that is outward — set explicitly or by its type — starts and returns at the company and goes to the assignment, unless you give its addresses.
assignment-event-group-types
| v1 | v2 |
|---|---|
id | uuid |
Read-only, as in v1. New: a name filter.
assignment-event-groups
| v1 | v2 |
|---|---|
id | uuid |
typeId | typeUuid |
assignmentId | assignmentUuid |
A group keeps its type: a patch with typeUuid is rejected with 400, where v1 accepted typeId and ignored it.
assignment-events/{assignmentEventUuid}/uploads
| v1 | v2 |
|---|---|
/v1/assignment-events/{assignmentEventId}/uploads/{id} | /v2/assignment-events/{assignmentEventUuid}/uploads/{uploadUuid} |
id, categoryId, assignmentEventId | uuid, categoryUuid, assignmentEventUuid |
- The list holds the event's files only; each is also on its assignment and listed there.
- Get, update and delete answer 404 for an upload that isn't on the event in the path; in v1 any upload of the company could be reached through any event.
- The create is still multipart;
uuid,descriptionandcategoryUuidgo next tofile. A missingfileor an invalid field is a 400. - An unknown or another company's event is a 400 on list and create.
assignment-status-types
| v1 | v2 |
|---|---|
id | uuid |
Read-only, as in v1. New: name and category filters; category takes null for status types without one.
assignments
| v1 | v2 |
|---|---|
id, /v1/assignments/{assignmentId} | uuid, /v2/assignments/{assignmentUuid} |
customerId, projectId, typeId, statusId | customerUuid, projectUuid, typeUuid, statusUuid |
responsibleUserId | responsibleUserUuid |
address: { …fields } | address: { link: "company" | "customer" } or { link: "custom", …fields }; null removes it |
- An assignment's address links to the company's or the customer's address, or is its own (
custom). - v1 changed whatever address the assignment had in place, a linked one too. In v2,
customchanges only an own address in place; on a linked one it creates a new own address, and the company's or customer's address stays as it is. - An address in v1's shape (fields without
link) is a 400.
assignments/{assignmentUuid}/uploads
| v1 | v2 |
|---|---|
/v1/assignments/{assignmentId}/uploads/{id} | /v2/assignments/{assignmentUuid}/uploads/{uploadUuid} |
id, categoryId, assignmentId | uuid, categoryUuid, assignmentUuid |
- The list holds every file on the assignment: its own and those of its events, event groups and forms. An event's files are also listed under that event's uploads.
- Get, update and delete answer 404 for an upload that isn't on the assignment in the path; in v1 any upload of the company could be reached through any assignment.
- The create is still multipart;
uuid,descriptionandcategoryUuidgo next tofile. A missingfileor an invalid field is a 400. - An unknown or another company's assignment is a 400 on list and create.
customers
| v1 | v2 |
|---|---|
id, /v1/customers/{customerId} | uuid, /v2/customers/{customerUuid} |
billingAddress optional, the address's fields | always present: null or { link: "custom", …fields }, written the same way; null removes it |
telephones, emails, urls: [] is ignored | [] removes the stored values; a list replaces all of them |
telephones documented as at most one | one per label (main, mobile, fax), so up to three |
isDefault in written numbers and emails | not accepted (it never did anything); still read |
gangs
| v1 | v2 |
|---|---|
id, userIds | uuid, userUuids (a patch replaces the whole list) |
order optional on the item | always present |
externalId filter | also takes null |
| — | deactivatedAt filter (date or null) |
projects
| v1 | v2 |
|---|---|
id, /v1/projects/{projectId} | uuid, /v2/projects/{projectUuid} |
POST with an existing number returns that project | 409 Conflict |
externalId in a patch: no length limit | at most 255 characters |
roles
| v1 | v2 |
|---|---|
id | uuid |
Read-only, as in v1. Users reference their role as roleUuid.
time-entries
| v1 | v2 |
|---|---|
id | uuid |
typeId | typeUuid |
userId | userUuid |
assignmentId | assignmentUuid |
eventId | eventUuid |
eventTypeId | eventTypeUuid |
The filters are renamed the same way and take null for entries without a type, assignment or event. endsAt is required on create and can't be set to null on a patch (400): an entry keeps its end. externalId takes at most 255 characters on a patch too.
time-entry-types
| v1 | v2 |
|---|---|
id | uuid |
eventTypeId | eventTypeUuid |
Time entry types still can't be deleted; set deactivatedAt instead. A patch leaves out what it doesn't send: v1 reset an omitted order to 1.
time-sheet-days
| v1 | v2 |
|---|---|
id, userId | uuid, userUuid |
approvedById, declinedById, manipulatedById | approvedByUuid, declinedByUuid, manipulatedByUuid |
startsAt, endsAt, breakTime, duration on create and patch | startsAtManual, endsAtManual, breakTimeManual, durationManual |
GET /v1/time-sheet-days/by-user?userId=…&day=… | POST /v2/time-sheet-days/by-user with { "userUuid": …, "day": … } |
startsAt, endsAt, breakTime and duration are the effective values the day calculation settles on, and read-only: sending one is a 400. The manual overrides are read and written under their own names; null drops one, and the effective value falls back to the calculated one.
A status change the day doesn't allow — re-submitting a submitted day, submitting an approved one — answers 400 naming the allowed statuses, where v1 ignores it silently. declinedNote is only accepted together with status: "declined".
by-user creates the day it doesn't find, so it is a POST now; it answers 200 with the day either way.
time-sheet-months
| v1 | v2 |
|---|---|
id, userId | uuid, userUuid |
month: "2026-05-01" | month: "2026-05", in filters too |
sumTarget: 0 without working-time rules | sumTarget: null, as on weeks |
PATCH with { "status": … } | POST /v2/time-sheet-months/{uuid}/submit, /approve, /decline, /reset |
GET /v1/time-sheet-months/by-user?userId=…&month=… | POST /v2/time-sheet-months/by-user with { "userUuid": …, "month": "2026-05" } |
status is read-only; a patch takes note, carryInManual, carryOutManual and externalId, and null drops a carry override. decline takes an optional declinedNote, set on every day it declines. Months now also show carryInManual and carryOutManual. Each action moves the month's days that can make the step (reset back to draft) and answers the month once its status is recomputed. by-user answers 200 with the month, found or created.
time-sheet-weeks
| v1 | v2 |
|---|---|
id, userId | uuid, userUuid |
PATCH with { "status": … } | POST /v2/time-sheet-weeks/{uuid}/submit, /approve, /decline, /reset |
GET /v1/time-sheet-weeks/by-user?userId=…&weekStart=… | POST /v2/time-sheet-weeks/by-user with { "userUuid": …, "weekStart": … } |
status is read-only; a patch takes only externalId. decline takes an optional declinedNote, set on every day it declines. Each action moves the week's days that can make the step (reset back to draft) and answers the week once its status is recomputed. by-user answers 200 with the week, found or created.
upload-categories
| v1 | v2 |
|---|---|
id, roleIds | uuid, roleUuids (a patch replaces the whole list) |
order nullable | always an integer ≥ 1 |
| — | createdAt, updatedAt on the item and as filters |
| — | name filter |
user-absence-types
| v1 | v2 |
|---|---|
id | uuid |
Every field of a type is always returned; color and deactivatedAt may be null. A create may set color: null. A patch leaves out what it doesn't send: v1 reset an omitted needApproval, needCompensation and order to their defaults. New: name, createdAt and updatedAt filters.
user-absences
| v1 | v2 |
|---|---|
id | uuid |
typeId | typeUuid |
userId | userUuid |
needApprovalById | needApprovalByUuid |
declinedById | declinedByUuid |
compensatedById | compensatedByUuid |
The …ByUuid filters take null to find absences without one. The status filter takes operators ($in, $ne, …). requestedAt is null only for drafts made in Artesa; a create keeps a given one and takes the time of the request otherwise. externalId takes at most 255 characters on a patch too.
users
| v1 | v2 |
|---|---|
id | uuid |
roleId | roleUuid |
A user's role is set at create: a patch with roleUuid is rejected with 400, where v1 accepted roleId and ignored it. A patch keeps an omitted color, where v1 reset it to #FFFFFF. The deactivatedAt and externalId filters take null.
New in v2
/v2/assignment-event-series: recurring work steps, read-only. A series' occurrences are ordinary assignment events withseriesUuidandseriesOccurrenceDateset; patching or deleting one changes only that occurrence. Series are created about twelve months ahead, so setting one up sends oneassignment-event.createdper occurrence.
Checklist
- Store the
uuidof every Artesa record you keep, while still on v1. - Switch the base path to
/v2and address records byuuid. - Rename references (
…Id→…Uuid) and assignment lists (assignedUsers→userUuids,assignedGangs→gangUuids), in bodies and filters. - Write linked addresses as
{ "link": … }. - On time sheets, write day overrides as
…Manual, replace status patches of weeks and months with their actions, andby-userGETs with POSTs. - Subscribe to webhooks at
/v2/webhook-subscriptionsif you poll for changes today.