Concepts
Errors
Error codes, what they mean, and which ones are safe to retry.
Oatmilk uses ordinary HTTP status codes and adds a stable code to every error. Branch on code, show message to people, and keep X-Request-Id in your logs: paste it into Developers › Request log to see who sent the request and how it ended.
{
"error": {
"code": "CONFLICT",
"message": "This record changed. Refresh it before retrying.",
"docUrl": "https://app.getoatmilk.com/docs/errors#conflict"
}
}| Status | Means |
|---|---|
200 | It worked. The result is in data. |
400 | The input doesn't match the action. field names the first problem. |
401 | The credential is missing, expired or revoked. |
403 | The credential or role doesn't allow this, or a person has to do it in Oatmilk. |
404 | The action or record doesn't exist, or you can't see it. |
409 | The record changed, or isn't in a state that allows this yet. |
413, 415 | The body is too large, or isn't JSON. |
429 | Too many requests. Wait for Retry-After. |
500, 503 | Something went wrong on our side, or a feature isn't ready. |
Common error codes
| Code | Status | Means | What to do |
|---|---|---|---|
UNAUTHORIZED | 401 | The Authorization header is missing, malformed, or the key or token has expired or been revoked. | Send Authorization: Bearer <key>. Check the key in Developers › API keys and create a new one if it was revoked. |
INSUFFICIENT_SCOPE | 403 | The key or token works, but it doesn't include a permission this action needs. | Every action's page lists the permissions it needs. Create a key that includes them. |
FORBIDDEN | 403 | Your role in the company doesn't allow this, or the step can only happen in the dashboard. | Ask an administrator, or give the person the dashboardUrl from the error. |
INTERACTIVE_ADMIN_REQUIRED | 403 | An administrator has to do this in Oatmilk itself, such as sending a payout. | Give the dashboardUrl from the error to an administrator. |
INTERACTIVE_APPROVAL_REQUIRED | 403 | A person approves this suggestion in Oatmilk. Listing and declining still work through the API. | Give the dashboardUrl to the person who approves it. |
INTERACTIVE_SIGNATURE_REQUIRED | 403 | Only the named signer can sign or decline, in their own session. | Send the signer the signUrl or portalUrl. Integrations can't sign for a person. |
INTERACTIVE_PORTAL_REQUIRED | 403 | The contractor does this in their own portal: bank details, tax numbers, or agreeing to tax slips by email. | Give the contractor the portalUrl from the error. |
COMPANY_PROTECTED | 403 | This is the company the Oatmilk installation runs for, so it can't be closed. | Nothing to fix: export its data with company.export if you need a copy. |
UNKNOWN_ACTION | 404 | There is no action with that name. Names are case-sensitive. | Check the spelling against the API reference. The error suggests the closest name when there is one. |
NOT_FOUND | 404 | The record doesn't exist in this company, or you can't see it. | Check the ID and the company you're working in (X-Accounting-Organization). |
METHOD_NOT_ALLOWED | 405 | GET was used for an action that changes something, or the method isn't GET or POST. | Use POST with a JSON body. GET only works for read actions. |
INVALID_INPUT | 400 | The input doesn't match the action's fields, or the body isn't JSON (status 415). | field names the first problem. Fix it and send the request again. |
CONFIRMATION_MISMATCH | 400 | The confirmation you typed doesn't match, such as the company's name when closing it. | Send the name exactly as Oatmilk shows it (case doesn't matter). Nothing was changed. |
TOO_LARGE | 413 | The JSON body is larger than 3 MB. | Upload files with uploads.prepare and a PUT to the upload address, not inside the JSON body. |
REQUEST_TIMEOUT | 408 | The request body took longer than 15 seconds to arrive. | Retry with the same idempotency key. |
IDEMPOTENCY_CONFLICT | 409 | The Idempotency-Key header and the idempotencyKey field were both sent, with different values. | Send one of them, or send the same value in both. |
STALE_REVISION | 409 | The record changed after you read it, so your `expectedRevision` is out of date. | Read the record again, check what changed, and send the new revision if your change still applies. |
CONFLICT | 409 | The record changed since you read it, or the idempotency key was already used for a different request. | Read the record again before deciding what to do. Use a new idempotency key for a new change; reuse one only to retry exactly the same request. |
DUPLICATE | 409 | A record with the same details already exists. | Look the existing record up instead of creating it again. |
INVALID_STATE | 409 | The record isn't in a state that allows this yet, such as approving something already approved. | Read the record's status and follow the next step it needs. |
RATE_LIMITED | 429 | This key sent more than 120 requests in the current minute. | Wait for the number of seconds in Retry-After, then retry with the same idempotency key. |
INTERNAL_ERROR | 500 | Something went wrong on our side. | Retry after a short wait with the same idempotency key. If it keeps happening, send us the X-Request-Id. |
SCHEMA_NOT_READY | 503 | The environment is finishing a database update for this feature. | Retry in a few minutes. |
CLOSE_UNAVAILABLE | 503 | This installation's database doesn't let Oatmilk delete a company's records in one step, so nothing was deleted. | Whoever runs the installation lets the database owner set session_replication_role, then the company can be closed. |
NOT_CONFIGURED | 503 | A feature this action needs isn't set up for your company or environment yet. | Finish its setup in Settings. The message says what's missing. |
Retrying safely
Retry only what can succeed later: 429, 500, 503, timeouts and dropped connections. Wait a little longer each time, and always resend the same Idempotency-Key so the change happens once. Don't retry other 4xx errors unchanged: they fail the same way until you fix the request. See Idempotency and revisions.
async function callOatmilk(action, input, idempotencyKey = crypto.randomUUID()) {
for (let attempt = 1; ; attempt += 1) {
const response = await fetch(`https://app.getoatmilk.com/api/v1/accounting/${action}`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.OATMILK_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey },
body: JSON.stringify(input),
});
if (response.ok) return (await response.json()).data;
const retryable = response.status === 429 || response.status >= 500;
if (!retryable || attempt === 5) throw new Error((await response.json()).error.code);
const wait = Number(response.headers.get("Retry-After")) || 2 ** attempt;
await new Promise(resolve => setTimeout(resolve, wait * 1000));
}
}Steps only a person can take
Some errors mean "a person does this in Oatmilk". They carry the link to the exact step, so your app can pass it on instead of failing.
| Code | What happens next |
|---|---|
INTERACTIVE_ADMIN_REQUIRED | An administrator finishes it in Oatmilk at dashboardUrl, such as sending a payout. |
INTERACTIVE_APPROVAL_REQUIRED | A person approves the suggestion at dashboardUrl. Listing and declining still work through the API. |
INTERACTIVE_SIGNATURE_REQUIRED | The signer signs or declines in their own session. Integrations can't sign for anyone. |
INTERACTIVE_PORTAL_REQUIRED | The contractor enters bank details or tax numbers in their own portal at portalUrl. |
Every error code
The API can return these codes. The list is generated from Oatmilk's source, so it always matches what the API does.
450 codes
ACCESS_ENDED403ACCESS_EXPIRED403ACCESS_REQUIRED403ACCOUNT_CONFIRMATION_REQUIRED409ACCOUNT_DETAILS_REQUIRED400ACCOUNT_EXISTS409ACCOUNT_NOT_ADDED503ACCOUNT_REQUIRED400AGREEMENT_CHANGED409AGREEMENT_REQUIRED409AGREEMENTS_OVERLAP409AI_CREDITS_REQUIRED403AI_KEY_REFUSED400ALREADY_ACCOUNTANT409ALREADY_INVITED409ALREADY_MATCHED409ALREADY_MEMBER409ALREADY_STARTED409ALREADY_SUBMITTED409AMOUNT_MISMATCH409ARCHIVE_FAILED503ARCHIVED409ATTACHMENT_LIMIT400ATTACHMENT_RETRIEVE503ATTACHMENT_SCOPE409AUDIO_TOO_LARGE413AUDIO_TOO_LONG413AUDIT_FAILED503BANK_MATCH_AMBIGUOUS409BANK_MISMATCH409BATCH_CLAIM_LOST409BATCH_SIZE400BUDGET_SCOPE_TOO_LARGE422CANNOT_REMOVE_SELF409CARD_IN_USE409CATEGORY_REQUIRED409CLASSIFICATION_FAILED503CLOSE_UNAVAILABLE503COMPANY_CHOICE_REQUIRED403COMPANY_LIMIT409COMPANY_PROTECTED403CONFIRMATION_MISMATCH400CONFLICT409CONNECTOR_CREDENTIAL_INVALID503CONNECTOR_KEY_MISSING503CONTRACTOR_CONFIRMATION_REQUIRED409CONTRACTOR_EXISTS409CORRECTION_CLOSED409CORRECTION_OPEN409CREDENTIAL_EMAIL_REQUIRES_MAILBOX403DATA_SOURCE_UNSUPPORTED400DATABASE_ERROR503DAY_FULL422DEPOSIT_EXCEEDED409DOCUMENT_CHANGED409DOCUMENT_STORAGE503DOWNLOAD_FAILED503DUPLICATE409DUPLICATE_ENTRY409DUPLICATE_NUMBER409EMAIL_CLAIM_CANCELLED409EMAIL_DISABLED409EMAIL_DOMAIN_EXISTS409EMAIL_DOMAIN_NOT_FOUND404EMAIL_DOMAIN_NOT_VERIFIED409EMAIL_DOMAIN_REJECTED409EMAIL_DOMAIN_SETUP_PENDING503EMAIL_DOMAIN_SETUP_REQUIRED503EMAIL_INBOUND_SETUP_REQUIRED503EMAIL_MISMATCH403EMAIL_NEEDED409EMAIL_NOT_ENABLED409EMAIL_UNVERIFIED403EMPTY_IMPORT400ENCRYPTION_KEY_CHANGED503ENCRYPTION_MISCONFIGURED503ENCRYPTION_NOT_CONFIGURED503ENTRY_EVIDENCE_BINDING409ENTRY_EVIDENCE_RECEIPT_DENIED409EVIDENCE_ARCHIVE_FAILED503EVIDENCE_HASH_MISMATCH409EVIDENCE_INTEGRITY409EVIDENCE_MISSING400, 409EVIDENCE_READ_FAILED503EVIDENCE_READING_PENDING409, 503EVIDENCE_REQUIRED409EVIDENCE_SCOPE403, 409EXPORT_TOO_LARGE400, 409, 413EXTRACTION_FAILED502EXTRACTION_TIMEOUT503FILE_FORMAT400, 415, 422FILE_SIZE400, 413FILE_TOO_LARGE400, 413FILE_TYPE400, 415FINANCIAL_SCOPE_TOO_LARGE422FINGERPRINT_KEY_READABLE409FINGERPRINT_KEY_UNREADABLE503FOLIO_REFUSED409FORBIDDEN403FORECAST_TOO_LARGE422FUTURE_HOURS422FX_RATE_NOT_PUBLISHED404FX_RATE_UNSUPPORTED422GOOGLE_DOCS_NOT_PUBLIC422GOOGLE_DOCS_TIMEOUT504GOOGLE_DRIVE_FORBIDDEN403GOOGLE_DRIVE_NOT_CONFIGURED503GOOGLE_DRIVE_NOT_CONNECTED409GOOGLE_DRIVE_NOT_FOUND404GOOGLE_DRIVE_SCOPE_MISSING400GOOGLE_DRIVE_STATE_INVALID400GOOGLE_DRIVE_TOKEN_FAILED400, 502HOURS_BEFORE_START409HOURS_LOCKED409HOURS_NOT_PAID409HOURS_ON_HOLD409HOURS_PAID409HUMAN_CORRECTED409IDEMPOTENCY_CONFLICT409IMAGE_LIMIT400, 422IMAGE_TOO_LARGE413IMPORT_FAILED409, 502, 503INBOX_SEARCH_BUSY429INBOX_SEARCH_DAILY_LIMIT429INBOX_SEARCH_RATE_LIMITED429INSUFFICIENT_SCOPE403INTEGRITY_ERROR409INTERACTIVE_ADMIN_REQUIRED403INTERACTIVE_APPROVAL_REQUIRED403INTERACTIVE_PORTAL_REQUIRED403INTERACTIVE_SIGNATURE_REQUIRED403INTERNAL_ERROR500, 502INVALID_ACCOUNT400, 409, 503INVALID_ADDRESS400INVALID_AI_RESULT400INVALID_AMOUNT400INVALID_ASSIGNEE400INVALID_AUDIO400INVALID_BUDGET_CATEGORY400INVALID_CATEGORY400INVALID_CSV400INVALID_CURRENCY400INVALID_CURSOR400INVALID_DATE400INVALID_DATE_RANGE400INVALID_DOCUMENT400INVALID_DOMAIN400INVALID_END_DATE400INVALID_EVIDENCE400INVALID_FIELD400INVALID_FINAL_CHARGE409INVALID_HEADER400INVALID_IDENTITY400INVALID_IMPORT400INVALID_INPUT400, 415, 422INVALID_INVITATION404INVALID_MAPPING400INVALID_MERCHANT400INVALID_PARENT400INVALID_PAYMENT_DETAILS400INVALID_PERIOD400INVALID_RECEIPT409INVALID_RECIPIENT400, 409INVALID_ROW400INVALID_SETTINGS409INVALID_SIGNATURE400INVALID_SOURCE_REFERENCE400, 409INVALID_STATE409INVALID_STRIPE_SIGNATURE400INVALID_TAX_NUMBER400INVALID_VERIFICATION_LINK400INVALID_WEBHOOK400INVESTIGATION_BUSY409INVESTIGATION_TOO_LARGE409INVITATION_CLOSED409INVITATION_EXPIRED409INVITATION_REVOKED409INVITATION_USED409INVITE_INVALID400, 403JOB_MISSING503JOB_SUPERSEDED409KIND_NOT_ALLOWED403LINK_KEY_CHANGED409LOOKUP_CREDITS_LOW503MAIL_HELD403MAIL_ORIGINAL_PENDING503MAIL_QUOTA429MAILBOX_ACCESS_REVOKED409MAILBOX_ADDRESS_MISSING502MAILBOX_CONNECT_FAILED503MAILBOX_CONNECT_RATE_LIMITED429MAILBOX_CREDENTIAL_INVALID409MAILBOX_GRANT_EXPIRED409MAILBOX_IMPORT_FAILED503MAILBOX_OFFLINE_ACCESS_MISSING409MAILBOX_PROVIDER_ERROR502MAILBOX_PROVIDER_NOT_CONFIGURED503MAILBOX_RATE_LIMITED429MAILBOX_SCOPE_MISSING409MAILBOX_SCOPE_TOO_BROAD409MAILBOX_STATE_EXPIRED400MAILBOX_STATE_INVALID400MAILBOX_STATE_MISMATCH403MAILBOX_STATE_USED400MAILBOX_TOKEN_FAILED502MATCHING_MODEL_RETRY_PENDING503MATCHING_SUPERSEDED409MATCHING_TOO_LARGE400METHOD_NOT_ALLOWED405MISSING_FIELDS400MULTIPLE_ACCOUNTS400NESTED_EMAIL_LIMIT400NO_ADMIN409NO_CHANGES400, 409NO_DOCUMENT409NO_FINGERPRINT_KEY409NO_HOURS_DATABASE400NO_MEMBER409NO_READABLE_SOURCE409NO_REQUESTS400NO_SPEECH422NOT_A_CARD_CHARGE409NOT_A_COMPANY_RECORD409NOT_A_HOTEL_CHARGE409NOT_A_PURCHASE409NOT_A_STATEMENT409NOT_AN_AGREEMENT422NOT_AVAILABLE409NOT_CONFIGURED409, 503NOT_FOUND404NOT_ISSUED409NOT_NEEDED409NOT_REGISTERED409NOT_SIGNED409NOT_TEAM_MEMBER409NOTES_LIMIT409NOTHING_CHANGED400NOTHING_TO_IMPORT400NOTION_ACCESS404NOTION_ARCHIVED409NOTION_FILE_GONE404NOTION_INVALID_VALUE400NOTION_LINK_INVALID400NOTION_MAPPING_INVALID400NOTION_MAPPING_REQUIRED409NOTION_NOT_CONFIGURED503NOTION_PROPERTY_READ_ONLY400NOTION_SYNC_FAILED503NOTION_WRONG_DATABASE409OLDER_AGREEMENT409ORGANIZATION_REQUIRED403ORIGINAL_IN_USE409ORIGINAL_REMOVED409PAGE_LIMIT400PAID_AMOUNT_REQUIRED400PARTY_HAS_AUTOMATIC_INVOICES409PAY_REQUIRED400PAYMENT_DETAILS_CHANGED409PAYMENT_TOO_SMALL409PERIOD_CLOSED409PERIOD_NOT_ENDED409PERIOD_PAID409PERIOD_SKIPPED409PERIODS_OVERLAP409PERSONAL_WORKSPACE403, 409POSSIBLE_DUPLICATE409PREVIEW_READ_ONLY403PREVIEW_SAMPLE404PROVIDER_ID400PROVIDER_RETRIEVE503PROVIDER_URL503RATE_LIMITED429READER_FAILED502RECEIPT_DETAILS_CHANGED409RECEIPT_DUPLICATE_REVIEW409RECEIPT_NOT_NEEDED409RECEIPT_ON_ITS_WAY409RECEIPT_TARGET_CHANGED409RECENTLY_SENT429RECIPIENT_IDENTITY_REVIEW409RECONCILIATION_TOO_LARGE400REMOVED_BY_PERSON409REPORT_TOO_LARGE400, 409REQUEST_CLOSED409REQUEST_TIMEOUT408RESTRICTED_EVIDENCE403REVERIFICATION_REQUIRED403REVIEW_REQUIRED409REVISION_CONFLICT409ROLE_IN_AGREEMENT409ROLE_MISMATCH409ROLE_REQUIRED409SAME_AGREEMENT_SIGNED409SCHEMA_NOT_READY503SEALED_DATA_UNREADABLE409, 503SEND_LEASE_LOST409SEND_STATE409SETUP_PENDING403SIGN_IN_REQUIRED401SIGNATURES_REQUIRED409SIGNERS_REQUIRED409SIGNING_KEY_EXISTS409SIGNUPS_CLOSED403SOURCE_LIMIT400STALE_FINANCIAL_SOURCE409STALE_HOURS409STALE_PROPOSAL409STALE_REVISION409STATEMENT_FORMAT422STATEMENT_IMPORTED409STATEMENT_PAGE_TOO_LONG422STATEMENT_SCOPE_TOO_LARGE413STATEMENT_SOURCE_CHANGED409STATEMENT_TOO_LARGE413STATEMENT_UNDECIDED500STATEMENT_UNDONE409STATUS_NOT_APPLICABLE409STORAGE_CONFLICT409STORAGE_ERROR503STRIPE_ACCOUNT_CHANGED409STRIPE_ACCOUNT_MISMATCH403STRIPE_ARCHIVE_FAILED503STRIPE_BODY_LIMIT413STRIPE_EVENT_SCOPE403STRIPE_HISTORY_REQUIRED409STRIPE_INVALID_AMOUNT502STRIPE_INVALID_CURRENCY502STRIPE_INVALID_CURSOR409STRIPE_INVALID_DATE502STRIPE_INVALID_EVENT400STRIPE_INVALID_EXCHANGE_RATE502STRIPE_INVALID_PAGE502STRIPE_INVALID_RESPONSE502STRIPE_INVALID_TRANSACTION502STRIPE_LEASE_LOST409STRIPE_MODE_MISMATCH400, 403STRIPE_NOT_CONFIGURED503STRIPE_NUMERIC_RUNTIME503STRIPE_PATH_DENIED400STRIPE_REPORT_LIMIT409STRIPE_RESPONSE_LIMIT502STRIPE_RESTRICTED_KEY_REQUIRED503STRIPE_TOTAL_CONFLICT502STRIPE_UNREPRESENTABLE_AMOUNT409SUBSCRIPTIONS_TOO_LARGE422TAG_NOT_LINKED409TAX_EXCEEDS_TOTAL400TAX_REVIEW_INCOMPLETE409TAX_REVIEW_REQUIRED409TAX_SETTINGS_REQUIRED400TAX_SOURCE_RECEIPT_DENIED409TERMS_PENDING409TEXT_LIMIT400TIMEOUT504TITLE_AGREEMENT_REQUIRED409TITLE_EVIDENCE_REQUIRED409TOO_LARGE413TOO_MANY_INVITATIONS409TOO_MANY_ROWS400TOO_MANY_TRANSACTIONS409TOO_SOON409TOTALS_CHANGED409TRANSCRIPT_TOO_LONG422TRANSCRIPTION_FAILED502UNAUTHORIZED401UNKNOWN_ACTION400, 404UNKNOWN_TOOL404UNPAID_WORK409UNREADABLE422UNREADABLE_EVIDENCE409UNSUPPORTED_AUDIO415UNSUPPORTED_RETRY_MODE409UPLOAD_BATCH_CHANGED409UPLOAD_FAILED503UPLOAD_INCOMPLETE409UPLOAD_INTEGRITY409UPLOAD_LIMIT429WAITLIST_NOT_NEEDED409WAITLISTED403WEBHOOK_KEY_CHANGED503WEBHOOK_KEY_MISSING503WEBHOOK_SECRET_INVALID503WEBHOOK_URL_INVALID400WISE_ACCOUNT400WISE_ATTACHMENT_NOT_FOUND404WISE_CURRENCY502WISE_DOCUMENT_NOT_READY404WISE_DOCUMENT_TOO_LARGE502WISE_FORMAT502WISE_FUNDING_PENDING409WISE_NO_BALANCE409WISE_NOT_CONFIGURED409, 503WISE_NOT_READY409WISE_PATH400WISE_PERIOD_NOT_STARTED409WISE_PRECISION503WISE_PROFILE400WISE_PROFILE_MISSING409WISE_PROFILE_OWNER403WISE_QUOTE_BLOCKED409WISE_RECIPIENT409WISE_RESPONSE502WISE_SCA_REJECTED502WISE_SETUP_REQUIRED400, 409WISE_SOURCE_BINDING_PENDING409WISE_STATUS_PENDING409WISE_SYNC_CONFIG_CHANGED409WISE_SYNC_LEASE_LOST409WISE_TRANSFER409, 503WISE_TRANSFER_DETAILS_REQUIRED409WORKSPACE_PAUSED403WRITE_OFF_TOO_LARGE400