Errors
The error response format, how to handle each kind of failure, and every error code the API returns.
Every failed request returns a JSON body with a stable, machine-readable code and a
message written for humans:
{ "code": "transaction_not_found", "message": "Transaction not found." }Branch on code. Message wording can change at any time, so don't match on it or show
it to end users as-is. Treat a code you don't recognize as a generic failure of its
HTTP status. While Penny is in pre-launch development, codes can change; see the
compatibility policy.
Handling errors
Authentication: 401
not_authenticated means the Authorization header is missing or the token is
malformed, expired, or revoked. Retrying won't help. Create a new token in
Settings → API Tokens and update your integration.
Permissions: 403
read_only_token: a read-only token made a write request. Use a read/write token.not_household_member: you don't belong to the household in the path, or you were removed from it. CallGET /v1/householdsto see the households you can access.insufficient_household_role: your role in the household doesn't allow the action. Viewers can only read, and some routes are limited to the owner.
A token never has more access than the person who created it, so a read/write token
held by a viewer still gets insufficient_household_role on writes.
Subscription: 402
subscription_required means the household's subscription has lapsed. The body also
carries the household_id it applies to:
{
"code": "subscription_required",
"message": "This household's subscription has lapsed",
"household_id": "Hh4vK8pQ2sLm"
}GET /v1/households reports these households with an access_status of
subscription_required, so you can check before requesting a household's data.
Validation: 422
validation_error means the request was understood but one or more values were
rejected. The errors array lists every field that failed, so you can fix them in one
pass:
{
"code": "validation_error",
"message": "Some fields are invalid.",
"errors": [
{
"field": "splits.0.amount",
"code": "decimal_parsing",
"message": "Input should be a valid decimal",
"context": null
}
]
}field is the path to the value, without the request source (body, query, or
path). context holds extra validator detail as a string-valued object, or null.
Not found: 404
A *_not_found code means the resource doesn't exist, was deleted, or belongs to a
different household. The API doesn't say which, so it never confirms that someone
else's ID exists.
Bulk transaction endpoints skip transaction IDs that are missing or ineligible and
return the transactions they changed. They return a 404 only when nothing matched,
so compare the returned IDs with the ones you sent to find what was skipped.
Conflicts and invalid operations: 400 and 409
These mean the request was well formed but can't be applied. A 409 conflicts with
the resource's current state: a name that's already taken (duplicate_name), an
export that's still being prepared (export_in_progress), or a transaction that
can't be split because it's pending. A 400 is a request that can never succeed as
sent, such as split amounts that don't add up to the original
(sum_does_not_match_total). Change the request or the resource before retrying.
Pagination: 400
invalid_cursor means the cursor is malformed, and filters_changed means it was
sent with different filters from the request that produced it. In both cases, start
again without a cursor.
Uploads: 400 and 413
Images must be JPEG, PNG, or WebP (invalid_file_type) and no larger than 5 MiB
(file_too_large).
Server errors: 500 and 503
internal_error means something went wrong on our side. Read requests are safe to
retry. The API has no idempotency keys, so after a failed write, read the resource
back before sending the request again.
service_unavailable means the request waited too long for another change to the
same household, such as a bank sync, and nothing it asked for was applied. Retry it
after a short wait.
Requests that don't reach an endpoint
A request body that isn't valid JSON returns 400 with the code bad_request. A path
that doesn't exist returns 404 with not_found, and an HTTP method an endpoint
doesn't support returns 405 with method_not_allowed, and a JSON body sent without
Content-Type: application/json returns 415 with unsupported_media_type. All four
use the same code and message body, with the code named after the HTTP status.
Error codes
Every code the API returns. Each endpoint's page lists the ones that apply to it.
| Code | Status | Example message | Returned by |
|---|---|---|---|
bad_request | 400 | Bad Request | 39 endpoints |
child_cannot_unsplit | 400 | Cannot unsplit a child of a split; pass the parent transaction id. | Unsplit Transaction |
filters_changed | 400 | Filters have changed since the cursor was created. Please start a new query. | List Connection Status, List Institutions, Dry Run, List Transactions |
invalid_cursor | 400 | Invalid pagination cursor. Please start a new query. | 8 endpoints |
invalid_file_type | 400 | File must be JPEG, PNG, or WebP | 5 endpoints |
invalid_frequency_fields | 400 | Invalid frequency fields: twice-monthly frequencies must omit start_date; all other frequencies require start_date | Create Recurring Transaction, Update Recurring Transaction |
invalid_month_format | 400 | month must be in YYYY-MM format | Get Summary |
invalid_rule_ids | 400 | Rule IDs must match exactly all rules in the household | Reorder Rules |
split_sign_mismatch | 400 | Each split's sign must match the original transaction. | Split Transaction |
sum_does_not_match_total | 400 | The sum of the split amounts must equal the original transaction amount. | Split Transaction |
too_few_splits | 400 | A split must contain at least two parts. | Split Transaction |
zero_amount_split | 400 | Split amounts must not be zero. | Split Transaction |
not_authenticated | 401 | Authentication required | 100 endpoints |
subscription_required | 402 | This household's subscription has lapsed | 89 endpoints |
insufficient_household_role | 403 | Insufficient household role | 62 endpoints |
not_household_member | 403 | Not a member of this household | 94 endpoints |
read_only_token | 403 | This API token only permits read requests | 63 endpoints |
account_not_found | 404 | Account not found | 11 endpoints |
category_group_not_found | 404 | Category group not found | Create Category, Update Category, Update Category Group, Delete Category Group |
category_not_found | 404 | Category not found | 11 endpoints |
export_not_found | 404 | Export not found | Get Export |
image_not_found | 404 | Image not found. | Bulk Delete Transaction Images, Delete Transaction Image |
merchant_not_found | 404 | Merchant not found | 15 endpoints |
no_image | 404 | No image to delete | Delete User Image, Delete Household Image, Delete Merchant Image, Delete Account Image |
not_found | 404 | Not Found | A path that no endpoint serves |
recurring_transaction_not_found | 404 | Recurring transaction not found | Get Recurring Transaction Occurrences, Get Recurring Transaction, Update Recurring Transaction, Delete Recurring Transaction |
rule_not_found | 404 | Rule not found | 5 endpoints |
security_not_found | 404 | Security not found | Get Security Prices |
suggestion_not_found | 404 | Dismissed suggestion not found | Restore Suggestion |
tag_not_found | 404 | Tag not found | 13 endpoints |
transaction_not_found | 404 | Transaction not found | 24 endpoints |
method_not_allowed | 405 | Method Not Allowed | An endpoint, for a method it doesn't support |
account_not_manual | 409 | Transactions can only be created for manually-added accounts. | Create Transaction |
child_cannot_split | 409 | Cannot split a transaction that is already a child of another split. | Split Transaction |
duplicate_emoji | 409 | Category with this emoji already exists | Create Category, Update Category |
duplicate_name | 409 | This name is already in use | 9 endpoints |
export_in_progress | 409 | An export is already being prepared for this household | Request Export |
transaction_deleted | 409 | Cannot split a deleted transaction. | Split Transaction |
transaction_not_split | 409 | Transaction is not currently split. | List Splits, Unsplit Transaction |
transaction_pending | 409 | Cannot split a pending transaction. | Split Transaction |
file_too_large | 413 | File exceeds maximum size of 5MB | 5 endpoints |
unsupported_media_type | 415 | Unsupported Media Type | 38 endpoints |
validation_error | 422 | Some fields are invalid. | 63 endpoints |
internal_error | 500 | Internal server error | Any endpoint |
service_unavailable | 503 | Service Unavailable | Any endpoint that changes data |
Last updated on