The Simplicate REST API

This is the documentation for the Simplicate v2 API. Read the contents of this page carefully. We make changes to the APIs from time to time. Endpoints are documented with the HTTP method for the request and a partial resource identifier.

Example:

GET /api/v2/crm/organization

Prepend your Simplicate URL to the resource identifier to get the full endpoint URL:

https://{yourdomain}.simplicate.nl/api/v2/crm/organization

The examples in the docs are cURL statements. You can run the statements on a command line to try out different API requests. To learn more, see Installing and using cURL. In Windows, you'll need to modify some of the examples in the docs to make them work. When documenting a resource, we use curly braces, {}, for identifiers.

Example: https://{yourdomain}.simplicate.nl/api/v2/crm/organization

Security and Authentication

This is an SSL-only API, regardless of how your account is configured.

You can authorize against the API using a tokenized approached based on API Key and Secret. You can create multiple API tokens for various purposes. API tokens are managed in the General settings under the API section in your Simplicate environment. The page lets you view, add, or delete tokens. More than one token can be active at the same time. Every API token inherits its rights from the user it was created for. Deleting a token deactivates it permanently.

Add the following authentication variables with every request header:

Authentication-Key: {API Key}
Authentication-Secret: {API Secret}

curl -H "Authentication-Key: {API Key}" -H "Authentication-Secret: {API secret}" https://{subdomain}.simplicate.nl/api/v2/crm/organization

Rate Limiting

This API is rate limited. We only allow a certain number of requests per minute. We reserve the right to adjust the rate limit for given endpoints to provide a high quality of service for all clients. As an API consumer, you should expect to be able to make at least 60 requests per minute. If the rate limit is exceeded, Simplicate responds with a HTTP 429 Too Many Requests

Request Format

The Simplicate API is a JSON-only API. You must supply a Content-Type: application/json header in all requests. You may get a text/plain response in case of an error like a bad request. You should treat this as an error you need to resolve.

Response Format

Simplicate responds to successful requests with HTTP status codes in the 200 range. When you create a resource, depending on the resource Simplicate renders the resulting created resource ID in the response body.

Simplicate responds to unsuccessful requests with HTTP status codes in the 400 range. The content type of the response may be text/plain for API-level error messages, such as when trying to call the API without SSL. The content type is application/json for business-level error messages because the response includes a JSON object with information about the error:
{
  "data": null,
  "errors": [
    "No country code given in the visiting_address property"
  ],
  "debug": null
}
 
If you experience responses with status codes in the 500 range, Simplicate may be experiencing internal issues or undergoing scheduled maintenance, during which we send a 503 Service Unavailable status code. When building an API client, we recommend treating any 500 status codes as a warning or temporary state. However, if the status persists and we don't have a publicly announced maintenance or service disruption, contact us at [email protected].

Filtering

Selecting specific fields to include in response only

Use the select parameter as a comma separated string to only include fields in the response you're interested in. If it's a nested property in the response, you may end the property name with a . (period) to include all its children. Selecting specific fields may be required in the future, and is regarded a good practice. Please note: some fields may be returned regardless of what you select.

Use case: Return only id, name, and all properties of relation_type:
GET /api/v2/crm/organization?select=id,relation_type.,name
This also works with nested properties in the response as well:

Use case: Return only id, name, and payment_term of its debtor property:
GET /api/v2/crm/organization?select=id,debtor.payment_term.,name

Filter the actual dataset returned

Filtering is allowed on almost all endpoints through the use of the q parameter, expressed as an array.
If this is a normal array, an OR-search will be done on the given fields.
Wildcards are allowed by means of * : q[name]=Don*.
NULL values may be searched with null : q[name]=null
Non NULL values may be searched with * : q[name]=*

Use case: Search for all entries in 'name' that contain 'Bob' somewhere in its value:
q[name]=*Bob*
Use case: Search for all entries in 'name' that have the value of 'Alice' OR 'Charlie'
q[name][]=Alice&q[name][]=Charlie
Use case: Search for all entries in 'name' that are of value 'Alice' AND values in 'email' that end with 'gmail.com'
q[name]=Alice&q[email]=*gmail.com
Use case: Search for all entries in 'name' that have 'Alice' OR values in 'email' that end with 'gmail.com'
q[name]=Alice&q[or][email]=*gmail.com&limit=10&sort=name

