Documentation
API error reference
Every non-2xx response from the Peerfold API is an RFC 7807 problem document, served as application/problem+json. Its type field is a URL into this page — one anchor per code, so a type of https://www.peerfold.com/developers/errors#plan_required lands on the plan_required section below.
The code field is the stable machine key. Branch on it. Do not branch on title or detail, which are prose and may be reworded, and do not match on type, whose host moves when the documentation moves. Codes are added over time; a code is never repurposed. Treat an unrecognized code as the status class it arrives with.
{
"type": "https://www.peerfold.com/developers/errors#plan_required",
"title": "Plan upgrade required",
"status": 403,
"code": "plan_required",
"detail": "Your plan does not include API access.",
"request_id": "req_01J…"
}detail is present when there is something specific to say and absent otherwise. request_id is present on every error raised inside a request context; quote it in support tickets. Errors also carry the Peerfold-Version response header, and 429s carry Retry-After.
A few codes cover more than one refusal, and those responses carry a second machine key: reason. It is an RFC 9457 extension member, it is as stable as code, and it names the branch inside the code rather than replacing it. Handle codefirst, then reason inside that branch, and treat a missing or unfamiliar reason as the code on its own. The sections below list the values for every code that has any. Registering a partner lead is the case to look at: lead_taken arrives with own_expired, owned_by_other, claim_not_permitted or existing_customer, and only the first of those clears by sending the request again.
bad_requestvalidation_failedidempotency_key_requiredunauthorizedinvalid_tokenforbiddenplan_requiredaccess_deniedlearner_access_blockedcheckin_refusednot_foundnot_enrolledidempotency_conflictenrollment_protectedusername_takenslug_takenidea_cap_reachedpoll_lockedlead_takenresource_in_useseat_unavailablealready_ownedcontent_unavailablesession_fullsession_endedcheckin_closednot_registeredgate_blockedusername_requiredrate_limitednot_configuredinternal_error
400: the request
The request never reached the resource. Nothing changed on the server, and resending the same bytes fails the same way.
bad_request400
The request could not be read: unparseable JSON, a missing required parameter, or a value the endpoint cannot use at all.
Read detail, fix the request, and send it again. Do not retry it unchanged.
validation_failed400
The request parsed, but a field failed the endpoint's schema: wrong type, out of range, or an unknown enum value.
detail names what failed. Correct that field and resend.
idempotency_key_required400
The endpoint replays safely and therefore insists on it: an Idempotency-Key header is mandatory here and was not sent. Progress writes are the main case.
Resend with a unique Idempotency-Key. Reuse that same key for retries of the same write.
401: the credential
The API could not establish who is calling. Both codes are about the credential itself, never about permissions.
unauthorized401
No credential was presented on an endpoint that requires one.
Send a secret key as Authorization: Bearer sk_… on the admin plane, or a learner JWT on the learner plane.
invalid_token401
A credential was presented and is not usable: an unknown or revoked secret key, or a learner JWT that is expired, malformed, or signed for another workspace.
Mint a fresh learner token, or check the key in Settings → Developers. Retrying with the same credential will not start working.
403: permission, plan, and access rules
The caller is known. Three different reasons they still may not do this, kept apart because the fix for each is a different person's job.
forbidden403
The credential is valid but is not allowed to make this call. The cause is a missing key scope, the wrong plane, a test key on a live-only flow, or a workspace that requires SSO for this sign-in. On the partner plane it also covers three program rules, and the reason field says which: tier_permission when the caller's commercial tier does not include the action, agreement_required when their program asks them to sign an agreement before they register a lead or ask for funding, and signing_closed when an agreement can no longer be signed or declined.
Use a credential that carries the scope or the plane the endpoint needs. This is a configuration fix, not a retry. Where a reason is present, branch on it: tier_permission and agreement_required are for a program administrator to settle, and signing_closed means the document is finished, so stop offering the control.
plan_required403
The workspace's plan or add-on does not include the capability the endpoint provides. API access, the lead-gen flows, the serverless relay and webhook redelivery are each gated this way.
Upgrade the workspace or buy the add-on. detail names the plan the call needs.
access_denied403
The learner is authenticated, and the content's access rules keep them out, as when they try to enroll in a course they are not eligible for.
Grant the learner access (membership, organization, or a direct enrollment made by an admin), then retry.
learner_access_blocked403
The workspace has turned this individual learner's academy access off. Their token is still valid, so re-authenticating changes nothing, and every learner-plane call answers this until somebody turns them back on. The learner keeps every enrollment, certificate and progress record while they are out.
Do not retry and do not clear their local data. Show the detail verbatim: it carries the workspace's own message for a blocked learner, the same one served as academy_access.blocked_message on the branding payload. An administrator turns it back on from the learner's record, from the API, or from a HubSpot workflow.
checkin_refused403
The live session records attendance itself, either by the host marking the roster or by the meeting provider reporting it, so there is no learner-facing check-in on it.
Do not retry, and draw no check-in button: tell the learner their host records attendance. The session's attendance_mode says which sessions do offer one.
404: not visible
A 404 from this API means “not visible to you”, which is deliberately weaker than “does not exist”. Workspace scoping, private community cohorts, and workspaces without the community add-on all answer 404 rather than leak that something is there.
not_found404
No such resource is visible to this credential. The resource may not exist, may belong to another workspace, or may be one the caller is not permitted to know about.
Check the id and the workspace the key belongs to. Never treat a 404 as proof the record was deleted.
not_enrolled404
The enrollment addressed by the call does not belong to the calling learner, so there is no progress, quiz or exam record to read or write.
Enroll the learner first, then use the enrollment id the enrollment call returned.
409: conflict with existing state
The request was well-formed and permitted; something already on the server contradicts it.
idempotency_conflict409
The Idempotency-Key has already been used for a request with a different body.
Use a new key for a genuinely new request. To get the original response back, resend the original body with the original key.
enrollment_protected409
The enrollment cannot be removed through the API because another system owns it: a purchase, a membership, or an org-level grant.
Remove it where it came from. detail names the source.
username_taken409
Community only. Another member of this workspace already holds the requested username.
Ask the member for a different one and resubmit.
slug_taken409
Community only. Another record in this workspace already uses the requested URL key.
Offer a different key and resubmit.
idea_cap_reached409
Community only. The member already holds as many OPEN ideas on that board as it allows. Shipped and declined ideas do not count against the limit.
Do not retry. A second attempt cannot succeed until a slot is freed. Move one of their ideas to shipped or declined, raise the board's limit, or give that member their own.
poll_locked409
Community only. The poll already has at least one vote, so its options are fixed, since changing them would change what those voters chose.
Do not retry. A poll never becomes editable again once somebody has answered it; post a new thread with a new poll if the options were wrong.
lead_taken409
Partner programs only. That account cannot be registered by this partner right now: somebody else holds it and their protection window is still running, the window has lapsed but this partner's tier may not claim another partner's lead, the account already has a won deal on it, or the caller's own earlier registration has lapsed. The reason field says which of the four it is, and the detail names who holds it and when the window clears.
Branch on reason. own_expired is the caller's own lapsed registration and clears by sending the same body again with confirmed: true; owned_by_other names a date to wait for; claim_not_permitted and existing_customer cannot be retried into a success. Read detail to the person, never with a regular expression.
resource_in_use409
Something still points at the record you asked to remove, so removing it would orphan them.
Move whatever depends on it first. The detail says how many there are.
seat_unavailable409
Organizations only. A seat call could not be honored, and the detail says which of four cases it was: the pool has no seats left, the batch you sent is bigger than the seats remaining, the pool has expired, or the seat you asked to return is already back or was spent on a learner who finished the training.
Read the detail. A batch that does not fit is refused whole, so nothing was assigned and a shorter list will land. Expired seats need renewing before anything can be assigned from them. A seat spent on a completion never comes back, so the next person needs a seat of their own.
already_owned409
The buyer already holds what this checkout would sell them, either an enrollment in the program or a live membership on the tier.
Send them to the thing they own rather than to a payment page. Nothing was charged and no order was created.
content_unavailable409
The request is right and there is still nothing to draw. An interactive experience whose workspace has no add-on, or that nobody has uploaded yet, or whose file has since been deleted. A lesson form an admin retired, one whose every question was archived, or a HubSpot form whose portal was disconnected between the page loading and the reader pressing Send.
Show detail in the block's place. It is written for a learner to read, not for a log. Retrying changes nothing until an admin changes something.
session_full409
The live session has no seats left. There is no waitlist, so a full session refuses rather than queuing anybody.
Offer another date if the session is a series, or another session. Retrying the same date only succeeds if somebody cancels.
session_ended409
The live session is over, so there is nothing left to register for.
Do not retry. A learner who attended but was never registered is marked present from the roster by an admin, not by registering after the fact.
checkin_closed409
Self check-in is not open right now: it is too early, or more than a day has passed since the session ended. On a repeating session the window belongs to the date the learner booked, not to the first date of the series.
Show the learner when the window opens. Past the window an admin can still mark the roster. The door closes for self-service, never for the truth.
not_registered409
The learner is checking in to a live session they never took a seat on.
Offer to register them first, then check in. Attendance without a seat is a roster question, not a check-in question.
422: the request is fine, the rules say no
Nothing about the request is wrong. A domain rule refuses it, and the caller can usually do something concrete about that.
gate_blocked422
A progression rule refused the completion: a required quiz not yet passed, minimum time on the lesson not elapsed, a video not watched, or a SCORM lesson that only its own package may complete.
Show detail to the learner. It states the condition, and a retry only succeeds once that condition is met.
username_required422
Community only. The action publishes a name and the member has not chosen one yet.
Send the member through the pick-a-username step, then retry the original action.
429 and 5xx: retry these
The only codes worth an automatic retry. The typed client marks exactly these as retryable.
rate_limited429
The caller exceeded the request budget for the workspace and key (admin plane) or the workspace and member (learner plane).
Wait for the Retry-After header, then retry with exponential backoff. X-RateLimit-* headers on every response show the remaining budget.
not_configured503
The endpoint depends on something that is not set up or not reachable right now: payments not connected for the store, the API disabled in this environment, or a CRM lookup that could not be completed.
If the workspace is missing the setup, finish it in the admin. Otherwise treat it as transient and retry with backoff.
internal_error500
Something failed on our side. The request may or may not have taken effect.
Retry with backoff, using the same Idempotency-Key if the endpoint takes one. If it persists, send us request_id.
Questions? Write to hello@peerfold.com and a person will answer.
Contact us