Skip to content

API Reference

To gain access to the API, you will need to generate an access key. To do this you must be logged into the platform securely, that is using 2FA. There will be an option from the Main Menu to generate an API key. You will need to enter your current password and a valid 2FA code. You will then be provided with an API key - please keep this securely as it can be used to access the billing platform under your identity. The key will only be shown once and cannot be recovered if you lose it - you may simply regenerate an API key at any time. Please note you may only have one API key at any time, so creating a new one will automatically invalidate any existing key, so you will need to update any systems using it.

If you wish to integrate third-party systems to the platform, we recommend setting up a dedicated user for each system, to assist with access control and auditing. To do so, you can create an API-only user - do not give the user login access, then you will be able to generate an API key from the Expert Mode menu when viewing the account. Please note that only users with Primary data protection access have the ability to reset other users’ API keys.

When setting up an API-only user, make sure the user has at least read access to the API in their permissions. You should also grant appropriate customer access (All Customers, Account Manager, or Commission Holder) depending on which customers the integration needs to work with.

Most records belong to a customer, so a key restricted to particular customers sees only those records. Notes, TODOs and tickets are the exception: one can exist with no customer at all, allocated to a person or a department instead. Those follow that allocation, the same way they do on the platform’s own screens:

RecordReachable by
Noteits owner, or a member of its departments
Ticketits owner, an allocated user, or a member of its departments
TODOan allocated or review user, or a member of the matching departments

A key whose holder has access to all customers is unaffected and sees every unlinked record. A key restricted to its own accounts reaches only the unlinked records it is allocated. Records that carry a customer are scoped by customer as they always are.

The same rule applies to the KPI endpoint and to AI assistants, which read through the same access checks.

Your platform can cap every API key to the permissions held by the platform’s own API user account, in the way an AI assistant connection is capped by the MCP settings user. Where this is switched on, a key reaches only what both its holder and the API account hold, so the API account acts as a ceiling over every integration at once.

The ceiling covers object permissions only. Customer access - All Customers, Account Manager or Commission Holder - stays with the key holder’s own account, since which customers an integration should see belongs to that integration rather than to the platform-wide ceiling.

A request the ceiling blocks is refused as an ordinary permission denial, without naming the ceiling as the reason. If an integration is denied something its own user clearly has permission for, ask your platform administrator whether the ceiling is set and what the API account holds.

Both this and the row maximum above are configured under Access Settings, as Cap API access by the API user and Maximum rows per list request.

The API may be accessed from within your platform using the base URL of /backend/api/v1 using the same domain as you normally use (companyname.callstats.net) and must be accessed over HTTPS.

The same API is also served at /crm/api/v1, and the KPI endpoint at /crm/kpi/, so a CRM-only user has an obvious address to point an integration at. They return exactly the same data, governed by exactly the same permissions; only the address differs. See Machine access.

You must present your API access key with every access of the API. This should be done using the bearer authentication scheme using the Authorization header, for example Authorization: Bearer MY_API_KEY . All requests will be logged, and your activity will be visible to other users of the platform, depending on their access level.

All requests should be JSON encoded using UTF-8 encoding, as will all responses. You should add suitable Accept headers to all requests and Content-Type and Content-Length headers where data are being sent to the platform.

You should use the appropriate HTTP method for your action: requests for data should use the GET method, updates to existing data should use PATCH, adding new data should use a POST as should performing an action on existing data.

As well as setting the required headers details above, we also recommend enabling compression by setting the Accept-Encoding header, for example Accept-Encoding: gzip . This can significantly reduce the size of the response.

The API may return the following headers in responses:

X-API-No-Changes When you send an update request (PATCH) where all submitted field values match the current values, the API returns this header set to 1. This indicates that no changes were made to the resource.

This is useful for:

  • Detecting when an update was unnecessary
  • Avoiding redundant processing in your application
  • Confirming data synchronisation status

Example Response Headers

HTTP/1.1 200 OK
Content-Type: application/json
X-API-No-Changes: 1

Note: The response body will still contain the full resource as normal.

X-API-Partial-Completion Where your platform sets a maximum number of rows per list request, a list response that reached that maximum carries this header, for example list capped at 500 rows - page with offset or idAfter.

The header means there is genuinely more to fetch. A result that happens to be exactly the maximum does not carry it, so a paging loop that continues only while the header is present never makes a wasted request for an empty page.

The records themselves are returned as normal; only how many you get is affected.

Within the base URL you may access a specific endpoint (such as /customers) corresponding to resource you would like to access.

You may request the details of a specific resource by using its ID (for example /customers/1234). All IDs are integers.

