Errors and limits
What a failed request looks like, how fast you may call, how to trace a request, and what happens when your webhook endpoint is down.
Error shapes
The status code says what went wrong. Bodies follow Cronofy’s conventions, so existing Cronofy clients read them unchanged.
| Status | Body | When |
|---|---|---|
| 400 | {"error": "…"} | OAuth endpoints only (/oauth/token, /oauth/token/revoke), including wrong client credentials. |
| 401 | empty, with WWW-Authenticate | The access token is missing, expired or revoked; or wrong client credentials on /v1/application_calendars. |
| 403 | empty | Writing to a read-only calendar. |
| 404 | empty | A calendar or channel the account does not have. Another customer’s id is “not found” too. (DELETE /v1/channels/… sends a small JSON body, as Cronofy does.) |
| 422 | {"errors": {…}} | The request is invalid; the body says which fields and why. |
| 429 | empty, with Retry-After | Rate limited (below). |
| 5xx | empty | Our fault. Retry with backoff; if it persists, write to support@calmonkey.com with the request id. |
422: field errors
{
"errors": {
"tzid": [
{ "key": "errors.required", "description": "required" }
],
"from": [
{
"key": "errors.invalid",
"description": "must be a date (YYYY-MM-DD) or an ISO 8601 time with an offset, e.g. 2026-10-10T01:00:00Z"
}
]
}
}
Each field maps to a list of { key, description }. Match on key; description is for people and may be reworded. Nested fields are written location.description, array items calendar_ids[0], problems with the whole body base.
| Name | Type | Description |
|---|---|---|
errors.required | key | The field is missing. |
errors.invalid | key | The value is not acceptable (a time, a time zone, end before start…). |
errors.invalid_type | key | Wrong type, such as a number where a string belongs. |
errors.invalid_format | key | Wrong format. |
errors.invalid_value | key | Not one of the allowed values (e.g. transparency). |
errors.too_small / errors.too_big | key | Too short or too long, or too many items. |
errors.invalid_json | key | The body is not a JSON object (base). |
errors.invalid_callback_url | key | The callback URL is not an https URL. |
errors.invalid_callback_url_blocked | key | CalMonkey will not call that host: local, private or example addresses, or a port other than 443. |
errors.too_many_channels | key | The account already has 100 open channels. |
Event writes have keys of their own for guests, repeating events and meeting links, such as errors.invalid_email, errors.attendees_unsupported, errors.not_an_occurrence and errors.conferencing_unavailable. They are listed in the API reference.
Applications in Cronofy compatibility mode get Cronofy’s wording instead: "tzid must be specified"-style descriptions, errors.tzid_unrecognized for a missing or unknown tzid, and errors.invalid_time_pair when an event ends before it starts.
OAuth errors
{ "error": "invalid_grant" }
| Name | Type | Description |
|---|---|---|
invalid_client | error | Unknown client id or wrong client secret (the two are not told apart). |
invalid_grant | error | The code is used, expired or was issued for another redirect URI; or the refresh token is revoked or belongs to another application. |
invalid_request | error | A required field is missing or the body cannot be read; error_description says which. |
unsupported_grant_type | error | Only authorization_code and refresh_token are supported. |
On /oauth/authorize errors go back to your redirect URI as error= (access_denied, unsupported_response_type, invalid_scope), except an unknown client id or redirect URI, which shows an error page.
Rate limits
Two token buckets apply to every authenticated request. A request needs room in both.
| Bucket | Burst | Sustained |
|---|---|---|
| Per application | 600 requests | 100 a second |
| Per authorised account (access token) | 150 requests | 25 a second |
Responses carry the state of the tighter bucket:
RateLimit-Limit: 150
RateLimit-Remaining: 149
RateLimit-Reset: 1
RateLimit-Reset is the number of seconds until the bucket is full again. Over the limit the answer is 429 with an empty body and Retry-After (seconds): wait that long, then retry.
Request ids and the request log
Every response has a Calmonkey-Request-Id header. Send your own in the request’s Calmonkey-Request-Id header (8 to 64 letters, digits, dots, dashes or underscores) and it comes back unchanged, which ties our logs to yours.
Authenticated API requests are kept in your request log in the dashboard for 30 days (7 days on the Developer plan): method, path, status, duration and the bodies, with tokens, credentials and event text replaced by [redacted].
Webhook delivery and retries
A notification counts as delivered when your callback URL answers with a 2xx status within 5 seconds. Redirects are not followed and count as failures. Anything else is retried:
| Attempt | When |
|---|---|
| 1 | Straight away (a change waits about a second, so changes close together share one notification) |
| 2 | 15 seconds after the first failure |
| 3 | 1 minute later |
| 4 | 5 minutes later |
| 5 | 15 minutes later |
| 6 | 1 hour later |
| 7 | 2 hours later |
| 8 | 4 hours later |
| 9 | 6 hours later |
| 10 | 10 hours later, the last attempt (about 23 hours after the first) |
- Each wait has up to 10% added at random, so a recovering endpoint is not hit by every waiting notification in the same second.
- After the tenth failed attempt, or 48 hours after the notification was created, the delivery is marked failed.
410 Gonecloses the channel: no more notifications are sent to it.- Every attempt of one notification has the same
Calmonkey-Delivery-Id, andCalmonkey-Delivery-Attemptcounts up. Delivery is at least once: ignore an id you have already handled. - Deliveries and their attempts are listed in the dashboard for 30 days.