Filtering on Nested Values

Nested values, sometimes even several layers deep, can be filtered on as well. A simplified example response of /api/v2/projects/project is as follows:
{
  "data": [
    {
      "id": "project:abc",
      "name": "My First Project",
      "organization": {
        "id": "organization:xyz",
        "name": "Google"
      },
      "my_organization_profile": {
        "id": "myorganizationprofile:123",
        "organization": {
          "id": "organization:456",
          "name": "My Company"
        }
      }
    }
  ]
}
Use case: Find projects related to an organization by the name of 'Google':
/api/v2/projects/project?q[organization.name]=Google
Use case: Find projects related to your own organization by the name of 'My Company':
/api/v2/projects/project?q[my_organization_profile.organization.name]=My%20Company

Advanced Filtering

Advanced filtering is available with these options:

Additional Operators

There are additional operators to be used:
operator in URL Math operator Explanation
ge >= greater than or equals
gt > greater than
le <= less than or equals
lt < less than
in IN column with a value that matches one of the values in a comma separated list of values
nin NOT IN same as above, but negating
Use case: Search for all entries where they were last modified since April 5th, 2024, max 10 records, order by 'updated_at' date in descending order:
q[updated_at][ge]=2024-04-05 00:00:00&limit=10&sort=-updated_at
Use case: Search for all entries where they were created between March 29th, 2024, and April 5th, 2024, max 10 records, order by 'created_at' in descending order:
q[created_at][ge]=2024-03-29 00:00:00&q[created_at][le]=2024-04-05 23:59:59&limit=10&sort=-created_at
Please note: when using comparison operators (ge, gt, le, lt), fields like created_at and updated_at are date time fields (among others). When working with date times, please include the time to avoid any confusion. Upon omitting the time part, it'll default to 00:00:00.

Use case: Find all services for projects that have invoice method 'Hours' or 'FixedFee', order by budget descending:
/projects/services?q[invoice_method][in]=FixedFee,Subscription&sort=-budget

Custom Fields

Custom fields is a module independent entity within Simplicate. Please see this support article for more information (in Dutch).

There are 5 entities that support custom fields. All of them are available through the API, though not all of them can be filtered on yet.

Entity URL Filtering supported?
Projects /projects/project Yes
Organizations /crm/organization Yes
Person /crm/person Yes
Sales /sales/sale Yes
Employee /hrm/employee No

Each custom field has a property called 'name'. This name must be used to filter on. See the results of each respective listing call to find out what the custom field names are.

Use case: Find all projects that have a link to Google Docs in the custom field 'external_link':

This use case assumes there is a custom field named "External link" for "Projects"

q[custom_fields.external_link]=*docs.google.com*

 

The following 2 use cases assume there is a custom field named 'Part of country' for 'Projects', with 5 possible values: (North, East, South, West, Centre).

Use case: Find all projects that have no 'part_of_country' set yet

This use case combines custom field filtering with the advanced operators found here.

q[custom_fields.part_of_country][nin]=*
Use case: Find all projects that have no 'part_of_country' set yet, created after April 17th, 2024, show only the id, name, and project_manager fields, order by updated_at descending.
q[custom_fields.part_of_country][nin]=*&q[created_at][ge]=2024-04-17 00:00:00&select=id,name,project_manager.employee_id,project_manager.name&sort=-updated_at

Return Limit and Pagination

By default, most GET requests return a maximum of 100 records. You may decrease the number of records on a per-request basis by passing a limit parameter in the request URL parameters. Example: limit=50. You can't exceed 100 records per page on almost all endpoints. When the results exceed the limit, you can iterate through the records by incrementing the offset parameter.

Use case: Search for all entries in 'name' that have '%Bob%' from offset 11 with max 10 records, order by 'name' descending:
q[name]=*Bob*&offset=11&limit=10&sort=-name

Sorting Results

You can pass a sort parameter in the request URL. This parameter should contain a list of fields where to sort on. By default, the sort order is ascending. This can be reversed by prefixing the field with a minus (-).