For each endpoint you may issue a request to the bare endpoint (such as /customers) to list all resources of that type, or resources which depend on a different resource, for example numbers belonging to a specific customer (using /customers/1234/numbers). In either case, you may optionally pass parameters to limit the results.

There are some standard parameters which apply to the majority of resources. To obtain recently added or updated resources you may use the idAfter or updatedSince parameters - if you maintain a record of the highest seen ID you may pass this to retreive newer resources (with a higher ID). Similarly, if you maintain a record of when you last requested updates, you may use the updatedSince parameter to retrieve resources modified since then. The updatedSince parameter should be a time passed in ISO 8601 format. Please note that not all automated updates currently update the last-changed date for an resource, so this should not be relied upon during alpha testing.

Resources are returned in ID order unless you request a different order with orderBy (see Sorting below).

You may also paginate the results by using the limit and offset parameters.

Your platform may set a maximum number of records a single list request may return. Where it does, asking for more than the maximum - or not naming a limit at all - returns at most that many records along with the X-API-Partial-Completion header, rather than failing. Page on with offset or idAfter to collect the rest.

A limit that is not a positive integer is refused with a 400 in the usual way; the maximum does not rescue an unusable value. limit=0 is left as you sent it, and expanded child lists are not capped.

Beyond each endpoint’s named filters, every list endpoint accepts generic field filters using the f parameter family:

GET /invoices/?f[invoiceDate][min]=2026-01-01&f[invoiceAmount][min]=100
  • Equality: f[field]=value matches records where the field equals the value.
  • Operators: f[field][operator]=value applies a comparison. The available operators depend on the field type:
Field typeOperators
Date and timestampeq, min, max, null
Numeric and monetaryeq, min, max, in, null
Lookup (status, type, and similar)eq, in, null
Booleaneq, null
Texteq, contains, null

Operator behaviour:

  • min and max are inclusive (on or after, on or before). Dates use ISO format (2026-01-31).
  • in takes a comma-separated list of values: f[statusID][in]=1,2,3.
  • null matches records where the field is empty; pass 0 to match records where it is not empty (f[dateDrop][null]=0).
  • contains performs a case-insensitive substring match and needs at least 3 characters.

Filtering a lookup field by name: on lookup fields such as statusID or numberTypeID, you can pass the displayed name instead of the numeric ID, matched without regard to letter case, so f[numberTypeID]=voip finds the VoIP type. Names work in an in list alongside IDs, but a name containing a comma cannot be used there. An ambiguous name is rejected with a suggestion to use the numeric ID, and an unrecognised one is rejected with a pointer to the available values.

Date values must be usable dates. A value that cannot be read as a date is rejected rather than being treated as a wildcard, so a mistyped range is reported instead of quietly returning a different set of records. Where a request expands a child list, an unusable filter on that list is reported before any of the response is returned.

Filtering by the parent customer: on customer-linked resources you can filter by properties of the owning customer, one level deep. Use a named customer filter or a nested field filter:

# Invoices for customers added in the last 12 months
GET /invoices/?f[customer][createdSince]=2025-07-12
# Numbers belonging to customers in a particular postcode area
GET /numbers/?f[customer][f][postcode][contains]=LS1

Only plain, visible fields can be filtered; derived or restricted fields are rejected. Invalid filters return HTTP 400 with one of these error codes:

Error codeMeaning
400021The customer filter needs a nested filter (f[customer][<filter>]=value)
400022The field is not filterable on this endpoint
400023The operator is not available for that field’s type
400024Invalid value: bad date or number, contains too short, bad in list, or an unknown or ambiguous lookup name

Use the orderBy parameter to sort list results. Pass one or more field names, comma-separated, each optionally suffixed with .desc (or .asc, the default):

# Largest invoices first
GET /invoices/?orderBy=invoiceAmount.desc&limit=10
# By customer, newest first within each customer
GET /invoices/?orderBy=customerID,invoiceDate.desc

The same fields that accept generic filters can be sorted on. The record ID is always appended as a final tiebreaker, so ordering is stable across pages. A non-sortable field returns HTTP 400 with error code 400025.

The API supports expanding related resources using the expand parameter family. This allows you to fetch related data in a single request instead of making multiple API calls.

Usage
Add the relevant expand parameter to include associated resources in the response:

GET /customers/?expandNumbers

Example Response

[
{
"id": "123",
"name": "Acme Corp",
"numbers": [
{
"id": "456",
"number": "+1234567890",
"status": "active"
},
{
"id": "789",
"number": "+1234567891",
"status": "pending"
}
]
}
]

Notes

  • Multiple expand parameters can be combined
  • Expansion may increase response time
  • By default, all expand parameters are false

The standard active parameter filters the main resource but doesn’t affect child objects when expanding. To filter expanded data, use the following parameters:

