Approvals in Codenica API
Start working with Approvals through Codenica API by creating an API key in Codenica settings. If you have not created one yet, open Codenica API - introduction in a new tab. It explains the common key creation, secret storage and authentication rules.
The technical object name is approval, and its collection name in the API is approvals. An approval stores a request, the person responsible for the decision, descriptive data and links to process objects. It can also have files and a pin.
The important difference from an ordinary update is that the decision result is not written directly to status. An approval is approved or rejected through a dedicated decision endpoint. This allows the API to check that the correct approver is acting and that the record has not changed since it was read.
The examples use the identifier PUBLIC-API-APPROVAL-20260906060644. Replace it with an identifier from your integrating application and adapt UUIDs and field values to your database.
Approvals - API address and installation choice
All Approval routes start with:
{BASE_URL}/api/v1/approvalsBASE_URL is the Codenica server address without the /api/v1 suffix. In Cloud, use the actual domain assigned to the company:
export BASE_URL="https://{actual-company-domain}"In the default On-Premise installation, the address registered locally by Codenica Discovery is:
export BASE_URL="http://codenica.local:5150"If an administrator has made the installation available through a company domain, reverse proxy, HTTPS or another port, use the exact address provided for that installation:
export BASE_URL="https://{actual-installation-address}"Do not use localhost when the integrating application runs on a different computer than the API. The target database is selected from the request host. Do not send tenantId in the body, query string or an additional header.
Approvals - API key scopes
The key used for Approvals should contain only the scopes required by the particular integration. The complete module scope set is:
approvals:read
approvals:write
approvals:delete
approvals:schema
approvals:stats
approvals:relationships:read
approvals:relationships:write
approvals:users:read
approvals:files:read
approvals:files:write
approvals:technical:read
approvals:technical:write
approvals:pin:write
approvals:decision:write
users:readFor lists and record reads, choose approvals:read. Creating and editing require approvals:write, and deletion requires approvals:delete. Add relationship, file, statistics, technical, pin and decision scopes only when the integration will use those operations.
If the integration searches for relationship targets, it also needs the appropriate read scopes for the collections from which targets are selected, such as notes:read, worktasks:read, requesteditems:read, tickets:read, changes:read, problems:read or releases:read. A key scope does not replace the user permissions.
Approvals - authentication
Authenticate every Codenica API request with two headers:
export CLIENT_ID="cna_your_client_id"
export CLIENT_SECRET="cns_your_client_secret"
curl --request GET --url "$BASE_URL/api/v1/approvals?page=1&pageSize=25" \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET"An external integration does not need an administrator JWT or Codenica panel cookies. Store the secret in a server-side secret store. Do not place it in browser-delivered code, a repository, a URL, shell history or logs. Use HTTPS outside local testing.
Save meta.requestId from responses. It helps locate a particular request in logs, but it is not the Approval UUID and it is not a secret.
Approvals - verify the connection context
Before the first write, retrieve the context. This confirms that the address reaches the right database and that the key has the required scopes and limits:
curl --request GET --url "$BASE_URL/api/v1/context" \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET"Verify data.apiVersion, data.contractVersion, data.tenant, data.caller.authentication equal to api_key, approvals in data.capabilities.resources, the key scopes and the limits.
If the context points to another company or lacks a required scope, correct the address or create a key with the required permissions. Do not try to reach another database by sending a foreign tenantId.
Approvals - schema and relationship targets
The schema is the source of truth for current fields, their types, writability and permitted relationship targets:
curl --request GET --url "$BASE_URL/api/v1/approvals/schema" \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET"The response includes data.itemType, data.fields and data.relationshipTargets. For this module, itemType is approval. For every field, check readable, writable, required, technical, unique, maxLength and hasAutoGeneration.
Example schema fragment:
{
"data": {
"itemType": "approval",
"fields": [
{ "name": "description", "type": "string", "writable": true },
{ "name": "level", "type": "string", "writable": true },
{ "name": "status", "type": "string", "writable": false },
{ "name": "pin", "type": "integer", "writable": false }
],
"relationshipTargets": [
{ "targetDataSet": "notes", "targetItemType": "note" },
{ "targetDataSet": "tickets", "targetItemType": "ticket" }
]
}
}Do not build the mapping solely from this example. Retrieve the schema for the actual database before the integration starts and use only the returned fields and targets.
Approvals - business and system fields
The main Approval fields are:
customIdlocation, departmenttag, linkinfo, descriptionlevel, categorystatus, dateApproved, dateRejectedremark, pinid, itemType, creator, updater, dateCreated, dateUpdated, importId, importSource and dateImported are assigned by the system or intended for technical reads. Do not send them in attributes.
creator
updater
dateCreated
dateUpdated
importId
importSource
dateImportedApprovals - available endpoints
The main routes in the approvals module are:
GET /api/v1/approvals
POST /api/v1/approvals
GET /api/v1/approvals/{APPROVAL_ID}
PATCH /api/v1/approvals/{APPROVAL_ID}
DELETE /api/v1/approvals/{APPROVAL_ID}
GET /api/v1/approvals/schema
GET /api/v1/approvals/stats
GET /api/v1/approvals/values
POST /api/v1/approvals:batch
GET /api/v1/approvals/{APPROVAL_ID}/relationships
POST /api/v1/approvals/{APPROVAL_ID}/relationships
POST /api/v1/approvals/{APPROVAL_ID}/relationships:batch
DELETE /api/v1/approvals/{APPROVAL_ID}/relationships/{DATASET}/{TARGET_ID}
GET /api/v1/approvals/{APPROVAL_ID}/user-relationships
GET /api/v1/approvals/{APPROVAL_ID}/files
POST /api/v1/approvals/{APPROVAL_ID}/files
POST /api/v1/approvals/{APPROVAL_ID}/files/{FILE_ID}
DELETE /api/v1/approvals/{APPROVAL_ID}/files/{FILE_ID}
GET /api/v1/approvals/{APPROVAL_ID}/files/{FILE_ID}/content
POST /api/v1/approvals/{APPROVAL_ID}/pin
POST /api/v1/approvals/{APPROVAL_ID}/decisionRead operations require read scopes, while individual mutations require the additional scopes described above. Every request that changes data also requires Idempotency-Key, and operations on an existing record additionally require the current If-Match.
Approvals - listing and pagination
Retrieve Approval lists page by page:
curl --request GET --url "$BASE_URL/api/v1/approvals?itemType=approval&page=1&pageSize=25" \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET"The response contains data.items and page information:
{
"data": {
"items": [
{
"id": "approval-uuid",
"itemType": "approval",
"attributes": {
"customId": "ERP-APPROVAL-2026-0042",
"category": "Procurement",
"status": "Open",
"level": "Supervisor"
},
"meta": {
"etag": "\"etag-value\""
}
}
],
"page": 1,
"pageSize": 25,
"totalItems": 1,
"totalPages": 1,
"hasNextPage": false
},
"meta": {
"requestId": "request-id"
}
}Move to the next page according to hasNextPage. Read the maximum page size from data.capabilities.limits.maxPageSize instead of hard-coding it.
Approvals - search and filters
Use search for text search. For synchronization, a stable customId or UUID is preferable:
curl --silent --show-error -G \
--data-urlencode "search=purchase" \
--data-urlencode "page=1" \
--data-urlencode "pageSize=20" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/approvals"Simple filters can use field names:
curl --silent --show-error -G \
--data-urlencode "status=Open" \
--data-urlencode "category=Procurement" \
--data-urlencode "customId=ERP-APPROVAL-2026-0042" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/approvals"Structural filters use the field:operator:value form:
category:eq:Procurement
level:ne:Assistant
description:contains:monitor
customId:startswith:ERP-
link:notempty:Supported operators include eq, ne, gt, gte, lt, lte, contains, startswith, endswith and notempty. URL-encode filter values, especially when they contain spaces, colons or special characters.
Approvals - field selection and included data
Use fields when you need only part of a response. Include files, relationships and users with include:
curl --silent --show-error -G \
--data-urlencode "fields=id,itemType,customId,location,department,level,category,status" \
--data-urlencode "include=files,relationships,users" \
--data-urlencode "ids={APPROVAL_ID}" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/approvals"Available include values are files, relationships and users. Each may require a separate scope. fields=* does not bypass access to technical fields or change access rules.
You can also filter by createdAfter, createdBefore, updatedAfter, updatedBefore, sort and direction. Check field names against the current schema.
Approvals - statistics and field values
The stats endpoint shows the distribution of data, while values returns values useful for building filters:
curl --silent --show-error -G \
--data-urlencode "field=category" \
--data-urlencode "limit=20" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/approvals/stats"
curl --silent --show-error -G \
--data-urlencode "field=level" \
--data-urlencode "search=super" \
--data-urlencode "limit=20" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/approvals/values"Both requests are read-only and do not change Approvals. The field must be allowed by the schema, and the values depend on records visible to the user.
Approvals - minimal creation
A useful minimal record contains the type, an identifier from the integrating application, a category, a description and the user who must make the decision:
{
"itemType": "approval",
"attributes": {
"customId": "ERP-APPROVAL-0001",
"category": "Procurement",
"description": "Approval for a monitor purchase"
},
"approverId": "{APPROVER_USER_ID}"
}Send it as JSON:
curl --fail-with-body --silent --show-error \
--request POST \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header "Idempotency-Key: erp-approval-create-0001" \
--data-binary @approval.json \
"$BASE_URL/api/v1/approvals"approverId identifies the active Codenica user who will make the decision. It is not a client record identifier or an arbitrary email address.
Approvals - complete creation example
This example contains process information, labels, an approval level and a technical value rule:
{
"itemType": "approval",
"attributes": {
"customId": "PUBLIC-API-APPROVAL-20260906060644-SOURCE",
"location": "Warsaw",
"department": "IT",
"tag": "public-api,approvals,PUBLIC-API-APPROVAL-20260906060644",
"link": "https://codenica.com",
"info": "Approval request created by the Public API complete flow.",
"level": "Supervisor",
"category": "Procurement",
"description": "Approval created through the Codenica Public API."
},
"approverId": "{APPROVER_USER_ID}",
"customValues": [
{
"name": "description",
"valuePattern": "[approval-example] PUBLIC-API-APPROVAL-20260906060644"
}
]
}approverId requires approvals:technical:write. customValues is optional and also requires the technical scope. Use it only for fields permitted by the schema.
In a real integration, choose the approver according to the company process. The user creating the record becomes the requester.
Approvals - create response and idempotency key
A successful creation returns 201 Created, the record identifier and the initial ETag. Store both on the integration side:
{
"data": {
"id": "{APPROVAL_ID}",
"itemType": "approval",
"attributes": {
"customId": "PUBLIC-API-APPROVAL-20260906060644-SOURCE",
"level": "Supervisor",
"category": "Procurement",
"status": "Open"
},
"meta": {
"customId": "PUBLIC-API-APPROVAL-20260906060644-SOURCE",
"etag": "\"{ETAG_AFTER_CREATE}\""
}
},
"meta": {
"requestId": "{REQUEST_ID}",
"etag": "\"{ETAG_AFTER_CREATE}\""
}
}Every mutating request must have its own Idempotency-Key. If the client does not know whether the first request reached the server, send the same body again with the same key:
curl --fail-with-body --silent --show-error \
--request POST \
--header "Content-Type: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header "Idempotency-Key: public-api-approval-source-create-20260906060644" \
--data-binary @approval.json \
"$BASE_URL/api/v1/approvals"Repeating the same request does not create a second Approval. A changed body with the same key is rejected because one key cannot represent two different operations.
Approvals - read one record and its ETag
After creation and before each subsequent change, read the record by UUID:
curl --fail-with-body --silent --show-error \
--request GET \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/approvals/{APPROVAL_ID}?fields=*"The response contains data.attributes and data.meta.etag. The server also returns the same value in an HTTP header:
HTTP/1.1 200 OK
ETag: "{ETAG_AFTER_GET}"After every successful mutation, the ETag may change, including after relationship changes, pinning, a decision or file operations. Always store the value returned by the last successful operation.
Approvals - edit fields with If-Match
A regular update changes business fields only. Do not use it to write the status, decision dates or pin:
curl --fail-with-body --silent --show-error \
--request PATCH \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header 'If-Match: "{ETAG_AFTER_GET}"' \
--header "Idempotency-Key: public-api-approval-update-20260906060644" \
--data '{
"attributes": {
"info": "Updated approval information from the integration.",
"level": "Manager",
"category": "Approved procurement",
"description": "Approval edited through the Codenica Public API."
}
}' \
"$BASE_URL/api/v1/approvals/{APPROVAL_ID}"Success returns 200 OK and a new ETag. Update only fields that actually changed. This makes conflicts easier to resolve and reduces the risk of overwriting data.
Approvals - required If-Match and conflict protection
The current ETag is required to change an existing record. A PATCH without this header returns:
{
"type": "https://docs.codenica.com/errors/if_match_required",
"title": "Precondition required.",
"status": 428,
"code": "if_match_required",
"detail": "Send the ETag returned by GET in the If-Match header."
}If the supplied ETag is stale, the API returns 412 Precondition Failed and the if_match_failed code:
{
"type": "https://docs.codenica.com/errors/if_match_failed",
"title": "Precondition failed.",
"status": 412,
"code": "if_match_failed",
"detail": "The supplied ETag is not the current approval version."
}A request rejected with 412 does not save changes. Read the Approval again, compare the data and only then prepare a deliberate update. Do not automatically overwrite changes made by another person or integration.
Approvals - pinning
Pin is a read-only field changed through a dedicated endpoint. Allowed values are integers from 0 to 3:
curl --fail-with-body --silent --show-error \
--request POST \
--header "Content-Type: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header 'If-Match: "{CURRENT_ETAG}"' \
--header "Idempotency-Key: public-api-approval-pin-20260906060644" \
--data '{"pin":3}' \
"$BASE_URL/api/v1/approvals/{APPROVAL_ID}/pin"To remove the pin, send null through the same endpoint:
curl --fail-with-body --silent --show-error \
--request POST \
--header "Content-Type: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header 'If-Match: "{ETAG_AFTER_PIN}"' \
--header "Idempotency-Key: public-api-approval-unpin-20260906060644" \
--data '{"pin":null}' \
"$BASE_URL/api/v1/approvals/{APPROVAL_ID}/pin"Both operations require the current ETag and approvals:pin:write. Do not send pin in a regular PATCH.
Approvals - execute an Approved or Rejected decision
Decisions use a dedicated endpoint:
/api/v1/approvals/{APPROVAL_ID}/decisionA positive decision sets status=Approved, dateApproved and dateEnd:
curl --fail-with-body --silent --show-error \
--request POST \
--header "Content-Type: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header 'If-Match: "{CURRENT_ETAG}"' \
--header "Idempotency-Key: public-api-approval-decision-approved-20260906060644" \
--data '{"approved":true,"remark":"Approved through the Public API integration."}' \
"$BASE_URL/api/v1/approvals/{APPROVED_APPROVAL_ID}/decision"A negative decision sets status=Rejected, dateRejected and dateEnd:
curl --fail-with-body --silent --show-error \
--request POST \
--header "Content-Type: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header 'If-Match: "{CURRENT_ETAG}"' \
--header "Idempotency-Key: public-api-approval-decision-rejected-20260906060644" \
--data '{"approved":false,"remark":"Rejected through the Public API integration."}' \
"$BASE_URL/api/v1/approvals/{REJECTED_APPROVAL_ID}/decision"Do not change the status through PATCH to bypass this procedure. A decision requires approvals:decision:write, the appropriate business permission (Approval_Accept or Approval_Reject), a current ETag and a call made by exactly the user assigned as approver.
Approvals - requester and approver
Read user relationships through a separate endpoint:
curl --fail-with-body --silent --show-error \
--request GET \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/approvals/{APPROVAL_ID}/user-relationships?page=1&pageSize=20"The response contains a requester relationship and, when an approver was provided, an approver relationship:
{
"data": {
"items": [
{
"userId": "{REQUESTER_USER_ID}",
"relationshipType": "requester"
},
{
"userId": "{APPROVER_USER_ID}",
"relationshipType": "approver"
}
]
}
}The requester is assigned automatically to the user who creates the Approval. Both relationships are read-only. Do not try to change the approver through POST on user-relationships; approver assignment belongs to the controlled creation or system process.
Approvals - permitted object relationships
Approval has a deliberately limited relationship target catalog:
This catalog does not include relationships with assets, clients, vendors, documents, confirmations or the Approval itself.
{
"targetId": "{TARGET_ID}",
"targetDataSet": "notes",
"targetItemType": "note"
}First select a target record from the list of the relevant collection. Do not assume that every database contains a record in all seven collections.
Approvals - the relationship format has one important difference
Object relationships for Approvals do not store relationshipType. The body contains only the target identifier, collection name and technical object type:
{
"targetId": "7bdda87a-6c37-49ae-9e40-272a7a9b8616",
"targetDataSet": "notes",
"targetItemType": "note"
}Do not send this field:
{
"targetId": "{TARGET_ID}",
"targetDataSet": "notes",
"targetItemType": "note",
"relationshipType": "related"
}relationshipType is used by other relationship models and by files, but it is rejected for Approval relationships with process objects.
curl --fail-with-body --silent --show-error \
--request GET \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/notes?page=1&pageSize=10"Choosing a target requires the collection's read scope, for example notes:read.
Approvals - add, read and remove a relationship
Direct relationship creation requires the current Approval ETag and approvals:relationships:write:
curl --fail-with-body --silent --show-error \
--request POST \
--header "Content-Type: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header 'If-Match: "{CURRENT_ETAG}"' \
--header "Idempotency-Key: public-api-approval-relation-add-0001" \
--data '{"targetId":"{NOTE_ID}","targetDataSet":"notes","targetItemType":"note"}' \
"$BASE_URL/api/v1/approvals/{APPROVAL_ID}/relationships"Read relationships after adding one:
curl --fail-with-body --silent --show-error \
--request GET \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/approvals/{APPROVAL_ID}/relationships?targetDataSet=notes&page=1&pageSize=100"Remove one relationship using the new ETag returned after the add:
curl --fail-with-body --silent --show-error \
--request DELETE \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header 'If-Match: "{ETAG_AFTER_RELATION_ADD}"' \
--header "Idempotency-Key: public-api-approval-relation-delete-0001" \
"$BASE_URL/api/v1/approvals/{APPROVAL_ID}/relationships/notes/{NOTE_ID}"After every write, retrieve the relationship collection again and confirm that the target was added or removed.
Approvals - relationship batches and relationships in PATCH
Change several relationships with one request:
{
"add": [
{
"targetId": "{NOTE_ID}",
"targetDataSet": "notes",
"targetItemType": "note"
}
],
"remove": []
}curl --fail-with-body --silent --show-error \
--request POST \
--header "Content-Type: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header 'If-Match: "{CURRENT_ETAG}"' \
--header "Idempotency-Key: public-api-approval-relationship-batch-0001" \
--data-binary @relationship-batch.json \
"$BASE_URL/api/v1/approvals/{APPROVAL_ID}/relationships:batch"The response contains added, removed and skipped counters. A PATCH can also contain relationshipsToAdd and relationshipsToRemove:
{
"attributes": {
"info": "Updated together with a relationship."
},
"relationshipsToAdd": [
{
"targetId": "{TICKET_ID}",
"targetDataSet": "tickets",
"targetItemType": "ticket"
}
],
"relationshipsToRemove": []
}The current ETag, idempotency and relationship scope are required in both variants.
Approvals - file list and upload
Files are separate resources linked to an Approval. First read the current list:
curl --fail-with-body --silent --show-error \
--request GET \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/approvals/{APPROVAL_ID}/files?page=1&pageSize=100"Upload a file as multipart/form-data. The file role is passed in the query string:
curl --fail-with-body --silent --show-error \
--request POST \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header 'If-Match: "{CURRENT_ETAG}"' \
--header "Idempotency-Key: public-api-approval-file-upload-0001" \
--form "[email protected];type=application/pdf" \
"$BASE_URL/api/v1/approvals/{APPROVAL_ID}/files?relationshipType=decision-form"The file name should be a single name without a path. Check the size before sending it and set the MIME type deliberately. Upload requires approvals:files:write.
{
"data": {
"id": "{FILE_ID}",
"fileName": "approval-decision-form.pdf",
"contentType": "application/pdf",
"size": 48231,
"relationshipType": "decision-form",
"isMain": false,
"downloadUrl": "/api/v1/approvals/{APPROVAL_ID}/files/{FILE_ID}/content"
}
}The Approval API does not provide an operation for setting a main file. Returned files have isMain=false. Do not build an integration that expects a /main endpoint for this object.
Approvals - download, attach and remove files
Download file content through the content endpoint and save it as binary:
curl --fail-with-body --silent --show-error \
--output downloaded-approval-form.pdf \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/approvals/{APPROVAL_ID}/files/{FILE_ID}/content"If a file already exists in the system and you have its File ID, attach it to an Approval without uploading another copy:
curl --fail-with-body --silent --show-error \
--request POST \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header 'If-Match: "{TARGET_APPROVAL_ETAG}"' \
--header "Idempotency-Key: public-api-approval-file-attach-0001" \
"$BASE_URL/api/v1/approvals/{TARGET_APPROVAL_ID}/files/{FILE_ID}?relationshipType=reference"Remove a file from an Approval:
curl --fail-with-body --silent --show-error \
--request DELETE \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header 'If-Match: "{CURRENT_ETAG}"' \
--header "Idempotency-Key: public-api-approval-file-delete-0001" \
"$BASE_URL/api/v1/approvals/{APPROVAL_ID}/files/{FILE_ID}"Attach creates a link to an existing file and does not upload a new copy. Upload, attach and delete change the Approval ETag. Download is read-only.
Approvals - batch operations
The /api/v1/approvals:batch endpoint creates, edits and deletes multiple records. It does not replace decision, pin, relationship or file operations:
{
"items": [
{
"operation": "create",
"create": {
"itemType": "approval",
"approverId": "{APPROVER_USER_ID}",
"attributes": {
"customId": "PUBLIC-API-APPROVAL-BATCH-A",
"location": "Warsaw",
"department": "IT",
"level": "Supervisor",
"category": "Procurement",
"description": "Batch-created Approval A"
}
}
},
{
"operation": "create",
"create": {
"itemType": "approval",
"approverId": "{APPROVER_USER_ID}",
"attributes": {
"customId": "PUBLIC-API-APPROVAL-BATCH-B",
"location": "Warsaw",
"department": "IT",
"level": "Manager",
"category": "Procurement",
"description": "Batch-created Approval B"
}
}
}
]
}curl --fail-with-body --silent --show-error \
--request POST \
--header "Content-Type: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header "Idempotency-Key: public-api-approval-batch-create-0001" \
--data-binary @approvals-batch-create.json \
"$BASE_URL/api/v1/approvals:batch"One batch may mix create, update and delete. Each update and delete must have its own id and current ifMatch.
{
"data": {
"items": [
{
"index": 0,
"operation": "create",
"status": 201,
"id": "{BATCH_ID_A}",
"data": {
"id": "{BATCH_ID_A}",
"meta": { "etag": "{ETAG_A}" }
}
}
],
"succeeded": 2,
"failed": 0
}
}Batch is not an all-or-nothing transaction. A partial result may return 207 Multi-Status. Analyze each response item and do not repeat operations that already succeeded.
Approvals - delete a record
Before deletion, read the record again, verify the UUID and current ETag, and confirm that the business process allows deletion:
curl --fail-with-body --silent --show-error \
--request DELETE \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header 'If-Match: "{CURRENT_ETAG}"' \
--header "Idempotency-Key: public-api-approval-delete-0001" \
"$BASE_URL/api/v1/approvals/{APPROVAL_ID}"A successful response returns 200 OK and data=true. After deletion, reading the same UUID should return 404 Not Found with approval_not_found. You can also verify the result through a list filtered by customId and expect totalItems=0.
Do not delete an Approval without checking its ETag. This protects an integration from deleting a newer version while working with an old copy.
Approvals - errors, limits and a safe sequence
Errors use the Problem Details format. Log status, code and requestId, but never log the Client Secret or complete headers:
authentication_failedapproval_approver_requiredapproval_not_foundapproval_unique_constraint or approval_concurrency_conflictif_match_failed, if_match_requiredvalidation_failed, approval_decision_rejected, approval_pin_rejectedrate_limit_exceededRetry-After.Read X-RateLimit-Limit and X-RateLimit-Remaining. Cache the schema and values, limit concurrency and use backoff after a 429.
A safe sequence is: context, schema, choose an approver, list or read a record, create with Idempotency-Key, store its UUID and ETag, add relationships or files, edit with If-Match, make the decision through /decision, verify by reading again and only then delete when required. The same sequence can be used in n8n by passing UUIDs, ETags and idempotency keys between steps.