Use case: Search for all entries in 'name' that have 'Alice' OR entries in 'email' that have 'gmail.com', max 10 records, order by name ascending:
q[name]=Alice&q[or][email]=*gmail.com&limit=10&sort=name
Use case: Search for all entries in 'name' that have 'Alice' OR entries in 'email' that have 'gmail.com', max 10 records, order by name *descending*:
q[name]=Alice&q[or][email]=*gmail.com&limit=10&sort=-name

Metadata

It is possible to receive metadata with your request. Below are the accepted values, separated by a comma.

Metadata Purpose
count Shows the count of the entire collection, ignoring the limit & offset parameters, but taking into account all query parameters. Useful for pagination
limit Returns the limit that's also set in the request parameters
offset Returns the offset that's also set in the request parameters
Use case: Find all entries with name like *my*, limit by 10, but show the count & limit in the metadata property
q[name]=*my*&limit=10&metadata=count,limit

Changelog

Below is a non-exhaustive list of changes (additions) made to the API. All changes made, bar necessary bug fixes, are to be backwards compatible. Some of these may contain references to API's that are not (yet) made public.

Year and month Change
2023-12
  • Added the following fields to the api/v2/projects/assignment endpoints:
    • created_at
    • updated_at
2023-11
  • Updated PUT api/v2/hrm/employee/{id} with these checks on if the employee status can be set to unemployed:
    • The employee user must have status blocked
    • All timetables of the employee must have an end date
2023-09
  • Updated PUT api/v2/crm/person/{id} making the field: family_name not mandatory
2023-08
  • Updated POST|PUT api/v2/crm/person making the field: family_name mandatory
2023-07
  • Updated POST|PUT api/v2/projects/service with support for 'Actual costs' (type = 'Hours')
2023-06
  • Updated GET api/v2/crm/organization with extra field
    • Added CustomerGroup value option
  • Updated GET api/v2/sales/salesreason
    • Added blocked property to response
  • Updated GET api/v2/sales/salesfilters with options that can be used to filter GET api/v2/sales/sales
    • Added opportunityprogress value options
    • Added administration value options
    • Added responsible_person value options
  • Updated PUT api/v2/sales/sales
    • It is now possible to change the relation of a Sales entity from an Organization to a Person or from a Person to an Organization with person_id or organization_id respectively
2023-04
  • Updated documentation 'added person_id' for PUT api/v2/hrm/employee
  • POST/PUT api/v2/hours/leave errors codes
  • POST/PUT api/v2/hours/absense errors codes
  • Added relation_number to various organization and person objects
  • Added mandatory boolean for customfields
  • Added duration_in_minutes int for hours approval
2022-08
  • Added API endpoints for assigning and removing project employees to projects:
    • POST /api/v2/projects/projectemployee
    • DELETE /api/v2/projects/projectemployee/{id}
2022-07
  • Added Known Limitations to the API docs
  • Added API endpoints for creating and updating employees, and creating, updating and deleting timetables:
    • POST /api/v2/hrm/employee
      • Updated the contents of PostEmployee model to match the implementation
    • PUT /api/v2/hrm/employee/{id}
    • DELETE /api/v2/hrm/employee/{id}
    • POST /api/v2/hrm/timetable
      • Updated the contents of PostTimetable model to match the implementation
    • PUT /api/v2/hrm/timetable/{id}
    • DELETE /api/v2/hrm/timetable/{id}
  • Added 'project_number' (string) property to GetTimesheetRowProject model (/api/v2/hours/timesheetrow)
2022-06
  • Adds email configuration properties to the Organization model (/api/v2/crm/organization)
    • 'send_invoice_email_to_contact' (bool)
    • 'send_invoice_email_to_project_contact' (bool)
    • 'send_invoice_email_to_fixed_email' (bool)
    • 'send_invoice_email_to_cc' (bool)
2022-03
  • Added 'is_active' (bool) to Person, Organization, and ContactPerson models (/api/v2/crm/person, /api/v2/crm/organization and /api/v2/crm/contactperson)
2022-02
  • Added GetAssignmentSimple model (/api/v2/timers/timer)
2022-01
  • Added 'use_custom_salutation' (bool) and 'custom_salutation' (string) properties to GetPerson (/api/v2/crm/person)
  • Added 'description' (string) property to Assignment model (/api/v2/projects/assignment)
