Errors and Status Codes
When the Web API can't do what a request asks, it answers with an HTTP status code instead of a result, in most cases with a short message that names the cause. The status, the endpoint and that message tell your application whether a template is missing, a quota is used up, too many requests run at the same time or the Authorization header is missing. The causes below come from the endpoint reference, and the answers to requests without valid credentials, to wrong URLs, methods and headers and to too many requests at the same time were observed on the live service in October 2026.
Status Codes at a Glance
| Status | Meaning | Body |
|---|---|---|
200 OK | The request succeeded | The result, as the endpoint reference describes it |
201 Created | templates/upload stored the template, or PUT /v1/account/apikey created an API key | Empty after an upload, the new API key as a JSON string after PUT /v1/account/apikey |
204 No Content | templates/delete deleted the template | Empty |
400 Bad Request | The body has no data or no valid document, the named template is not in the template storage, or a query parameter such as returnFormat has a value the endpoint doesn't accept | Plain text, such as Template doesn't exist in storage; for a parameter given without a valid value, such as max of proofing/suggestions, JSON with a Message member |
403 Forbidden | The request is not authorized, a quota of the account is exceeded, the plan doesn't include a requested setting such as PDF/A, more than two merge requests of the account run at the same time, or the web server refuses too many requests from one IP address at the same time | Plain text, such as Invalid username/password combination or invalid APIKey; an HTML page when the web server refuses the request |
404 Not Found | A URL that matches no endpoint, or a request without a required query parameter, such as language of proofing/check or templateName of templates/pagecount | For a URL below /v1/, JSON: {"Message":"No HTTP resource was found that matches the request URI '…'."}; an HTML page outside /v1/ |
405 Method Not Allowed | A request with another HTTP method than the endpoint's, such as GET for processing/review/trackedchanges, which takes POST | JSON with a Message member, with an Allow header that names the endpoint's method |
411 Length Required | A POST or PUT request that has neither a body nor a Content-Length header | An HTML page |
415 Unsupported Media Type | A POST request with a body but no Content-Type header, or with a Content-Type such as text/plain that the endpoint doesn't read | JSON with a Message member |
500 Internal Server Error | A request without an Authorization header, with an empty one or with one that is not the authorization method, a space and the credentials. Some endpoints also answer 500 to a body they can't read, such as a document that is not valid Base64 | JSON with a Message member, such as {"Message":"An error has occurred."} for missing credentials |
Only an answer with a success status holds a result, so check the status before you decode the body.
Bad Request
The endpoint reference lists these causes of 400 Bad Request:
| Cause | Endpoints |
|---|---|
| The template is not in the template storage | templates/delete, templates/download, templates/info, templates/pagecount, templates/thumbnails, document/share |
| No data in the body, no template uploaded with the request, or the template is not in the template storage | document/merge, document/findandreplace |
A find and replace pair with only one string, an empty find string or a null replacement | document/findandreplace |
No data in the body, no documents, or a document that is missing or not valid Base64 | document/append |
| No data in the body, or a body that is not a JSON string | document/convert, processing/review/trackedchanges |
A returnFormat that is not one of the possible values | document/append, document/convert, document/findandreplace, document/merge |
| The document is not valid or not in the body | document/thumbnails |
| The extension of the template name is not .doc, .docx, .rtf or .tx, or the template has a wrong format, is not in the format of its extension or is corrupted | templates/upload |
An empty documents array is not an error for POST /v1/document/append: it answers 200 with an empty string instead of a document. The endpoints with more query parameters, the two thumbnails endpoints, GET /v1/proofing/check, GET /v1/proofing/suggestions and POST /v1/processing/review/removetrackedchange, also answer 400 for a parameter value they can't use, and their pages name the cases. For a name that is not stored, GET /v1/templates/exists doesn't answer 400; it returns false.
When you get a 400, compare the template name with the names that GET /v1/templates/list returns, and check that documents and templates travel as a Base64 string in a JSON string, as Managing Templates shows.
Forbidden
403 Forbidden has four kinds of causes: the credentials, the quotas of your plan, a setting your plan doesn't include and the number of requests that run at the same time.
Not Authorized
Every endpoint answers 403 when the request is not authorized, for example because the API key was deleted or the user name or password of a Basic header is wrong. The body of that answer is plain text (text/plain), not JSON. A request with an unknown API key gets this answer:
HTTP/2 403
content-type: text/plain; charset=utf-8
Invalid username/password combination or invalid APIKey
Authentication shows the two forms of the Authorization header.
A Quota Is Exceeded
Three quotas limit what an account does per month or keeps in its storage. When one is used up, the endpoints that count against it answer 403:
| Quota | Endpoints |
|---|---|
| Document quota | document/append, document/convert, document/findandreplace, document/merge, document/thumbnails, templates/thumbnails, processing/review/removetrackedchange |
| Template storage quota | templates/upload |
| Spell checking quota | proofing/check, proofing/suggestions |
What Counts Against Which Quota explains how each request counts.
For a template name that has more than 207 characters or contains # or /, POST /v1/templates/upload answers 403 with the text Template quota exceeded as well, even when the quota isn't used up.
The size of each quota depends on your plan, as Plans and Licensing lists. To see how much is left, call GET /v1/account/settings: it returns createdDocuments with maxDocuments, uploadedTemplates with maxTemplates and proofingTransactions with maxProofingTransactions. The portal shows the same numbers, as Transactions and Statistics describes.
A Setting Your Plan Doesn't Include
A request for returnFormat=PDFA to POST /v1/document/merge, POST /v1/document/append, POST /v1/document/convert or POST /v1/document/findandreplace gets 403 with the plain text Setting is not supported in this license. An Extreme license is required when the account has no Extreme license, as on a trial account. Plans and Licensing shows which plan includes PDF/A.
Too Many Concurrent Requests
POST /v1/document/merge answers 403 when more than two requests of the same account arrive at the same time. Send merge requests one after another or at most two at a time, for example from a queue, and send a refused request again once one of the running requests has finished. Merge Requests Return 403 While Quota Is Left shows a .NET application that never runs more than two.
Apart from that, the web server limits, on every endpoint, the number of requests from one IP address that run at the same time. A request over the limit gets 403 with an HTML page (text/html) of the web server instead of the plain-text message, such as the page HTTP Error 403.501 - Forbidden or the shorter 403 - Forbidden: Access is denied., and the SDKs pass that page on as the message of their exception. The same request, sent again a few seconds later, succeeds. When a 403 comes with an HTML body, wait a few seconds before you send the request again, and send fewer requests at the same time from that address.
Wrong Address, Method, Length or Content Type
The service checks the URL, the HTTP method and the Content-Length and Content-Type headers before the credentials, so these answers come even for a request without valid credentials:
404 Not Foundfor a URL that matches no endpoint or leaves out a required query parameter.405 Method Not Allowedfor another HTTP method than the endpoint's,HEADandOPTIONSincluded. TheAllowheader of the answer names the endpoint's method, and the JSON body reads, for aGET,{"Message":"The requested resource does not support http method 'GET'."}.411 Length Requiredfor aPOSTorPUTthat has neither a body nor aContent-Lengthheader. The body is an HTML page that readsHTTP Error 411. The request must be chunked or have a content length.SendContent-Length: 0with a request that has no body, such asPUT /v1/account/apikey.415 Unsupported Media Typefor a body without aContent-Typeheader, or with aContent-Typesuch astext/plainthat the endpoint doesn't read. SendContent-Type: application/jsonwith every body.
Requests Without Credentials
The reference documents 403 for a request that is not authorized. A request that has no Authorization header at all gets another answer, for example for GET /v1/fonts/list:
HTTP/2 500
content-type: application/json; charset=utf-8
{"Message":"An error has occurred."}
The same answer comes for an empty Authorization header and for one that is not the authorization method, a space and the credentials, such as ReportingCloud-APIKey: followed by the key. A 500 doesn't always mean missing credentials, though: some endpoints answer it as well to a body they can't read, such as a document that is not valid Base64.
When you get this answer, check first that your code sends the Authorization header with every request, written as the authorization method, a space and the credentials, and then that the body is what the page of the endpoint describes. If both are right, look at the status page, which shows whether the Web API, the portal and the backend are available and lists reported incidents, as Check the Service Status describes.
Processing Time Limit
Six endpoints limit the pure processing time of a request to 60 seconds and cancel the task after that: POST /v1/document/append, POST /v1/document/convert, POST /v1/document/findandreplace, POST /v1/document/merge, POST /v1/document/thumbnails and GET /v1/templates/thumbnails. A typical document takes less than 5 seconds. When a request takes longer, make it smaller: fewer data rows and a smaller template for a merge, fewer or smaller documents for an append, a smaller document or template for a conversion or a find and replace, and fewer pages for thumbnails. The reference doesn't name the status code of a canceled request.
The PHP SDK waits up to 120 seconds for an answer by default, longer than the limit. Its timeout option changes the value.
Test Mode
While you develop, the optional query parameter test=true of POST /v1/document/merge, POST /v1/document/convert, POST /v1/document/findandreplace and POST /v1/document/append creates documents with a "TEST MODE" watermark that don't count against the document quota. On a trial account, this holds for document/convert and document/append only: a test run of document/merge or document/findandreplace counts against the document quota like any other request. The following command merges the demo template with the merge data in merge.json, as the Overview of the Mail Merge section builds it:
curl POST "https://api.reporting.cloud/v1/document/merge?templateName=sample_invoice.tx&returnFormat=PDF&test=true" \
"Authorization: ReportingCloud-APIKey $REPORTINGCLOUD_API_KEY" \
"Content-Type: application/json" \
@merge.json
Test requests are handled with a lower priority. Test mode is not available with a Free account, as Test Mode in Plans and Licensing describes. In the .NET SDK, MergeDocument, ConvertDocument, FindAndReplaceDocument and AppendDocument have a test parameter. In the PHP SDK, the option 'test' => true of the ReportingCloud constructor adds test=true to every request the instance sends.
Handle Errors in the SDKs
The SDKs turn a refused request into an exception.
.NET. Every method of the NuGet package TXTextControl.ReportingCloud throws a System.ArgumentException when the status is not a success code. The exception's Message is the body of the answer as it came, such as {"Message":"An error has occurred."}, and the exception has no property for the status code. When the service can't be reached, the methods throw an AggregateException whose inner exception is an HttpRequestException.
PHP. The SDK textcontrol/textcontrol-reportingcloud throws two exception classes of the namespace TextControl\ReportingCloud\Exception, which both implement its ExceptionInterface:
| Exception | Thrown when | getCode() |
|---|---|---|
RuntimeException | The Web API answered with a 4xx or 5xx status, or could not be reached | The HTTP status, or 0 without an answer |
InvalidArgumentException | The SDK refused an argument before sending the request, such as a template name with an unsupported extension, a dictionary not on its list or a missing API key | 0 |
The message of a RuntimeException names the request and the status, followed on the next line by the body of the answer, cut off after 120 characters, for example:
Client error: `GET https://api.reporting.cloud/v1/templates/pagecount?templateName=invoice.docx` resulted in a `400 Bad Request` response:
The following script reads the page count of a template and handles each case:
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use TextControl\ReportingCloud\Exception\InvalidArgumentException;
use TextControl\ReportingCloud\Exception\RuntimeException;
use TextControl\ReportingCloud\ReportingCloud;
$reportingCloud = new ReportingCloud([
'api_key' => (string) getenv('REPORTINGCLOUD_API_KEY'),
]);
try {
$pageCount = $reportingCloud->getTemplatePageCount('sample_invoice.tx');
printf("%d page(s)\n", $pageCount);
} catch (InvalidArgumentException $exception) {
// Refused by the SDK, no request was sent
echo 'Invalid argument: ', $exception->getMessage(), PHP_EOL;
} catch (RuntimeException $exception) {
$status = $exception->getCode();
if (400 === $status) {
echo 'The template is not in the template storage.', PHP_EOL;
} elseif (403 === $status) {
echo 'Not authorized: check the API key.', PHP_EOL;
} else {
// For example 0 when the service could not be reached
echo "Request failed ({$status}): ", $exception->getMessage(), PHP_EOL;
}
}
The code works as follows:
getTemplatePageCountchecks the template name before it sends anything. A name with a path or an unsupported extension throws theInvalidArgumentException.- Every answer with a
4xxor5xxstatus throws theRuntimeException, andgetCode()returns the status. The script tells the two causes of this endpoint apart by it. - A connection error throws a
RuntimeExceptionas well, with the code0.
Java and Ruby. The Java SDK throws an IllegalArgumentException and the Ruby gem raises a RuntimeError, each with the body of the answer as its message and without the status code. Java and Ruby in the SDKs section describe them.