Errors
Mixpost returns conventional HTTP status codes, and every error body is JSON with at least a
message key.
Error format
All errors share the same envelope:
{
"message": "Unauthenticated."
}
Validation errors (422) add an errors object keyed by field name:
{
"message": "The name field is required. (and 1 more error)",
"errors": {
"name": ["The name field is required."],
"hex_color": ["The hex code is not valid"]
}
}
The top-level message on a 422 is Laravel's summary of the first error, not a fixed string.
Match on the errors keys rather than on message.
Status codes
| Code | Meaning |
|---|---|
200 | Success. |
201 | Created. See which endpoints return 201. |
401 | The bearer token is missing, malformed, unknown or expired. |
403 | The token is valid, but the user may not perform this action. See Permissions. |
404 | The workspace or the record does not exist. |
422 | The request body failed validation. |
500 | Unhandled server error. |
Created responses
Most successful calls return 200. These five return 201:
POST /{workspace}/tagsPOST /panel/workspacesPOST /panel/workspaces/{workspace}/subscriptionPOST /panel/usersPOST /panel/receipts
If you check for an exact 200, these will look like failures. Treat any 2xx as success.
401 Unauthenticated
{
"message": "Unauthenticated."
}
Returned when Authorization: Bearer <token> is absent, the token does not match a stored token, or
the token's expiry has passed. Tokens are matched by hash, so a token that was revoked in the
dashboard fails the same way as one that never existed.
403 Access forbidden
{
"message": "Access forbidden."
}
The token authenticated successfully, but the user behind it is not permitted. This is returned when
the user is not a member of the workspace in the URL, when their workspace role is too low for the
route, or — on /panel endpoints — when the user is not a platform admin.
On Mixpost Enterprise, a workspace whose plan has the API feature turned off is also refused with a
403, with a message describing the disabled feature rather than the text above. This does not apply
to Mixpost Pro, where the API is not gated per plan.
404 Not found
An unknown workspace UUID returns:
{
"message": "Workspace not found."
}
A workspace segment that is not a UUID at all matches no route, so it returns 404
body instead, before authentication runs.
An unknown record on a workspace endpoint returns the model name:
{
"message": "Post not found."
}
The message follows the model — Post not found., Tag not found., Media not found.,
Account not found.
The /panel/* endpoints (Workspaces, Subscriptions, Users, Receipts) do not produce these
friendly messages. Mixpost Enterprise registers no exception handler for them, so a missing record
returns Laravel's default 404 body instead. Rely on the status code, not the message.
422 Validation failed
{
"message": "The name field is required.",
"errors": {
"name": ["The name field is required."]
}
}
Media endpoints have one extra 422 case worth handling: when a workspace has hit a plan limit, the
body carries an additional top-level limit object alongside errors.limit.
Publishing requirements
Before a post is committed to publishing, its content is checked against what the platform of each
of its accounts needs. The check runs when a post is scheduled,
added to the queue, approved, or
created with schedule, schedule_now or queue. It runs again when a
scheduled post is updated and stays scheduled, so the post cannot be
changed into one its platforms would refuse. A draft is never checked, so unfinished content can be
saved and completed later.
A scheduled post can still stop meeting a requirement without being updated through the API: it can
be edited in the dashboard, which saves every change as it is made, or lose a media file or a
Pinterest board it relies on. Each account is checked once more right before it is published. An
account that fails the check is not sent to its platform — a thread is never published up to the post
at fault — and it is marked failed, with the requirement's message in its errors, exactly as a
platform's own rejection would be.
Every unmet requirement is its own entry in errors:
{
"message": "Instagram → Add a photo or a video. (and 1 more error)",
"errors": {
"requirements.0.instagram.post_0": ["Instagram → Add a photo or a video."],
"requirements.12.pinterest.board": ["Pinterest (Home Decor) → Choose a board."]
}
}
A key reads requirements.<scope>.<provider>.<rule>:
<scope>is0when the problem is in the original content, shared by every account without a version of its own. It is an account ID when the problem is in that account's own version, or in an option kept per account (a Pinterest board, a TikTok privacy level).<provider>is the platform, such asinstagramorpinterest.<rule>names the requirement:
| Rule | Platforms | Requirement |
|---|---|---|
post_<n> | All but Google Business Profile | Post n (from 0) has what the platform publishes: text or media on X, Threads, Bluesky, Mastodon, LinkedIn and Facebook; a photo or video on Instagram, Pinterest and Pixelfed; a video on YouTube and TikTok (photos too, when TikTok photo uploads are enabled); exactly one video for a reel; exactly one photo or video for a story. Threads are checked post by post, other platforms on their first post only. |
text_<n> | All | The text of post n fits the platform's limit, counted the way the platform counts it: on X links count 23 characters and emoji or wide characters 2; on Mastodon and Pixelfed links count 23 and a mention leaves out its server; on Bluesky links count as the shortened form Bluesky shows. On platforms with comments the first comment is held to the limit too. |
photos_<n>, videos_<n>, gifs_<n> | All | Post n carries no more photos, videos or GIFs than the platform accepts. |
hashtags_<n> | Instagram, Facebook | Post n has no more hashtags than the platform allows: 5 on Instagram, 30 on Facebook. Stories are not checked. |
media_items_<n> | Pixelfed | Post n carries no more media in total than the instance accepts. |
mixed_media_<n> | X, Bluesky, Mastodon, Facebook, LinkedIn, Pinterest, YouTube, TikTok, Google Business Profile | Post n does not mix photos, videos and GIFs. |
first_comment | Facebook, Instagram, LinkedIn | A first comment that is present has text. Stories publish no comment and skip this check. |
board | options.pinterest.boards.account-<id> holds a board for the account. | |
link | options.pinterest.link, when set, is an http or https URL. | |
title | YouTube, Google Business Profile | YouTube: options.youtube.title is set and at most 100 characters. Google Business Profile offers and events: options.gbp.event_title is set. |
dates | Google Business Profile | Offers and events have a start_date and an end_date, and the end is not before the start. |
button_link | Google Business Profile | A button other than NONE or CALL has a valid button_link. |
offer_link | Google Business Profile | An offer with offer_has_details has a valid offer_link, when one is set. |
privacy_level | TikTok (direct posting) | options.tiktok.privacy_level.account-<id> is set. |
content_disclosure | TikTok (direct posting) | When content_disclosure is on, brand_organic_toggle or brand_content_toggle is on as well. |
The messages are translated, so match on the keys rather than on the text.
500 Server error
{
"message": "Server Error"
}
When your instance runs with APP_DEBUG=true, this body also includes exception, file, line
and trace. Never run a production instance with debug enabled.
Rate limiting
Mixpost does not rate limit the API. No throttle is applied to any API route by the package itself.
However, Mixpost's API routes run inside your host Laravel application's api middleware group, and
that group is yours to configure. Laravel 8–10 skeletons include throttle:api (60 requests per
minute) in that group by default; Laravel 11 and newer do not, unless the application opts in via
->throttleApi().
So whether you ever receive a 429 — and at what threshold — depends entirely on the application
hosting Mixpost. If throttling is enabled there, responses carry the standard Retry-After and
X-RateLimit-* headers. Treat 429 as possible and back off on it, but don't assume a fixed budget.
Permissions
A token carries the full authority of the user who created it — there are no per-token scopes. What an endpoint allows is decided by that user's role in the workspace.
ADMIN and MEMBER have identical access across the API; no endpoint distinguishes them. The only
meaningful distinction is VIEWER:
| Endpoints | ADMIN | MEMBER | VIEWER |
|---|---|---|---|
GET accounts | yes | yes | yes |
GET posts, POST /posts/approve/{post} | yes | yes | yes |
| All other post endpoints | yes | yes | — |
GET /media, GET /media/{media} | yes | yes | yes |
| All other media endpoints | yes | yes | — |
| All tag endpoints, including reads | yes | yes | — |
Two results here are easy to trip over:
- A
VIEWERcan approve a post but cannot create one. - A
VIEWERcannot read tags. Unlike posts and media, whose read endpoints are open to viewers, every tag endpoint requiresADMINorMEMBER— soGET /tagsreturns403for a viewer.
The /panel/* endpoints are not in the table because they ignore workspace roles entirely: they
require a platform administrator account, whatever the user's role in any workspace.
Approving a post has two further conditions beyond role: the user must have approval rights in the workspace, and the workspace's plan must allow scheduling.