How the API responds when something goes wrong, how many calls fit in a minute, how to go through large lists and how to repeat a request without duplicating anything.
Error format
Every /api/v1 error comes in application/problem+json (RFC 9457):
{
"type": "about:blank",
"title": "Unprocessable Content",
"status": 422,
"detail": "Some fields are invalid.",
"code": "validation_failed",
"errors": { "address.line": ["The address field is required."] },
"request_id": "9a6c1f2e-3b4d-4e5f-8a9b-0c1d2e3f4a5b"
}
codeis stable: it does not change between versions. Decide by it.detailis for people to read, in the language requested inAccept-Language(pt-BR,enores). The text may change.errorsonly invalidation_failed: messages per field.scopesonly ininsufficient_scope: the missing permissions.request_idis the same as theX-Request-Idheader. Give it to support.
Codes
code |
Status | Meaning |
|---|---|---|
unauthenticated |
401 | Key missing, wrong, expired or revoked |
api_disabled |
403 | The API is turned off for the school |
insufficient_scope |
403 | The key does not have the route's permission |
forbidden |
403 | Access denied |
student_limit_reached |
403 | The school's plan has reached its student limit |
not_found |
404 | The record does not exist in this school |
method_not_allowed |
405 | HTTP method not accepted at this address |
bad_request |
400 | Malformed request |
invalid_cursor |
400 | Invalid page cursor: restart the list |
idempotency_key_required |
400 | The route requires the Idempotency-Key header |
idempotency_key_invalid |
400 | Idempotency-Key in the wrong format |
conflict |
409 | The request conflicts with the current state of the record |
idempotency_in_progress |
409 | Another request with the same Idempotency-Key is still running |
enrollment_conflict |
409 | The student already has a live enrollment in the course or track (see existing_enrollment_id) |
enrollment_not_cancellable |
409 | The enrollment is already cancelled or completed |
installment_not_payable |
409 | The installment cannot have a second copy (paid, cancelled or waived) |
boleto_still_valid |
409 | The current boleto is still valid and cannot be cancelled at the bank |
gateway_not_connected |
409 | The school has not connected its payment account |
student_inactive |
409 | The student is inactive |
student_without_portal_access |
409 | The student has no e-mail or no portal account |
portal_not_configured |
409 | The school's student portal has no address |
portal_link_ambiguous_email |
409 | Two students at the school use the same e-mail |
validation_failed |
422 | Invalid fields (details in errors) |
idempotency_key_reused |
422 | The Idempotency-Key was already used with different data |
rate_limited |
429 | Rate limit reached |
internal_error |
500 | Internal error at EduPay |
gateway_error |
502 | The bank declined or did not respond |
http_error |
others | The request could not be completed |
Rate limits
| Limit | Per minute |
|---|---|
Reads (GET) per key |
120 |
Writes (POST, PATCH, DELETE) per key |
30 |
| All of the school's keys combined | 300 |
Every response has X-RateLimit-Limit and X-RateLimit-Remaining. Once you go over the limit, the response is 429 with Retry-After (in seconds). Wait that long and repeat.
The Moodle plugin routes have their own limit and do not count here.
Paginated lists
Lists return up to limit items (1 to 200; without it, 50), in creation order:
{
"data": [ { "id": 101, "...": "..." }, { "id": 102, "...": "..." } ],
"meta": { "next_cursor": "eyJpZCI6MTAyfQ", "has_more": true }
}
For the next page, repeat the call with cursor= set to meta.next_cursor, keeping the other filters. Stop when has_more is false. The cursor never skips or repeats a record, even if new records come in meanwhile.
To sync only what changed, use updated_since (date and time in ISO 8601).
Retry safely
Writes (POST and PATCH) accept the Idempotency-Key header; creating an enrollment requires it. Send a unique code per request, from 1 to 255 visible characters (a UUID or the order number in your system).
| Situation | What happens |
|---|---|
| Same key, same request, within 24 hours | The first response comes back, with Idempotent-Replayed: true. Nothing is written again |
| Same key, different data | 422 with code idempotency_key_reused |
| Same key while the first request is still running | 409 with code idempotency_in_progress: wait a few seconds and repeat |
| The first request returned an error | The key is free again: fix the request and repeat it with the same key |
The key is valid per school and for 24 hours. "Same request" means the same method, the same address and the same data, in any field order.
When to retry
429: afterRetry-After.500,502,503or timeout: with increasing waits (1 s, 2 s, 4 s...) and the sameIdempotency-Key.400,401,403,404,409and422: repeating the same request will not help. Fix the request, the key or the permission.