ParameterTypeDescription
numberActiveflagWhen expanding numbers, only include active numbers
serviceActiveflagWhen expanding services, only include active services
featureActiveflagWhen expanding features, only include active features

These parameters only take effect when combined with the relevant expand parameter. For example, featureActive=true has no effect unless you also include expandFeatures=true.

Example

GET /customers/123?expandNumbers=true&numberActive=true

This returns the customer with only their active numbers included in the response. Dropped numbers are excluded from the expanded data.

Combined Example

GET /customers/?expandNumbers=true&numberActive=true&expandFeatures=true&featureActive=true

This returns all customers with their active numbers and active features only.

You may update an existing resource by issuing a PATCH request with the changes to the endpoint of the specific resource (for example /customers/1234). The current state of the resource will then be returned. Currently, a PUT request will be treated as a PATCH request, but this is subject to change.

You may add a new resource by issuing a POST request to the endpoint of the resource (for example customers ). The current state of the resource will then be returned - this may differ from what was actually submitted as some processing may take place, for example adding default values. To add an resource linked to an existing resource (for example a number to a customer) POST it to the endpoint belonging to that resource (for example /customers/1234/numbers).

Where a field takes a value from a list, such as a number type or a status, you may supply the displayed name instead of its numeric ID. Names are matched without regard to letter case, so VOIP selects an existing VoIP number type. A name that matches more than one value fails the request for correction rather than picking one.

Beyond standard CRUD operations (Create, Read, Update, Delete), many resources support actions - specific operations that perform business logic on existing resources. Actions allow you to trigger complex workflows, state changes, or calculations through simple API calls.

Actions are invoked by making a POST request to a specific resource with an action parameter in the URL. Any parameters required by the action are passed in the JSON request body.

General Format:

POST /backend/api/v1/{resource}/{id}?action={action_name}

The action will be performed on the specified resource, and the updated resource will be returned in the response.

Example without parameters:

POST /backend/api/v1/customers/1234?action=generatePIN
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{}

Example with parameters:

POST /backend/api/v1/customers/1234?action=drop
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"status": "Ex-Customer",
"dateDrop": "2025-01-24",
"statusReason": "Customer requested cancellation"
}

While actions are resource-specific, here are some common types you’ll encounter:

State Changes

  • drop - Deactivate or cancel a resource
  • reinstate - Reactivate a previously dropped resource
  • approve / unapprove - Change approval status
  • cancel - Cancel a pending operation

Unless otherwise stated, actions return the complete updated resource in JSON format, exactly as a GET request would:

{
"id": 1234,
"customerName": "Example Corp",
"statusID": 2,
"updatedDate": "2025-01-24",
"statusReason": "Customer requested cancellation",
// ... other fields
}

This allows you to immediately see the effect of the action without making a separate GET request.

If an action fails, you’ll receive an appropriate HTTP status code with error details:

{
"error": "Invalid Action",
"error_code": 400501,
"hint": "The action 'invalidAction' is not recognised for this resource"
}

Common error scenarios:

  • 400501 - Action not recognised for this resource type
  • 400502 - Action failed to execute (check the hint for details)
  • 400503 - Missing required parameters for the action
  • 403xxx - Permission denied (user lacks necessary access rights)
  • 409xxx - Conflict (resource is in wrong state for this action)
  1. Check Resource State - Some actions are only valid in certain states (e.g., can’t reinstate an active customer)

  2. Verify Permissions - Actions often require specific permissions beyond basic read/write access

  3. Handle Responses - Always check the returned resource to confirm the action succeeded as expected

  4. Use Idempotent Actions - Where possible, actions should be safe to retry (e.g., generating a PIN replaces the existing one)

  5. Validate Parameters - Ensure all required parameters are provided with valid values before making the request

  6. Monitor for Side Effects - Some actions may trigger additional processes (emails, webhooks, etc.)

To find what actions are available for a specific resource:

  1. Check the resource-specific documentation in the Endpoints section
  2. Look for an “Actions” subsection listing all available actions
  3. Each action will document its parameters, permissions, and effects

Note that not all resources support actions - they’re typically available for core business objects like customers, invoices, and payments.

Some resources support file attachments. Files can be uploaded using two different methods depending on your requirements:

  1. JSON with Base64 encoding - Consistent with the standard API format
  2. Multipart form data - More efficient for larger files

Both methods support optional file verification and compression to ensure data integrity and reduce bandwidth usage.

For consistency with the rest of the API, you can upload files by encoding them as Base64 and including them in a standard JSON request. This method supports both creating new attachments (POST) and updating existing ones (PATCH).

Example Request (Create)

