Aller au contenu

Responses and errors

The envelope of every response, the errors array that always comes back, the HTTP status codes and what to do with each, validation and business messages, payout and receiving refusals, refund refusals, API key errors (with the stable code), the language of messages and deleted items.

Cet article n’a pas encore été traduit. Vous lisez la version en anglais.

Every API response comes in the same envelope. That lets a single handler serve every call.

Success

{
  "success": true,
  "message": "Login successful.",
  "data": { },
  "pagination": { "page": 1, "limit": 20, "total": 137, "totalPages": 7 }
}

pagination only appears in lists. See Pagination.

Error

{
  "success": false,
  "message": "Valid email is required",
  "errors": [{ "param": "email", "message": "Valid email is required" }]
}
  • The errors array always comes back, even with a single error.
  • param says which field caused the error, when there is one.
  • message is the message to show.
  • code, when present, is a stable identifier to handle in code (like the API key errors); the message changes with the language, the code doesn't.

This way, validation and business rules are handled the same way: show errors[].message next to the param field; without a param, show it at the top.

The two kinds of error

KindExampleHow to recognize it
ValidationA field missing or in the wrong format400, param = the field, a short message ("Title is required")
Business ruleAn action the current state doesn't allow400, 403, 404 or 409, a message that explains ("Insufficient balance…")

Status codes

CodeMeansWhat to do
200 / 201It worked (201 when something is created)—
204It worked, no body (deletions)—
400Validation failed, or X-CommunityId is missing (errors[0].param = "X-CommunityId")Read errors and fix it
401Token missing, invalid or expired; token from another community; token issued before a password change; API key invalid, revoked, expired or from another communitySign in again, in the X-CommunityId community; with a key, check the key in Integrations & APIs
402The payment was declinedShow the message to the buyer
403No permission: insufficient role, the space restricts who can create, or the API key doesn't reach the actionCheck the role, the X-CommunityId and the code
404Doesn't exist, was deleted, belongs to another community, or the path is one of the old ones with /communities/{communityId}/Check the id, the community and the path
409Conflict with the current stateRead the message
422The payment provider declined the operation (e.g. a refund), with the reason in the message; or, with errors[0].param = "altcha", the security check for sign-in, sign-up or password recovery was missing, expired, already used or lighter than requiredShow the reason; for altcha, ask for a new challenge and resend
429Too many attempts (e.g. 10 invalid coupon codes in 15 minutes, more than 120 calls per minute with an API key, or too many sign-in, sign-up, password recovery and challenge attempts)Wait; with an API key and on the /api/auth routes, the Retry-After header says how many seconds
500Internal errorTry again; if it persists, write to support
502A service the operation depends on didn't answer (e.g. the payout account balance, on a payout). Nothing was recordedTry again in a few minutes
503The security check is unavailable right now (param = "altcha")Try again in a moment

Real examples