2021-11
  • Added /projects/service/{id}/planningBudget (GET)
  • Renamed /projects/project/{id}/assignmentBudgetInfo (GET) to /projects/project/{id}/planningBudget
  • Reworked AssignmentBudgetInfo model
    • Added properties with almost all of them having an AssignmentBudgetInfoFuturePast model
  • Added QuotetemplateConfigurationMetaData model (/api/v2/sales/quotetemplate)
  • Added QuotetemplateMetaData model (/api/v2/sales/quotetemplate)
  • Added 'is_editable' and 'is_deletable' properties to GetHours (/api/v2/hours/hours)
    • Added IsSatisfied model
    • Added IsSatisfiedReason model
  • Added 'attachments' property to Expense model, array of ExpenseAttachment
    • Added ExpenseAttachment model (/api/v2/costs/expense)
  • Added 'is_attachment_allowed' property to GetPurchaseType, PostPurchaseType, PurchaseType (/api/v2/projects/purchasetype)
2021-08
  • Moved 'custom_fields' property of models Organization, Person, Sales, Project and Hours to their Get* models
    • Replaced 'CustomField' type with 'GetCustomField' type
  • Added PostCustomFieldValue type to custom_fields property of PostOrganization, PostPerson, PostSales, and PostProject models
  • Added 'work_function', 'work_email', 'work_mobile' properties to PostContactOrganizationFk and PostContactPersonFk models
  • Added 'vat_class' properties to GetSalesService model (/api/v2/sales/service)
  • Removed 'vat_code' and 'vat_description' properties from GetSalesService model (/api/v2/sales/service)
  • Added 'revenue_group_id' property to PostSalesService (/api/v2/sales/service)
2021-07
  • Added 'service_purchase_start_date' and 'service_purchase_end_date' to Expense model (/api/v2/costs/expense)
  • Added 'HoursCorrectionsTotal' and 'HoursCorrectionsUntil' to AssignmentBudgetInfo model (/api/v2/project/service/{id}/planningBudget)
2021-07
  • Added separate model for updating assignments (PutAssignment) (PUT /api/v2/projects/assignment)
    • PutAssignment no longer accepts 'start_date' and 'end_date' properties; you can no longer modify dates through the API (PUT /api/v2/projects/assignment)
  • Added 'start_date' and 'end_date' to PostAssignment specifically (POST /api/v2/projects/assignment)
2021-06
  • Added /projects/project/{id}/assignmentBudgetInfo (GET)
  • Added 'note' property (string) to Expense model (/api/v2/costs/expense)
  • Added 'is_productive' property (boolean) to GetHours model (/api/v2/hours/hours)
  • Added 'related_hours_id' property (string) to Mileage model (and its derived models: GetMileage, PostMileage, PutMileage) (/api/v2/hours/mileage)

Known Limitations

You are probably not the first running into limitations of our v2 API, we know it is far from perfect. A new API-first architecture is being designed starting 2022 which will take care of most known limitations, since they can't easily be changed in the current v2 API (how small they sometimes seem to be). Don’t expect any news before 2024 about a new API. Until then, we will only do minor modifications to the current v2 API, these can be found in our changelog. We'll try to provide you with workarounds when possible in the following list:

Types of Project Services (invoicing methods) (/api/v2/projects/service)

You're only able to create and modify project services where the invoice_method is of type FixedFee or Hours.

Installments

There's currently no support for creating or modifying installments or installment plans.

Webhooks

There are currently no webhooks. If you wish to see what's new since your last API call or in a certain period of time, see the use case Tracking Creates and Updates

Tracking Deletions

The only way to see what items have been deleted, is to do a full comparison with data you've stored yourself. When calling the API repeatedly, please keep the Rate Limiting into account, and please limit your selection of fields. Example: use only /api/v2/crm/organization?select=id,name,created_at to make the requests as lightweight as possible.

For very large datasets (like /api/v2/hours/hours), it's more performant to query the id's you've stored directly:

/api/v2/hours/hours
  ?limit=100
  &offset=0
  &select=id,updated_at
  &q[id][in]=hours:abc,hours:xyz
Invoice Services

There's currently no support for Services with regard to invoicing. Invoices created through the API are of the composition type 'lines', meaning you're only able to interact with the invoice lines directly. There's no link between the project services on a project, and the invoice lines. Invoices created through the API will not have a tab for 'Services'.