POST /backend/api/v1/notes/1234/noteAttachments
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"fileName": "order-details.pdf",
"_fileContent": "JVBERi0xLjQNCiXi48/TDQo...",
"description": "Customer order specifications",
"_compression": "zstd",
"_fileSize": 1048576,
"_fileHash": "da39a3ee5e6b4b0d3255bf...",
"_mimeType": "application/pdf"
}

Example Request (Update)

PATCH /backend/api/v1/noteAttachments/5678
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"fileName": "revised-order-details.pdf",
"_fileContent": "JVBERi0xLjQNCiXi48/TDQo...",
"description": "Updated customer order specifications",
"_fileSize": 1253872,
"_fileHash": "f3b8c2a4d5e1a9b8c7d6e5f4..."
}

For larger files or when bandwidth efficiency is important, you can use multipart form data. The JSON data is sent in a data field, with the file sent as binary data. Note that multipart uploads are only supported for creating new resources (POST).

Example Request

POST /backend/api/v1/notes/1234/noteAttachments
Authorization: Bearer YOUR_API_KEY
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="data"
Content-Type: application/json
{
"fileName": "order-details.pdf",
"description": "Customer order specifications",
"_fileSize": 1048576,
"_fileHash": "da39a3ee5e6b4b0d3255bf...",
"_mimeType": "application/pdf"
}
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="order-details.pdf"
Content-Type: application/pdf
[Binary file data]
------WebKitFormBoundary7MA4YWxkTrZu0gW--

The following optional parameters can be included to verify file integrity:

_fileSize (integer)
Expected file size in bytes of the original file (before compression). If provided, the uploaded file size will be verified against this value and the request will be rejected if they don’t match.

_fileHash (string)
Expected SHA1 hash of the original file content (before compression). If provided, the SHA1 hash of the uploaded file will be calculated and compared. The request will be rejected if the hashes don’t match.

_mimeType (string)
Override the automatically detected MIME type. If provided, this value will be used instead of auto-detection. No verification is performed - the API trusts that you know the correct MIME type for your file.

For the JSON method, you can compress file content before Base64 encoding using the _compression parameter:

Supported compression schemes:

  • gzip - Good compression with wide compatibility
  • zlib - Common in PHP applications
  • bzip2 - Higher compression ratio
  • zstd - Excellent compression and speed balance (recommended)

Example with compression

{
"fileName": "large-order-summary.xlsx",
"_fileContent": "eJzT0yMa...",
"_compression": "zstd",
"description": "Compressed quarterly order summary"
}

Both upload methods return the same response format:

{
"id": 5678,
"noteID": 1234,
"storedFileID":9876,
"description": "Customer order summary"
}

When accessing an existing attachment (e.g., GET /backend/api/v1/noteAttachments/5678), the response depends on the Accept header:

Getting attachment metadata (JSON)

GET /backend/api/v1/noteAttachments/5678
Authorization: Bearer YOUR_API_KEY
Accept: application/json
{
"id": 5678,
"noteID": 1234,
"storedFileID":9876,
"description": "Customer order summary"
}

Note: When listing resources with attached files, you can use ?expandStoredFiles to include file metadata (name, size, hash) in the response without needing separate requests.

Downloading the file content

GET /backend/api/v1/noteAttachments/5678
Authorization: Bearer YOUR_API_KEY
Accept: application/octet-stream

Returns the binary file content with appropriate headers for download.

Accept header behaviour:

  • Accept: application/json or no Accept header → Returns JSON metadata
  • Accept: application/octet-stream, Accept: */*, Accept: application/pdf, etc. → Returns file content

This allows API clients to easily choose between getting information about the attachment or downloading the actual file.

  • Method choice: Use JSON for consistency and smaller files. Use multipart for larger files or when bandwidth is a concern.
  • Creating vs. updating: Multipart uploads only support creating new attachments (POST). Use the JSON method for updating existing attachments (PATCH).
  • File verification: File size and hash verification are performed on the original file content, before any compression is applied.
  • File size limits: Check with your platform administrator for any file size restrictions.
  • Storage limitations: Uploaded files will count against your storage limits.
  • Compression: Only applies to the JSON method. Multipart uploads are already efficient for binary data.
  • Verification: File verification parameters are optional but recommended for critical uploads.
  • Security: All file uploads are logged and subject to the same authentication and authorization controls as other API operations.
  • Upload failures: An upload refused because of the request itself - no permission for the record, or a value the platform does not recognise - returns the status and error code naming that reason, so it can be acted on directly. A failure in the underlying file storage returns a 500 with a general message, and the detail is recorded on the platform rather than returned in the response.

Need a billing platform with a comprehensive REST API for custom integrations? discover SAFE Billing Platform's API capabilities for telecommunications billing automation