SituationResponse
Listing products without the X-CommunityId header400 · Send the community in the X-CommunityId header., with param: "X-CommunityId"
Signing in without the altcha field422 · Wait for the security check to finish and try again., with param: "altcha"
Creating a space in a section that doesn't exist400 · Seção não encontrada ou não pertence a esta comunidade. (section not found or not in this community)
Creating a course module in a course that doesn't exist404 · Curso não encontrado. (course not found)
Creating a video lesson with an address that is not YouTube, Vimeo or Loom400 · Videos can only come from YouTube, Vimeo or Loom. For another provider, use a link lesson, with param: contentUrl
Creating a second course in a space in A single course mode409 · This space holds a single course and already has one. Edit the existing course or switch the space to "Course list"., with param: "spaceId"
Switching a space with 2 or more courses to A single course409 · the message says how many courses there are and asks you to delete them until one is left, with param: "moduleConfig.mode"
Creating a course with an address (slug) that already exists400 · translated message, with param: "slug"
Turning on the course certificate with a template from another community404 · translated message, with param: "certificate.templateId"
Turning on the course certificate with an archived or deleted template400 · translated message, with param: "certificate.templateId"
Certificate text with HTML or over 5,000 characters400 · translated message, with param: "certificate.text"
Enrolling again in a course you are already in200 · the enrollment that already exists, with no extra points
A member posting in a "Staff" space403 · Only staff members (admin, owner, moderator) can perform this action
Opening a padlocked space (GET /api/spaces/{id} and the space's routes)403, with data: { reason, actions, lockReason, space, … }. When actions has buy or seePlans, it also carries data.offers: { plans, addOns, products }, in the same shapes as the Store (GET /api/storefront/spaces/{spaceId}/products), only with what opens the space and checkout sells to the caller; never for a group-only space. See Visibility and access
An admin changing an owner403 · Only an owner or a SuperAdmin can remove, demote or change an owner.
Taking an add-on with subscribers off a plan409 · This add-on cannot be taken off the plan…
Creating a billing option (POST /api/products, SUBSCRIPTION) without a plan400 · Every billing option must belong to a plan. Choose the plan., with param: "subscriptionGroupIds"
Deleting a plan that still has billing options400 · The plan cannot be deleted: it still has these billing options: Growth Monthly, Growth Annual. Move or delete them first., with param: "productIds"
Buying an add-on as the main product (POST /api/checkout)400 · This add-on can only be bought through its plan: together with the plan subscription or added to your current subscription., with param: "productId"
Buying a billing option that isn't in any plan400 · This billing option is not in any plan and is not for sale., with param: "productId"

Errors for product kinds, free products, seats, Downloads and hosted media:

SituationResponse
Creating a product with kind event, course or download (POST /api/products)400 · Course and event products are created together with the course or the event. (for download: Download products are created together with the download item, not through the products route.), with param: "kind"
Changing a product's kind400 · The product kind cannot be changed after it is created.
A physical product without hasStock and stockQuantity400 · A physical product needs stock control with a quantity (zero is allowed)., with param: "hasStock"
Installments (INSTALLMENT) with a zero price400 · An installment product needs a price above zero.
Deleting the product of a course, event or download409 · This product belongs to a course or an event. Delete the course or the event instead; the product is archived with it.
Deleting a course, event or item with a paid purchase that still gives access409 · There are N paid purchase(s) with active access. Refund them or wait for the access to end before deleting.
Enrolling in or attending a paid course or event without having bought it403 · This course is paid. Buy it, or subscribe to a plan that includes it, to enroll. (or This event is paid…), with data: { productId, price, currency }
Enrolling, attending or buying with the seats sold out409 · Sold out, with data.soldOut: true; join the line with POST /api/products/{id}/waitlist
Joining the waitlist while there are seats409 · There are places available, so there is no need to join the waitlist.
POST /api/checkout for a physical product without an address400 · The delivery address is required for physical products., with param in shippingAddress
POST /api/checkout without paymentMethod for a priced product400 · paymentMethod is required, with param: "paymentMethod"
Reprocessing a video that didn't fail (POST /api/media/{id}/reprocess)409; more than 20 requests per hour, per person: 429
Claiming a free product many times in a short time429 · Too many attempts in a short time. Try again in a few minutes.
Refunding a free purchase409 · A free claim has nothing to refund.
Downloading a Downloads file without access (paid item, not bought)403 · You do not have access to this download yet., with the price and the product; sold out: 409
A second item or second file in a Downloads space in single mode409
Downloads or media file: type outside the list, content that doesn't match the type, size that doesn't match or over the limit400 · translated message (The file content does not match the declared type., The file is over the N MB limit.)
Image, video or document refused on any upload route (/api/upload/…): file type not accepted, unexpected field, too many files or a failed upload400 · message in the sender's language, with the field in errors[].param (This file type isn't accepted. Send a JPEG, PNG, GIF, WebP or ICO image.)
Linking media from another space, for another purpose or already in use400 or 409 · translated message
Asking for a media link without being able to open the space or the item (a private or unpublished post only opens for the author and the team; an event's recording follows the event's visibility)403 · You do not have access to this media.
More than 60 media upload or confirmation requests (POST /api/media/upload-url and /api/media/confirm) in one hour, per person429 · Too many requests. Try again in a few minutes.

Going over the plan's storage refuses only new uploads: asking for the upload URL returns 409 storageLimitReached, in media, Downloads and lessons; what's already stored stays. The month's upload volume only warns. See Plan and extensions.

hosted media. A file type outside the list, a size over the limit (500 MB or 50 MB), a size that does not match, content that does not match the format, or a lesson that is neither video nor file: 400; the lesson's file changed during the upload: 409; reading the media without being enrolled, with the lesson locked or the course unpublished: 403, with no URL; a lesson with no hosted file: 404; a member, moderator or finance trying to upload or remove: 403; storage unavailable: 503. Going over the storage limit is never an error: the upload continues and the response carries a warning.

quiz.maxAttempts (1 to 100; empty = unlimited) when creating and editing the quiz lesson, and certificateMinGrade (0 to 100) on the course. The attempt returns maxAttempts, attemptsUsed, attemptsLeft, bestScorePercent and closedWithoutPassing; an attempt after they run out: 409. The course returns userFinalGrade to the student and finalGrade in the student list; below the minimum grade, userCertificate.status is BELOW_MIN_GRADE, with finalGrade and minGrade.

