Error Handling
Most Fangate API endpoints use the application's standard JSON envelope:
{
"success": true,
"errors_message": null,
"data": {}
}For failures:
{
"success": false,
"errors_message": "Human readable message",
"data": null
}Machine-readable codes
Every JSON error under /api carries an errors_code next to the message, so a client does not have to match on text: a named code where the API knows the exact reason, otherwise the fallback for the HTTP status (validation_failed, forbidden, not_found, ...). Validation failures add errors_field, the first field that failed. The full list is on Error codes. The fields are additive - message and status are unchanged.
{
"success": false,
"errors_message": "Confirm the AI content declaration before uploading.",
"errors_code": "ai_attestation_required",
"data": null
}errors_code | HTTP | Where |
|---|---|---|
ai_origin_required | 422 | POST /api/media, POST /api/products - a new file without its AI classification (see AI content). |
ai_attestation_required | 422 | Same calls and PATCH /api/media/{media}/ai-origin - an AI value without ai_attested: true. |
price_below_minimum | 422 | POST /api/products, PATCH /api/products/{product}/price, POST /api/products/{product}/price-links - a price under the floor of 500 minor units (read it from GET /api/app-data → products.min_price_cents). |
product_text_too_short | 422 | POST /api/products, PATCH /api/products/{product}, POST /api/products/{product}/price-links - from 27 September 2026: neither title nor public_description has 15 characters (one of the two is enough; spaces at the ends do not count). Reported on public_description. A call that only attaches a file (product_id) or changes neither text is never refused for this. See POST /api/products. |
The verification-required response below carries errors_code: creator_verification_required as well; data.error_code holds the same value and stays for older clients.
Rate limits
The OpenAPI file carries every limit machine-readable: operations with a limit of their own have an x-ratelimit object - limit per period, what it is counted per, the ip_ceiling (and ip_ceiling_agency_programs / ip_ceiling_registered_servers where agency programs and registered servers get more), and daily_limit where one applies.
Creator endpoints are throttled. Every throttled response carries X-RateLimit-Limit and X-RateLimit-Remaining; a 429 adds Retry-After (seconds to wait) and X-RateLimit-Reset (unix timestamp of the next window). The 429 body is Laravel's plain {"message": "Too Many Attempts.", "errors_code": "rate_limited"}, not the envelope. Current limits: media list and single-file reads 60/min per key and IP (120/min per IP), product list 60/min per key and IP (120/min per IP; the single-product read shares that counter for generated keys), wallet and analytics 30/min per key and IP (60/min per IP), AI-origin corrections 60/min per account, webhook redeliveries 10/min and 300/day per account (one per minute per delivery), key management see API Keys.
Agencies. An agency's API key that works inside one of its accounts - naming the account with the X-Fangate-Account header - is counted per account: every account has its own counters with the limits above (media and product reads 60/min, wallet and analytics 30/min), the same as for that account's own key. Across all of its accounts the agency's program is held to 1000/min per IP for media reads, 1000/min per IP for product reads and 500/min per IP for wallet and analytics, instead of 120 / 120 / 60. This ceiling is counted apart from every other caller behind the same address, so neither uses up the other's. A key the agency created on a creator's account is counted the same way.
Your own servers. The ceilings per IP are meant for one caller's address. If your software runs the keys of many creators or agencies from a few fixed servers - a CRM or a vault that every client connects with their own key - all of those keys share the ceiling of that address, and creator keys share the regular one (120 / 120 / 60). Send us the egress addresses of your servers (single IPs or CIDR ranges) by mail to info@fangate.app; calls with a generated key from a registered address are counted on a ceiling of their own, 1000/min per IP for media and product reads and 500/min per IP for wallet and analytics, for creator and agency keys alike - the agency program ceiling, but for every key from that address. The limit per key and account is unchanged.
Important exception
Not every endpoint uses the standard wrapper.
Current notable exceptions include:
GET /api/veriff/createPOST /api/veriff/decisionGET /api/maps/autocomplete,GET /api/maps/place-details(raw Google payloads)POST /api/report- validation failures only; a stored report answers the envelope withdata.idGET /api/analytics/*-successanddatawithouterrors_messagePOST /api/yoti/session/create
These endpoints should be handled according to their endpoint-specific schemas in the API reference.
Common status codes
| Status | Meaning | Typical cause |
|---|---|---|
200 | Success | Standard successful request |
201 | Created | Resource created successfully |
202 | Accepted | Async processing has started, but work is not finished yet |
204 | No content | Request succeeded without a response body |
400 | Bad request | Invalid body or malformed request |
401 | Unauthorized | Missing or invalid bearer token |
403 | Forbidden | Token is valid, but user cannot access the resource |
404 | Not found | Resource id or route target does not exist |
422 | Validation / business rule failure | Rule violation, workflow constraint, or additional action required |
500 | Internal server error | Unexpected backend failure |
Validation failures
Validation failures may appear as:
- the standard
success=falseenvelope witherrors_message - Laravel-style validation payloads for endpoints that do not use the wrapper
Examples:
{
"success": false,
"errors_message": "The email has already been taken.",
"data": null
}or, for a non-wrapped validation response:
{
"message": "The given data was invalid.",
"errors": {
"email": [
"The email has already been taken."
]
}
}Async upload / processing responses
Some content-creation and upload flows can return 202 Accepted.
This means:
- Fangate has accepted the request
- background processing is still happening
- the client must poll the documented status endpoint or refresh the resource later
This is especially important for:
- upload sessions
- consent flows where media is not fully ready yet
Consent-tag verification response
When a tagged creator tries to accept or decline a consent tag before completing Veriff, Fangate currently returns a verification-required response instead of applying the decision immediately.
Expect fields such as:
{
"success": false,
"errors_message": "Verification required",
"data": {
"requires_verification": true,
"verification": {
"url": "https://..."
}
}
}Upload and storage failures
Common failure categories in upload flows:
- staged upload expired
- multipart part list invalid
- completion called before all required parts exist
- storage provider returned an error
- finalization job failed after upload
Client guidance:
- treat presigned-upload instructions as time-limited
- retry upload-session polling before recreating the entire product
- if finalization fails, surface the backend message and restart the upload flow cleanly
Authentication and authorization failures
| Scenario | Expected result |
|---|---|
| Missing bearer token | 401 |
| Invalid / revoked bearer token | 401 |
| Accessing another creator's product or wallet data | 403 |
| Acting on a consent tag that does not belong to the caller | 403 or 404 depending on resolution path |
Error-handling recommendations
- Always check the HTTP status code first.
- Then inspect
success,errors_message, anddatawhere the standard envelope is used. - Do not assume that
200means the workflow is fully complete. Some flows use follow-up status checks. - Do not assume every validation failure has the same JSON shape.
- Log raw backend responses during integration until your client is stable.