Questions and Support

If you encounter any issues when using our APIs, please contact us at [email protected].

Please note that we cannot help you develop software or scripts to connect with our API - support is limited to the actual API-calls themselves. We can direct you to the appropriate API call and explain how it works, but we do not provide substantive support in setting up the calls or developing integrations.

Use Cases

Obtaining the PDF of an invoice

After obtaining an invoice ID from /invoices/invoice, use /invoices/document to find the relevant document(s):

/invoices/document?q[linked_to.invoice_id]=invoice:abcdef

You'll find a list of documents pertaining to the invoice. Locate the document you wish to download. In the result set you'll find a download_url property. Please note that this URL requires a valid web session on the Simplicate application. To programmatically download the documents, please continue.

From the result of /invoices/document, use the id (as document:abcdef), to proceed to:

/documents/download/document:abcdef

This is the location of the actual document. If the document is a PDF, the API will respond with a Content-Type: application/pdf header. In all other cases, the API will respond with Content-Type: application/octet-stream header.

Please note: the /invoices/document and /documents/download API's are protected by all the relevant rights in the Simplicate application. When trying to download documents, please make sure you're using the API keys of a user who has access to these documents.


Uploading, Linking, and Using Files
Uploading files

When uploading documents to Simplicate, you'll follow this flow:

  • Let the API know your intentions; what's the file name, of what file size
  • Upload the file (repeatedly if need be), in chunks of max 500 kB.
  • The API will let you know you've uploaded all you needed, and that the file is complete
  • You'll be able to create a document, and link to this file, or use it as an attachment in timeline messages

Start by letting the API know you're about to submit a file. For this example, we'll upload this image: Logo of Simplicate. Send the following body to /upload/chunked.

POST /api/v2/upload/chunked

{
    "file_name": "simplicate-logo.png",
    "file_size": 858
}

Note that file size is in bytes. The result will be like:

200 OK

{
    "data": {
        "chunked_upload_id": "chunkedupload:a00a5662be4cf8b54da2058e5c846d06"
    }
}

We're going to use this ID to start uploading the actual contents of the file. Please note that the file contents must be base64 encoded, and each chunk must not exceed 500 kB (base64 encoded).

PUT /api/v2/upload/chunked/chunkedupload:a00a5662be4cf8b54da2058e5c846d06

{
    "chunk": "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAACxKAAAsSgF3enRNAAADDElEQVRIiY1VO2hUQRTd7LoRjYpg/EOipEgRxUJRDNgoVhZKUAu/BH8E0lgoNjbpFNGAhZ95hQTnqYU2In6QFBoEg2gjFiYiJiKopHCeEgWT9dyZM7uzEXb2weG+N2/u78y9d3K54EmTLKcTIzKvlZXL0sSchRwCJoAS8BN4g/+XIdeldp8RvQbRr/nQeIPGRsjDUPymnVGPXzA8jfVSquz3FHAO79aJ6NZ2oEyeG89oGMC3GBnB2inILcBaYCPQ7bIydGzuAo2iG3MgkXQFEQ8A81KX0QzYQPrECbM8XwdFWRM2jdL4nSqDyiyCsTVAi1CJYMTxI+79iLWOqAPHu1X4Dqxwhm20vaDsM//9wFo/5CAjH4PxNjlsoTiWwX3yeo0UCA4GlJlUmfDQx+C4HRDjBR07Ayh8peIxW7IKpafMMJ3eAJbivYd7xvF/JYPISwXVQ9EfKu/lQc6B9NRspVPZtxNyNeRy4AIw2wcUczBOCnpdA9noBl0G5jEoKGq/7uRr7frhnj0rFc3ApDZalaUuIutge8D5Q8s1uxffR+Foik13qR6KdrFLJ/He4SqIB63K3fsAe2aVx4oyJ4MANtR0wFExzM3PoVwMnBwIM8F6YzWNmTTcrVgGknonKPhLQ09tJysXMXCo7ERl1xmQlHc3K22ytgPwS0N7GBGGnVnINf4zJzgeZPCtohN0eDYtFNaRga1pcbYjdXUupdgPg/PZ2XPFODPZTJ0WwGYdy4CjAfw6xWYYe8/0pUw7gavOuJH7YTGzWu+pqyeDAp20wtiorkzLkh/PdpQn2Wntp6qtJNsrE5EMcJM5J0LNiHZKL4Dd+H4C+QHyFXBcVyatZPuWGVys6YBKbam9ZKzCEA6yKRgRS7CnmFbfCVdSP2UT0xpxIHdrto0VZB0A7SzFmZdNM+SA5x4B9NTRyeUrs8tXBfAbHN+EPCJDDtgnVQU6vwSN18fxEbmTwzJNsk3Ay8CIHxUlNxBldJhP+N4fUBjJoNL6Ba8EyGhOgGdYe0ent+2gS7IFzNgH9Z/NfzoINxdnkeUkAAAAAElFTkSuQmCC"
}