quiz and campaigns. POST /api/courses/lesson and PUT /api/courses/lesson/{lessonId} accept the quiz block (passPercent 1 to 100, 1 to 30 questions, 2 to 6 options with exactly one isCorrect, optional explanation), owner and admin only; a format error: 400, with errors[].param on the field (for example quiz.questions[0].options); changing the type of a quiz with attempts: 409. POST /api/courses/{courseId}/lesson/{lessonId}/quiz/attempts sends the answers (all of them) and returns the score, passed and, only when passed, the correct answers; more than 30 attempts per hour on the lesson: 429, with Retry-After; GET on the same route lists the last 10. POST .../progress with completed: true on a quiz lesson: 409. Campaigns: POST /api/campaigns/{id}/schedule (scheduleKind DAILY or WEEKLY, timeOfDay, weekdays; a past or invalid time: 422 on sendAt), POST .../pause, .../resume and .../cancel (owner and admin only), GET .../runs, GET .../report and GET .../preview (team), GET /api/campaigns/automatic and PUT /api/campaigns/automatic/{systemKey} (turning on requires consent), POST /api/upload/campaigns/image and DELETE (the image of a campaign that left draft: 409). An audience over 500 people on the community's first send: 422; a fixed item in a block that not everyone sees: 422 on blocks[i]; sending, scheduling, editing or deleting an automatic through the ordinary routes: 409.

course community. POST /api/courses/{courseId}/leave on a completed course: 409; without being enrolled: 404. Course or lesson discussion without being enrolled, or writing without being allowed: 403; writing or reacting in a draft or archived course: 409; replying to a reply, empty text or over 2,000 characters, an invalid reaction: 400, with param on the field; a comment or lesson that does not exist (or belongs to another course): 404. Deleting someone else's comment without being on the team: 403. GET /api/courses/{courseId}/leaderboard with the leaderboard off: 403. An instructor whose profile is not owner, admin, moderator or finance: 400; learningPoints with more than 8 items or an item over 140 characters: 400; a member sending instructorMemberId, learningPoints or leaderboardEnabled: 403.

e-mail campaigns. POST /api/campaigns/{id}/send without consent: true (param: "consent"), an empty subject or content, an invalid block, an empty audience or one over 20,000 people, or an unsubscribed person chosen by hand (param: "audience.memberIds"): 422; a moderator or finance trying to send: 403; a campaign already sent: 409; daily, weekly or monthly quota exhausted: 429, saying the limit and when it frees up, before any e-mail goes out. Editing or deleting a campaign already sent: 409. A campaign from another community: 404.

locked lesson. POST /api/courses/{courseId}/lesson/{lessonId}/progress on a lesson the sequence or a date has not opened yet: 409 · Finish "…" before this lesson. or This module opens on …, with param: "lessonId". The lesson gets no progress or points. A requireSequential that is not true or false: 400; both release fields together, or an invalid date or days: 400, with the message on the field; a member or moderator sending these fields: 403.

events: RSVP and times. POST /api/events/{id}/participate with going or maybe after the end: 409 · This event has already ended. You can no longer confirm attendance., with param: "status" (not_going still works). POST and PUT /api/events with an end equal to or before the start: 400 · End time must be after start time, with param: "endTime". Turning on the event certificate without endTime: 400 · Set an end time to issue a certificate…, with param: "certificate.enabled". Template from another community: 404; archived or deleted template: 400, with param: "certificate.templateId"; text with HTML or too long: 400, with param: "certificate.text". A moderator or member sending the certificate block: 403.

events: draft and publishing. PATCH /api/events/{id} with an updatedAt (or If-Unmodified-Since) older than the last save: 409, with param: "updatedAt" and the current data.updatedAt; nothing changes. POST /api/events/{id}/publish with something missing: 422, one errors[] item per field (title, startTime, endTime, location for in-person or hybrid, ticketTypes) and data.readiness; an event already published: 409. RSVP, ticket purchase, registration, waitlist or streaming on a draft: 409 for the team and 404 for everyone else.

events: who reads. GET /api/events/{identifier} without X-CommunityId: 400. A Members event signed out: 401; signed in without being in the community: 403. An event that does not exist, belongs to another community, or is a draft, private, awaiting moderation or rejected: 404. A plan event or one sold through a paid product, for someone without access, is not an error: 200, with requiresPlan or requiresPurchase set to true and the livestream, recording and address set to null.

workflows: role of whoever saves. Creating, editing, turning on or resuming a workflow with an action above the role of whoever does it (a moderator never saves workflows; an admin acts like the owner, except for an owner-only action, and none exists today; or a called workflow that does something above their role): 422 · workflows.graph.actionAboveRole or subWorkflowAboveRole, on the block. Running one of these now or calling its webhook address: 403 workflows.actionsAboveRole. Deleting a run still under way from the history: 409 · This run is still under way. Stop it before deleting. POST /api/workflows/{id}/simulate with sendEmailsToMe: true: more than 10 in an hour, 429; an account without e-mail, 422 emails.noEmail. PUT /api/vault/ai-limits out of bounds, or with per call above per run or per run above per day: 422.