As this file is way smaller than 500 kB, the API will set the state to 'done', and no new chunks will be accepted:

200 OK

{
    "data": {
        "pointer": 1,
        "state": "done",
        "upload_queue_id": "uploadqueue:7de64ec93190dd457a9bfc4dc6429b75",
        "file_size_current": 858,
        "file_size_expected": 858,
        "download_url": "/download?name=simplicate-logo.png"
    },
    "errors": null,
    "debug": null
}

If you're uploading a larger file, the API will respond like below, and you'll need to send the remaining chunks, until the file is completely uploaded:

200 OK

{
    "data": {
        "pointer": 1,
        "state": "in_progress",
        "file_size_current": 384000,
        "file_size_expected": 3781286
    },
    "errors": null,
    "debug": null
}
Converting to document

With the upload_queue_id in hand, we can convert this file to a document, with a title, a document type id, and link it to an existing entity (Person, Organization, Sales, Project, Invoice)

POST /api/v2/documents/document

{
    "upload_queue_id": "uploadqueue:7de64ec93190dd457a9bfc4dc6429b75",
    "title": "simplicate-logo.png",
    "document_type_id": "documenttype:b423eae1f54f879041c55b3092b4a1f8",
    "linked_to": {
        "person_id": "person:b1c692b0993bb602eb694ed52cdf49ac"
    }
}

---

200 OK

{
    "data": {
        "id": "document:91b5a2aee0d60fae298fdc63dd8ada0b"
    }
}

This file can now be found in its linked entity, on the tab 'Documents'.

As a timeline message attachment

Additionally, if you wish to link this document to a timeline message, then please see POST /timeline/attachment, and send the following request:

POST /api/v2/timeline/attachment

{
    "message_id": "message:914edc4509a28e23ccd1f796716510b1",
    "document_id": "document:91b5a2aee0d60fae298fdc63dd8ada0b"
}

That's it for uploading files, and using them as documents!

PHP example for uploading chunks

A PHP example for uploading a file, in chunks:

function uploadFileToApi(string $fileName, string $filePath): string
{
    $uploadId = $this->callSimplicate('POST', '/upload/chunked', [
        'file_name' => $fileName,
        'file_size' => filesize($filePath),
    ]);
    $chunkSize = 500 * 1000; // 500 kB, times a thousand, for bytes, to err on the side of caution

    $base64String = base64_encode(file_get_contents($filePath));
    $offset = 0;

    while (true) {
        $chunk = substr($base64String, $offset, $chunkSize);
        $offset += $chunkSize;

        $apiResponse = $this->callSimplicate('PUT', '/upload/chunked/' . $uploadId, [
            'chunk' => $chunk,
        ]);

        if (!isset($apiResponse['data']) || !isset($apiResponse['data']['state'])) {
            throw new Exception('Expected a succesful API resonse, with data and data.state');
        }

        if ($apiResponse['data']['state'] === 'done') {
            return $apiResponse['data']['upload_queue_id'];
        }
    }
}

$uploadQueueId = uploadFileToApi('simplicate-logo.png', '/path/to/simplicate-logo-24x24px.png');


Tracking Creates and Updates

You can combine the filtering and sorting parameters to keep track of what was created or updated since the last API call:

/crm/organization?q[updated_at][ge]=2024-04-19+00:00:00&sort=-updated_at

Our Zapier integration uses this same method.

See also: Known Limitations > Tracking Deletions