SMS and WhatsApp. PUT /api/sms/settings or /api/whatsapp/settings turning on without a daily cap: 422, with param: "dailyCap"; outside 1 to 50,000: 422. Turning on a channel the plan doesn't have: 409 · SMS is not available on this community's plan. (on WhatsApp below Business, 403 whatsapp.notOnPlan). personDailyCap outside 1 to 20: 422. POST /api/members/me/phone-verification with SMS off in the community: 409; a phone from a blocked or unknown country (today, the United States on SMS): 422; too many codes in the community on the same day: 429 · This community reached today's code limit. Try again tomorrow.; another code within 1 minute: 429 with Retry-After and data.retryAfterSeconds, and more than 3 an hour or 6 a day for the same phone, 6 a day per account or 10 an hour per IP: 429. .../confirm with the wrong code: 400, with the tries left; expired or with the 5 tries used: 400, ask for another. PUT /api/members/me/notice-preferences turning SMS or WhatsApp on without a verified phone: 409. The tests (POST /api/sms/test, /api/whatsapp/test) never fail on a sending rule: they answer 202 with skipped and the reason (smsPhoneUnverified, whatsappTemplateNotApproved…).

Plan quotas. A campaign whose audience goes over the recipients left in the period: 429 quota_recipients. Reprocessing a video without conversion minutes: 409 transcodeQuotaReached (or transcodeNotOnPlan on Starter). See Plan and extensions.

More examples, in the order you run into them while building something, in Creation order.

Version conflict

The editors check the version before saving, so one save never wipes out another without warning. Send the version you read in one of these ways:

  • updatedAt in the body, with the value the GET returned (on the community row, settingsUpdatedAt);
  • or the If-Unmodified-Since header.

If another save came first, the answer is 409, nothing changes, and data carries the current version:

{
  "success": false,
  "message": "Someone else saved this after you opened it. Reload to see the current version and make your change again.",
  "errors": [{ "param": "updatedAt", "message": "…" }],
  "data": { "updatedAt": "2026-10-28T14:05:00.000Z" }
}

Read again and repeat the change, or send the save with the new version to write over it on purpose. Every success answer carries the new version. It is optional: without the field and the header, the save goes as before.

WhereVersion
Event (PUT, PATCH), campaign, library e-mail and each language version, workflow, course (with modules and lessons), form, survey, certificate templatethe item's updatedAt
Default e-mails (PATCH /api/email-overrides/{emailKey}; PUT and DELETE of /{language})updatedAt of the e-mail or of that language's version
Community row: profile, brand, images, directory, workflow senders, custom domainsettingsUpdatedAt
Chat, notifications, SMS and WhatsApp (one version for both), credits, business information, payout settingsthe setting's updatedAt (empty before the first save: no check)

Whoever cannot edit the item gets the usual 403 or 404, never the version. To know right away that someone saved, listen to item.saved in Realtime.

Chat limits

The chat (Messages) has anti-spam limits per person and per community, for the app, the API and the MCP alike. Going over a limit answers 429 (ChatRateLimited in the reference), with the translated message saying when it frees, in the community's time zone:

{
  "success": false,
  "message": "…",
  "errors": [{ "param": "limit", "message": "…", "code": "limitNewConversationsHourly", "limit": 10, "retryAt": "2026-10-22T18:42:10.000Z" }]
}
errors[0].codeLimit
limitConnectionsDaily20 connection requests a day
limitConnectionsWeekly100 connection requests a week
limitPendingRequests200 requests waiting for an answer; frees by canceling requests (no retryAt)
limitNewConversationsHourly10 new conversations an hour (existing ones don't count)
limitNewConversationsDaily30 new conversations a day
limitMessagesPerMinute30 messages a minute
limitDuplicateMessageThe same text to at most 5 different people an hour

limit is the maximum that applies to the caller: half of the request and new-conversation limits for someone who joined less than 24 hours ago, and double every limit for the team (owner, admin, moderator and finance). When time frees it, retryAt (ISO 8601) and the Retry-After header, in seconds, come along. The windows slide: a slot frees when the oldest action in the window completes the period.

Person name

Each person has one name (see Account and profile › Name). In the API:

  • POST /api/members/invite accepts people: [{ email, name }] besides emails (one of the two is enough): the new account is created with the name, and the invitation email goes out with it.
  • PUT /api/members/{id} accepts name (the older displayName still works) and PUT /api/members/me accepts only name. The name changes only in this community; on someone else's, the display name changes, and the full name only in the provisional-name case.
  • The responses of GET and PUT /api/members/{id}, GET /api/members and PUT /api/members/me include name, the name that's shown.
  • Sending the same name that's already shown changes nothing and skips the rules below.
SituationResponse
Changing the name of someone with a role equal to or above yours403 · You can only change the name of someone whose role is below yours., with param: "name"
Changing the name of a member from another community (the X-CommunityId isn't their community)404 · Profile not found, the same as an id that doesn't exist
Empty name400, with param: "name"

Stock

A one-off product with hasStock has stockQuantity, the number available right now.

SituationResponse
Creating or editing a product with hasStock: true and no stockQuantity400 · Enter the stock quantity., with param: "stockQuantity"
Turning hasStock on for a subscription (SUBSCRIPTION)400 · Stock only applies to a one-off product, not to a subscription., with param: "hasStock"
Buying with no unit left (POST /api/checkout or POST /api/checkout/calculate-price)409 · Sold out, with param: "productId". Nothing is recorded or sent to the payment provider
Two buyers on the last unitOne gets 201; the other, 409
Buying again with an open PIX or boleto for the product409 · You already have an open payment for this plan. Pay the same PIX or boleto, or cancel it to choose another payment method., with param: "productId" and the open payment in data.openPayment. No other unit is reserved

The unit is reserved on the order and comes back only once (payment declined, PIX or boleto expired, a refund that revokes access). The direct-link catalog, GET /api/checkout/products, returns soldOut (true or false, without the quantity), like the Store. The management routes (GET /api/products and GET /api/products/{idOrSlug}) return stockQuantity to the owner, the admin and finance; other roles get only the active products, with soldOut.

Appearance and themes

The community's appearance is written only through themes (/api/community-themes, see Theme and colors).

SituationResponse
PATCH /api/communities/appearance with mode or navigation400, with param on the field: the mode and the menu belong to the theme (use PATCH /api/community-themes/active or activate another theme). darkLogoUrl stays here
PUT /api/communities/{id} with themePalette or themeColor (even null)400, with param: "themePalette" or "themeColor": the colors belong to the theme (values.light in /api/community-themes). On community creation (POST), they are still accepted and become the colors of the initial Light theme
Saving the 11th theme422 · The community already has 10 saved themes. Delete one to save another.
Deleting the active theme422 · The active theme cannot be deleted. Activate another theme first.
Font, corners, density or menu outside the list, or a color that isn't hexadecimal422, with param on the field (for example values.font.heading)
Saving or activating a dark or automatic theme without the dark theme enabled422, with param: "values.mode"
POST /api/communities/appearance/theme-from-image with SVG, a file that isn't an image or one over 5 MB422 · SVG isn't accepted here. Upload a PNG, JPG or WebP. (or the message for the case)
The same route with source: "logo" and a community without a logo422 · The community has no logo yet. Upload an image.
The same route with the plan's AI quota used up429, with the same message as the other AI features
Moderator, finance or member on any theme route403

Payouts and receiving payments

SituationResponse
Checkout (POST /api/checkout) or price calculation in a community without an active payout account400 · This community cannot receive payments yet: its payout account is not active., with param: "productId". Nothing is charged
Request a payout (POST /api/payouts) without an active payout account400 · the same message
Request a payout above the maximum400 · Not enough balance for this payout. You can withdraw up to US$ 997.26 now: a payout of US$ 1,000.00 plus the US$ 2.75 payout fee comes to US$ 1,002.75, and the balance available for payouts is US$ 1,000.00., with param: "amount"
Request a payout below the minimum400 · The minimum payout is US$ 10.00., with param: "amount"
Request a payout with more than 2 decimal places (1.005)400 · Enter the payout amount with at most 2 decimal places., with param: "amount"; the amount is never rounded
Publish a product (POST /api/products/{id}/publish, or PUT with status: ACTIVE) with an approved country that has no payment provider400 · We don't sell in PT yet: the business information's country has no active payment provider., with no param. The product stays a draft
Request a payout when the payout account balance can't be checked502 · The balance available for payouts cannot be checked right now. Please try again in a few minutes. Nothing is recorded
Payout sent, but refused by the bank201, with data.status: "FAILED", data.failureCode and data.failureMessage (The bank did not accept the transfer. The amount and the fee are back in your balance.)
Request a payout above the maximum with refunds waiting for balance400 · the same refusal, plus US$ 92.01 is kept for refunds waiting for balance.
Cancel a payout that isn't pending400 · Cannot cancel payout with status PROCESSING

The maximum payout is the smaller of the statement's available amount and the payout account balance, with the amount plus the fee fitting in it. To avoid a refusal, read GET /api/payouts/balance first: withdrawableBalance is the balance you can pay out, transferFee the fee and maxPayoutAmount the largest amount you can request. Refunds waiting for balance also come out of what can be paid out: pendingRefunds says how much is reserved for them. An accepted request answers 201 with the payout in the state it ended in: PROCESSING when it was sent to the bank, FAILED when it was refused right away. See Payouts. minPayoutAmount is the smallest amount accepted. Only available counts towards what can be paid out: pending (sales before their release date) and retained (the chargeback reserve) never do. With a balance in more than one currency, balances[] has one line per currency, and ?currency= picks the currency of the answer.

Memberfy team routes. Two SuperAdmin routes follow payouts and, like every /api/admin route, stay out of the public reference: GET /api/admin/payouts lists the payouts of every community (filters communityId, status, from and to), and POST /api/admin/payouts/{payoutId}/retry retries a failed payout as a new payout, linked to the previous one, only once. Retrying a payout that didn't fail, or that was already retried, answers 409. For the community, the new payout shows up in GET /api/payouts like any other.

Refund

Three routes, all through the X-CommunityId community, for owner, admin and finance (moderator and member: 403; a purchase from another community: 404):

RouteWhat it does
GET /api/manage/purchasesThe community's sales, newest first. Filters situation (one or more, comma separated; the total and the pages count only what matches), status, productId, paymentMethod, refunded=true, from, to, search (buyer name or email), page and limit
GET /api/manage/purchases/{purchaseId}One sale, with refunds (all refunds, newest first) and the timeline, the sale's history
POST /api/manage/purchases/{purchaseId}/refundsRefunds the purchase and emails the buyer

Besides the amount, the platform fee and the net (netAmount), each sale carries refundedAmount (what has already been refunded or is on its way), refundableAmount (what still can be), refundMethod (PROVIDER: Memberfy gives it back through the card or PIX; SETTLED_OUTSIDE: boleto, the community returns it directly), lastRefund and refundBlockedReason, with the translated sentence in refundBlockedMessage, when the purchase can't be refunded through Memberfy yet:

refundBlockedReasonMeaning
NO_CHARGEThe purchase has no charge at the payment provider
nullNothing prevents it

Read refundBlockedReason before offering the refund: the POST refuses these purchases with the same message. On card and PIX, a refund can be partial, from 0.01 up to refundableAmount (partialRefundAvailable is always true), and in an order with several purchases (a plan and its add-ons) each purchase is refunded on its own. A partial one usually comes back as PENDING until the provider confirms it.

Status and timeline. Every sale carries situation, the same word as the Status column and filter of the Sales tab: PAID, PROCESSING, AWAITING_BALANCE, REFUNDED, PARTIALLY_REFUNDED, SETTLED_OUTSIDE, IN_REVIEW, DECLINED, DISPUTED, AWAITING_PAYMENT and FAILED. The detail also carries timeline, the sale's timeline, oldest first. Each event has type, at (the date; future: true on a date that hasn't come yet, such as a release), actor (BUYER, STAFF with name and role, MEMBERFY, PROVIDER or SYSTEM, with label already translated), amount, currency, params and text, the event already translated by Accept-Language. The type values:

Grouptype
Purchase and paymentPURCHASE_CREATED, PAYMENT_CONFIRMED, PAYMENT_CONFIRMED_INSTALLMENTS, PAYMENT_FAILED, PAYMENT_CANCELED
Balance releaseBALANCE_RELEASE_SCHEDULED, BALANCE_RELEASED, BALANCE_RELEASE_SCHEDULED_INSTALLMENT, BALANCE_RELEASED_INSTALLMENT
Chargeback reserveRESERVE_HELD, RESERVE_RETURN_SCHEDULED, RESERVE_RETURNED, RESERVE_RETURNED_REFUND, RESERVE_RETURNED_CHARGEBACK
RefundREFUND_REQUESTED, REFUND_REQUESTED_REASON, REFUND_SETTLED_OUTSIDE, REFUND_SETTLED_OUTSIDE_NOTE, REFUND_PROCESSING, REFUND_AWAITING_BALANCE, REFUND_IN_REVIEW, REFUND_SUCCEEDED, REFUND_DECLINED, REFUND_AT_PROVIDER, REFUND_AT_PROVIDER_IN_REVIEW, REFUND_REVIEWED_SUCCEEDED, REFUND_REVIEWED_DECLINED
Access and subscriptionACCESS_KEPT, ACCESS_REVOKED, SUBSCRIPTION_CANCELED
ChargebackCHARGEBACK_OPENED, CHARGEBACK_WON, CHARGEBACK_LOST
E-mails to the buyerEMAIL_SENT, EMAIL_DELIVERED, EMAIL_NOT_DELIVERED

The timeline never carries provider cost or provider ids.

The POST body: access (KEEP keeps access; REVOKE removes access and, on a subscription, cancels the subscription right away), idempotencyKey (required: generate one when opening the dialog and resend the same one on retries), amount (default: what is left to refund), reason and, on boleto only, settledOutside (at and note), which records the money returned directly by the community.

The response is the refund (PurchaseRefund): 201 when it is new, 200 on a repeat with the same key, never a second refund. status says where it stands: SUCCEEDED (done), PENDING (the provider still has to confirm), AWAITING_BALANCE (no balance in the receiving account for the community's part: it goes out on its own, in order of arrival, and the access choice already applies), DECLINED (declined, with failureReason) or NEEDS_REVIEW (under review: Memberfy reviews it and completes or declines it). It also carries accessDecision, source (MEMBERFY, or PROVIDER when it was made outside Memberfy), communityAmount (what left the community's balance) and platformFeeReturned (the platform fee returned).

SituationResponse
A purchase that doesn't exist or belongs to another community404 · Purchase not found in this community.
Zero or negative amount400 · Enter an amount greater than zero.
Amount above what is left400 · The amount is above what is left to refund (97.00).
Boleto without settledOutside400 · Memberfy cannot give back a paid boleto. Contact the buyer, return the money directly (PIX or bank transfer) and record it here as "already returned directly to the buyer".
settledOutside on card or PIX400 · Only a boleto can be recorded as returned directly. Card and PIX refunds are always made by Memberfy.
Pending or disputed purchase409 · This purchase cannot be refunded (status: …).
Nothing to refund409 · This purchase has nothing left to refund.
Purchase without a charge at the provider (NO_CHARGE)409 · This purchase has no charge at the payment provider to refund.
Refunds turned off in the environment (outside production)409 · Refunds are off in this environment.
idempotencyKey already used on another purchase409 · This refund key was already used for another purchase. Open the refund dialog again.
The provider declined422 · The payment provider declined the refund: …, with the reason. The refund stays DECLINED and nothing changes in the statement; a new request takes a new key

On card and PIX, the statement debits only the community's part (communityAmount), and the platform fee goes back in proportion. On a boleto returned directly, communityAmount and platformFeeReturned are zero. A purchase refunded with KEEP stays COMPLETED and active; the refund shows in refundedAmount and lastRefund. For the buyer, GET /api/purchases/me returns refundedAmount, refundedAt and refunds; on a subscription, each charge in GET /api/manage/subscriptions/{subscriptionId} carries the purchaseId the refund needs. See Refunds and disputes.

Memberfy team route. GET /api/admin/refunds (SuperAdmin only, out of the public reference) lists the refunds of every community; ?status=NEEDS_REVIEW are the ones under review, which Memberfy completes or declines.

Plan change

POST /api/subscriptions/{id}/change-tier charges the card, so it accepts an idempotency key: idempotencyKey in the body or the Idempotency-Key header, 8 to 100 characters (A-Z, a-z, 0-9, _, ., : and -). Generate one per attempt by the person (for example, when they confirm a combination of plan and add-ons) and resend the same one on retries, such as after a timeout.

SituationResponse
First time200 with the result of the change (mode, subscription, scheduledChange, charge)
The same key again200 with data.replayed = true and the first result; nothing is charged or changed again
A change charge for the same subscription still in progress (with any key, or no key)409 · Your plan change is already being processed. Wait a moment and check your subscription.
The subscription switched options in the middle of the change (a renewal applying another change)409; if the change had already been charged, the amount is refunded in full and nothing changes
Card declined402; the key is released, and a new attempt (with another card) can use another one

Two changes at the same time on the same subscription (two tabs, the app and the assistant) never charge twice: one goes through, the other gets the 409. The same rule applies to the preview and to the change with addOnProductIds (the add-ons in the same charge) and with cardToken (a new card, which becomes the subscription's card when the charge goes through). The route does not accept an API key. See Change plan.

PIX and boleto

POST /api/subscriptions/{id}/change-tier takes paymentMethod: CREDIT_CARD (the default), PIX or BOLETO, one of the preview's allowedMethods.

FieldWhereWhat it is
allowedMethodsPreview (/change-tier/quote)The payment methods for the charge now, the ones the target option accepts at checkout: [{ method, totalAmount, cashAmount, installmentOptions? }], with installments only by card. The up-front total is the same for all (the platform fee doesn't depend on the payment method). Boleto only with more than 4 days to the renewal. With no charge now (scheduled change or below the minimum), []
pendingChangePreview and changeThe change waiting for its PIX or boleto: purchaseId, orderId, paymentMethod, newProductId, addOnProductIds, amount, currency, pixCode (copy and paste), qrCode, boletoUrl, boletoBarcode, expiresAt, createdAt; or null
mode: "PENDING_PAYMENT"ChangeWith PIX or boleto, the answer is 200 with mode = PENDING_PAYMENT, charge (the order) and pendingChange (what to pay). The plan doesn't change yet: the change and the add-ons apply once the payment is confirmed

The amount is the prorated one calculated when the order is created and doesn't change until it is paid. On confirmation, if the subscription is no longer on the option and period the order was priced for, or the target or an add-on is no longer valid, the payment is refunded and nothing changes. Expired or canceled order: nothing changes and the subscription is free again; a PIX paid after it expired is refunded. The same idempotencyKey while the order waits returns the same payment data (replayed = true, mode = PENDING_PAYMENT).

SituationResponse
A change is already waiting for its PIX or boleto (with any key)409 · monetization.subscription.changeTierPendingPayment · A plan change is waiting for its payment. Pay it or wait for it to expire before starting another one. The preview carries pendingChange with what to pay
BOLETO with 4 days or less to the renewal400 · param = paymentMethod · There is not enough time to pay a boleto before the renewal. Choose PIX or card.
paymentMethod not in the list, not in allowedMethods, or PIX/boleto together with cardToken or paymentMethodId400 · param = paymentMethod · Invalid payment method

Undoing is DELETE

Taking back a reaction or removing an event recording uses the DELETE method, on the same path used to add it. POST only adds or changes: sending the same reaction again no longer takes it back.

Add or changeTake back
POST /api/status/{id}/reactDELETE /api/status/{id}/react
POST /api/content/{id}/reactDELETE /api/content/{id}/react
POST /api/feed/{type}/{id}/reactDELETE /api/feed/{type}/{id}/react
POST /api/status/{id}/comment/{commentId}/react and /api/content/{id}/comment/{commentId}/reactDELETE on the same paths
POST /api/gallery/images/{imageId}/reactDELETE /api/gallery/images/{imageId}/react
POST /api/courses/{courseId}/react (lesson: ?lessonId=) and /api/courses/{courseId}/comments/{commentId}/reactDELETE on the same paths
PUT /api/events/{id}/recording (sets the recording)DELETE /api/events/{id}/recording. mediaId: null on the PUT answers 400

Clearing a field inside an edit (the event banner or highlight, for example) is still a PUT with the field empty, and state changes (cancel, archive, decline, pin to the feed, leave a course) keep their own routes.

API key errors

With the X-Api-Key header (see Authentication › API key), the key's refusals carry a code in errors[0]:

codeStatusWhenWhat to do
API_KEY_SCOPE_MISSING403The route requires sign-in and the key doesn't have the scope. errors[0] carries module, level and scope ("feed:write")Create another key with the scope (scopes can't be edited)
API_KEY_BLOCKED403The action doesn't accept a key, with any scope (money, account, SuperAdmin, OAuth and MCP, key management, X-MemberId)Do it on screen, signed in
API_KEY_ROUTE_NOT_COVERED403The route requires sign-in and belongs to no moduleUse the sign-in token
API_KEY_IP_NOT_ALLOWED403The caller's IP isn't in the key's list; the message carries the IPAdd the IP to the key, in Integrations & APIs
API_KEY_RATE_LIMITED429More than 120 calls per minute from the same person in this communityWait the Retry-After seconds
API_KEY_WITH_AUTHORIZATION400X-Api-Key and Authorization on the same call (param: "Authorization")Send only one of the two
API_KEY_PATH_INVALID400The path has a . or .. segment, raw or encoded, or an encoded character that changes the route's module, like /api/communities/%70lans (param: "path")Call the route's path without . or .. and without encoding what is not a parameter

A key that is invalid, revoked, expired, from another community or belongs to someone who left the community gets 401, with no code, and the reason in the message ("This API key was revoked.").

{
  "success": false,
  "message": "This action does not accept an API key.",
  "errors": [{ "param": "X-Api-Key", "message": "This action does not accept an API key.", "code": "API_KEY_BLOCKED" }]
}

Language of messages

Messages come in the language of the Accept-Language header (pt-BR, en-US, es-ES, it-IT, de-DE and the other variants). Without a recognized language, they come in English. This also holds for authentication and role errors (401 and 403), which use the same envelope, with errors[].

Item from another community

An item that belongs to another community answers 404, the same as an id that doesn't exist. The API doesn't reveal that the item exists elsewhere: for the caller, it doesn't exist in the X-CommunityId community.

WhenResponse
The route names a space from another community (?spaceId=, the space of a course or a call for papers)404 · space.notFound (Space not found)
The route names an item from another community (reading a post, its comments, an event's participants)404 · common.notFound (Not found)
Editing, deleting, commenting on, reacting to or pinning content, an event or a status from another community404 · the module's message (content.notFound, event.notFound, status.notFound, course.notFound)
Reading, editing or deleting a member from another community, changing their role or picture (/api/members/{id}, /api/members/{id}/role, /api/upload/avatar/member with memberId)404 · profile.notFound (Profile not found)

Being an owner, admin or moderator in one community gives no access to anything in another, even when sending your own community's header. If the item is from the community and still answers 404, check that X-CommunityId is the right community.

Deleted items

The API doesn't truly delete: deleted items are marked as removed and stop showing up. That's why a deleted id answers 404, as if it never existed.

With the SDK

The SDK throws a MemberfyError with .status, .body (the whole envelope) and .errors. See JavaScript SDK.

AI assistant turned off

If the owner turned off the AI assistant for your role, OAuth authorization answers 403 with "The owner has turned off the AI assistant for your role in this community…", and /mcp refuses every call with "The owner of {community} has turned off the AI assistant for {group}. Talk to the community owner to turn access back on." Anyone who tries to change the switches without being the owner gets 403; a value that is not a boolean, 400. See Authentication.