GET /api/api-keys — List API keysSession only (Dashboard). Returns all API keys for the authenticated user. Keys are shown with prefix only — the full key is never returned after creation.
POST /api/api-keys — Create API keySession only (Dashboard). Creates a new API key. The full key (`plainKey`) is returned only in this response — store it securely.
GET /api/api-keys/{id} — Get API key detailsSession only (Dashboard).
PATCH /api/api-keys/{id} — Update or revoke API keySession only (Dashboard).
DELETE /api/api-keys/{id} — Delete API keySession only (Dashboard). Permanently deletes an API key. This action cannot be undone.
POST /api/api-keys/verify — Verify API keyCheck whether an API key is valid. Send the key via the Authorization header.
GET /api/content — List contentReturns blog posts and articles. Filtered by organization and user role automatically.
POST /api/content — Create contentCreates a new blog post or article. The content starts as a draft.
GET /api/content/{id} — Get content by IDPATCH /api/content/{id} — Update contentDELETE /api/content/{id} — Delete contentSoft-deletes the content. Optionally deletes from WordPress too.
POST /api/content/{id}/generate — Generate AI contentUses AI to generate the blog post body based on the content's prompt, keywords, and site context. Consumes AI credits.
POST /api/content/{id}/publish — Publish to WordPressPublishes the content to the connected WordPress site. Creates or updates the WordPress post.
POST /api/content/{id}/unpublish — Unpublish from WordPressRemoves the post from WordPress and resets the content status to draft.
POST /api/content/{id}/featured-image — Generate featured imageGenerates an AI featured image based on the content's image prompt. Optionally uploads it to WordPress.
GET /api/content/{id}/changes — Change historyChange history of a content item during the customer approval process, newest first.
POST /api/content/{id}/generate-image — Generate featured imageGenerates an AI featured image, stores it in the media library and sets it on the content. Optionally uploads it to the WordPress target. Billable (credits or an included revision).
POST /api/content/{id}/image-prompt-preview — Preview image promptReturns the suggested image prompt and the credit cost of the next image generation. No side effects.
GET /api/content/{id}/links — List in-text linksPOST /api/content/{id}/links — Replace in-text linksReplaces all keyword links of the content (at most 3).
DELETE /api/content/{id}/links — Delete all in-text linksPOST /api/content/{id}/publish-async — Queue publishSchedules the content for immediate publishing and hands it to the background job. The result shows in the content status. Already published content is updated via `POST /api/content/{id}/publish`.
POST /api/content/{id}/reconcile-publish — Resolve a stuck publishChecks a content item stuck in `publishing` read-only against WordPress and either finalizes it as published or releases it to draft.
POST /api/content/{id}/request-publication — Request marketplace publicationCreates a marketplace channel booking from the draft's marketplace target and submits it for publisher review. Idempotent: an existing booking is returned with `alreadyRequested: true` and status 200.
POST /api/content/{id}/restore-published-url — Restore published URLReads the published post back from WordPress to restore a missing public URL.
POST /api/content/{id}/upload-image — Upload featured imageUploads a custom featured image (JPEG, PNG, GIF or WebP, at most 10 MB) and sets it on the content.
POST /api/content/generate — Generate free-standing contentGenerates AI text for a prompt, or topic suggestions for a website when `websiteUrl` and `websiteName` are sent (then the suggestions branch wins). Not bound to a content item.
POST /api/content/generate-keywords — Suggest SEO keywordsAI keyword suggestions for a title and optional HTML content. Billable.
POST /api/content/generate-seo-meta — Generate SEO metaAI meta title (at most 60 characters) and meta description (at most 160 characters). Billable.
POST /api/content/generate-suggestions — Suggest title, prompt and keywordsAI suggestions for a new article, with a hint when the topic conflicts with existing content. Billable.
GET /api/author-profiles — List author profilesReturns public author identities for the active organization. Requires `content:read`.
POST /api/author-profiles — Create author profileCreates an organization-owned public author identity. Requires `content:write`.
GET /api/author-profiles/{id} — Get author profilePATCH /api/author-profiles/{id} — Update author profileDELETE /api/author-profiles/{id} — Archive author profileGET /api/wordpress-sites — List WordPress sitesReturns all connected WordPress sites for the authenticated user or organization.
POST /api/wordpress-sites — Connect WordPress siteConnects a WordPress site using an Application Password. Format for apiToken: `username:applicationpassword`.
GET /api/wordpress-sites/{id} — Get WordPress siteSite details incl. `maxPostsPerDay`; the API token stays redacted. With an API key only sites of the key's tenant (no admin cross-tenant access, no deleted sites); foreign, missing and malformed ids return 404 `not_found`.
PATCH /api/wordpress-sites/{id} — Update WordPress siteWith an API key (`sites:write` plus RBAC `wordpress.sites.edit`) the body may ONLY contain `maxPostsPerDay` (positive integer or null = unlimited); any other field gives 400 `unknown_field`. Programme-bound sites give 409 `program_managed`; a site that is today's target of an active one-shot draft gives 409 `site_target_locked`. The dashboard session may change all fields listed below.
DELETE /api/wordpress-sites/{id} — Delete WordPress siteSession only (Dashboard). Soft-deletes the site. The site can be restored by an admin.
GET /api/wordpress-sites/daily-capacity — Get today's daily capacity of a siteRead-only, reserves nothing. With an API key: today's occupancy (Europe/Vienna calendar day, WordPress posts plus CMM reservations) of one site of the key's tenant, addressed by `siteId` (required); deleted, external and foreign sites give 404 `not_found`. The dashboard session may also pass a `channelId` and receives only a localized `{ warning }`.
POST /api/wordpress-sites/auto-connect — Auto-connect WordPress siteSession only (Dashboard). Automatically creates an Application Password via the WordPress REST API. The admin password is used once and never stored.
GET /api/wordpress-sites/{id}/categories — List site categoriesStored categories of the site. With `sync=true` they are first synced from WordPress; that needs site access and the `sites:write` scope (plain reads need `sites:read`; marketplace listings are readable without site access). If the sync fails but categories are stored, those are returned with 200.
POST /api/wordpress-sites/{id}/categories — Create local categoryCreates a category record for the site in ChatCMM only; it is not pushed to WordPress. Requires the site edit permission.
POST /api/wordpress-sites/suggest — Suggest sites for an articleUp to 5 accessible sites with a selectable publishing channel, ranked for the article (keyword prefilter, then AI ranking). Billable by token usage after a successful AI ranking; if the AI ranking fails, a keyword-only result is returned without charge.
GET /api/site-groups — List site groupsLists the caller's site groups with their member sites. Requires the RBAC permission `wordpress.sites.view`. API keys receive the allow-listed `SiteGroup` view; only sites of the key's tenant are listed as members. With an API key the group must belong to the key's organization (without an organization: to the key owner, `orgId IS NULL`). Foreign, missing and malformed ids all return an identical 404 `not_found` (no existence oracle). The dashboard session keeps its own, wider ownership rule and its unchanged response shape.
POST /api/site-groups — Create site groupCreates a group, optionally with member sites. Requires `wordpress.sites.create`. Every `siteIds` entry must be a non-deleted site of the key's tenant, otherwise 400 `site_not_owned` and nothing is created. The group can then be used as `channelFilter.groupId` (with `mode = "group"`) in `PATCH /api/campaigns/{id}/recurring`; that endpoint checks the group and its member sites with the same strict tenant rule (#1168). With an API key the group must belong to the key's organization (without an organization: to the key owner, `orgId IS NULL`). Foreign, missing and malformed ids all return an identical 404 `not_found` (no existence oracle). The dashboard session keeps its own, wider ownership rule and its unchanged response shape.
PATCH /api/site-groups/{id} — Update site groupRenames the group, changes its description and/or replaces its members (`siteIds`, atomic). Replaces the members visible to the key; members outside the key's tenant stay unchanged. Requires `wordpress.sites.edit`. With an API key the group must belong to the key's organization (without an organization: to the key owner, `orgId IS NULL`). Foreign, missing and malformed ids all return an identical 404 `not_found` (no existence oracle). The dashboard session keeps its own, wider ownership rule and its unchanged response shape.
DELETE /api/site-groups/{id} — Delete site groupDeletes the group; its memberships are removed with it (the sites stay). Requires `wordpress.sites.delete`. A recurring campaign that still points at the group via `channelFilter.groupId` is not changed here and fails its next check until a new group is set. With an API key the group must belong to the key's organization (without an organization: to the key owner, `orgId IS NULL`). Foreign, missing and malformed ids all return an identical 404 `not_found` (no existence oracle). The dashboard session keeps its own, wider ownership rule and its unchanged response shape.
GET /api/models — List AI modelsLists the text and image models of the AI Gateway catalog (public catalog data, no tenant data). Use `gatewayId` as `channelFilter.generationModels.text` / `.image` in `PATCH /api/campaigns/{id}/recurring` (a `text` model for `text`, an `image` model for `image`). With an API key `type` and `grouped` are validated strictly (400 `invalid_input`) and an unavailable or empty catalog answers 503 `model_catalog_unavailable`. The dashboard session receives the legacy response without `gatewayId` and keeps its lenient query parsing.
GET /api/campaigns — List campaignsReturns campaigns with article counts and cost summaries. API keys receive the allow-listed view (`PublicCampaignListItem` / `PublicCampaignArticle`): no approval tokens, agent log or conversation, and `channelFilter` only with its editable fields. The dashboard session receives an undocumented superset.
POST /api/campaigns — Create campaignCreates a new campaign with articles. Each article maps to a channel or own WordPress site.
GET /api/campaigns/{id} — Get campaign with articlesAPI keys receive the allow-listed view: `channelFilter` only with its editable fields, and approval tokens, agent log and server-owned fields are never returned. The dashboard session receives an undocumented superset; with a session and an API key at the same time, the session applies. The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
PATCH /api/campaigns/{id} — Update campaignUpdates the plain campaign fields. Recurring settings are changed only via `/api/campaigns/{id}/recurring`, `/recurring/activate` and `/recurring/pause` (an API key sending recurring keys gets 400 `unknown_field`; the dashboard session additionally accepts the `RecurringConfigPatch` keys plus `isRecurring`). `status` is not settable (booking workflow); `summary` and `campaignLinks` are not settable via PATCH. Setting `customerOrganizationId`, `primaryContactUserId`, `campaignApprovalMode` or `campaignApprovalMinCount` can lift the customer approval block and therefore additionally requires the key scope `content:publish` and the RBAC permission `content.publish`. An API key has no admin bypass for the customer organization (`customer_org_not_owned`). Campaigns managed by an automation program return 409 `program_managed`. The response is the flat `PublicCampaign` view. The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
DELETE /api/campaigns/{id} — Delete campaignMoves the campaign to the trash (soft delete). A second DELETE on a trashed campaign returns 404. Permanent deletion is possible only for admins in the browser session. The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
GET /api/campaigns/{id}/links — Get campaign-wide linksReturns the campaign-wide keyword links (the fallback for every generated article without its own article links). Legacy data with more than `maxLinks` entries is returned unchanged. An unknown stored `linkType` is returned as `dofollow`. `updatedAt` also changes when a recurring run claims a topic or remembers one. The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
PUT /api/campaigns/{id}/links — Replace campaign-wide linksReplaces the whole list (an empty list removes the links). At most 10 links, `http`/`https` only without credentials, no duplicate URL (compared as `URL.href`) and no duplicate anchor text (case-insensitive); a violation returns 400 `invalid_input` with `details.reason = duplicate_link` and the `index`. `linkType` is a hint stored with the link; today only the mock generation path renders it as `rel`. Unknown fields return 400 `unknown_field`. `expectedUpdatedAt` is optional: after every recurring run the token is stale (409 `conflict_stale`), so re-read first. For a RECURRING campaign (`isRecurring` or a recurrence pattern set) the links flow into every automatically generated article, so an API key additionally needs the scope `content:publish` (otherwise 403 `forbidden_scope`) and the RBAC permission `content.publish` (403 `forbidden_permission`). Campaigns managed by an automation program return 409 `program_managed`; an active one-shot draft returns 409 `one_shot_active`. The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
GET /api/campaigns/{id}/roadmap — List open roadmap topicsReturns the open roadmap topics of a recurring campaign (`status = planned`, no content, no booking) in the order the generator draws them (scheduled date, series order, id), at most 500; `truncated` is true when more exist. `eligible` and `blockedReason` mirror the worker candidate filter exactly (no normalisation): `locked`, `content_in_progress`, `already_generated_topic`, `missing_category` (daily pilot and draft upload need a category per topic) and `site_fit` (only with exactly one fixed target site: language mismatch, excluded topic or editorial site, see `siteFitReason`). The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
POST /api/campaigns/{id}/roadmap — Add roadmap topicsAdds 1 to 52 topics to a recurring campaign without AI and without credits. New topics are unlocked, so the generator draws them from the next cycle on. Duplicates (case-insensitive, whitespace and Unicode normalised) against open and generated topics, or within the request, are returned in `skipped`; cancelled topics do not block. At most 365 open topics per campaign (400 `invalid_input`, `details.reason = roadmap_full`, nothing written). A category needs exactly one fixed target site (`channelFilter.mode = list` with one site) and must belong to that site and be synced to WordPress (400 `category_invalid`, `details.reason` one of `no_single_target_site`, `category_not_found`, `category_not_synced`, `site_not_in_tenant`, `site_unavailable`, `category_required`); a foreign and a non-existent category are indistinguishable. The call is rejected with 409 `roadmap_cycle_in_progress` while a cycle is due (within 30 minutes after the planned time) or running (latest claim younger than 30 minutes without a booking), so a new topic never skips the due cycle. A non-recurring campaign returns 400 (`details.reason = campaign_not_recurring`). Requires the `content.publish` permission and, with an API key, BOTH scopes `campaigns:write` and `content:publish`. The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
PATCH /api/campaigns/{id}/roadmap/{articleId} — Change a roadmap topicChanges `topic`, `keywords`, `scheduledDate` (ordering hint only: the generator overwrites it on booking) or `wordPressCategoryId` of a planned topic. At least one field; `expectedUpdatedAt` is optional. Only topics without a site or channel can be changed (409 `roadmap_topic_not_planned`, `details.reason = site_bound`; edit those in the dashboard). A topic that is booked, generated, generating or reserved for a cycle returns 409 `roadmap_topic_not_planned` (`not_planned`, `content_in_progress`, `reserved_for_cycle`). A title that already exists returns 400 `invalid_input` with `details.reason = duplicate_topic`. The category rules and the cycle guard of `POST` apply. Requires the `content.publish` permission and, with an API key, BOTH scopes `campaigns:write` and `content:publish`. The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
DELETE /api/campaigns/{id}/roadmap/{articleId} — Cancel a roadmap topicSoft delete: sets the topic to `cancelled`. A cancelled topic is no longer drawn, does not use site capacity and does not block a new topic with the same title. Cancelling an already cancelled topic is idempotent (`changed: false`). Cancelling the last candidate with a category of a daily pilot pauses the campaign at its next run (intended). The request must have no body. Requires the `content.publish` permission. The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
GET /api/campaigns/{id}/recurring — Get recurring configurationReturns the editable recurring configuration as an allow-listed view. The stored `channelFilter` is never returned raw: server-owned fields are not readable. The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
PATCH /api/campaigns/{id}/recurring — Update recurring configurationPartial update of the recurring configuration. State and configuration are separate: `isRecurring` is NOT accepted (use `/recurring/activate` and `/recurring/pause`). Unknown fields are rejected with 400 `unknown_field`. `channelFilter` is deep-merged over the stored value; `null` clears `publicationMode`, `generationModels` and `contentLength`. `expectedUpdatedAt` is optional; if it differs from the stored `updatedAt` the call returns 409 `conflict_stale`. `updatedAt` has second granularity: a change made by the dashboard in the same second as your read is not detected. `nextGenerationAt` is recomputed only on a real schedule change (pattern, day, time, pauseOnHolidays, effective time zone); sending identical values changes nothing. Without `channelFilter.timeZone` the server time zone (UTC) applies. Requires the `content.publish` permission. With an API key the key needs BOTH scopes `campaigns:write` and `content:publish` (otherwise 403 `forbidden_scope` and nothing is written), because this call can enable automatic WordPress publishing. The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
POST /api/campaigns/{id}/recurring/activate — Activate recurringActivates (or resumes) the recurring automation. Idempotent: an already active campaign with a scheduled run is left unchanged (`changed: false`). The next run is computed once on activation (next occurrence after now). Requires a configured `recurrencePattern`. With an API key the key needs BOTH scopes `campaigns:write` and `content:publish` (otherwise 403 `forbidden_scope` and nothing is written), because activation starts automatic WordPress publishing. The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
POST /api/campaigns/{id}/recurring/pause — Pause recurringPauses the recurring automation and clears the next run. Idempotent: an already paused campaign is left unchanged. Pause always wins; there is no compare-and-set on the schedule. The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
GET /api/campaigns/{id}/recurring/status — Get recurring statusDerived status: next run (unix, ISO, time zone), counters, last successful run, pause reason (from the owner notification, never its text), pilot-guard result and costs. The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
GET /api/campaigns/{id}/recurring/runs — List recurring runsRun history: one entry per claimed cycle of the recurring generator, newest cycle first. Programme campaigns (editorial and product programmes) are not recorded. The campaign must belong to the caller's organization (without an organization: to the caller). Foreign, missing and deleted campaigns all return an identical 404 `not_found` (no existence oracle).
GET /api/channels — List marketplace channelsSession only (Dashboard). Returns available channel listings from the marketplace. Channels represent connected WordPress sites or manually fulfilled external publisher offers.
POST /api/channels — Create or update channel listingSession only (Dashboard). Lists a WordPress site on the marketplace or updates an existing listing.
GET /api/channels/{id} — Get channel detailsSession only (Dashboard).
PATCH /api/channels/{id} — Update channel listingSession only (Dashboard).
DELETE /api/channels/{id} — Delete channel listingSession only (Dashboard).
GET /api/channels/{id}/supplier — Get private supplier detailsOffering owner or active offering-account owner/admin only; site ownership also required. Cache-Control: private, no-store.
PUT /api/channels/{id}/supplier — Replace private supplier detailsSame offering-account guard as GET. Original import metadata is preserved.
GET /api/bookings — List bookingsReturns marketplace bookings. Filter by role to see bookings as creator (advertiser) or publisher.
POST /api/bookings — Create bookingBooks a marketplace channel for a sponsored post.
GET /api/bookings/{id} — Get booking detailsSession only (Dashboard).
PATCH /api/bookings/{id} — Update bookingSession only (Dashboard). Update booking status, schedule, or publish URL.
GET /api/media — List media itemsReturns uploaded and AI-generated images.
POST /api/media — Create media itemUploads an image (multipart, stored in the media bucket, variants processed in the background) or registers an image that already lives at an external URL (JSON).
POST /api/media/multipart/create — Start multipart video uploadStarts a direct-to-bucket multipart upload for a video (MP4, QuickTime, WebM). Upload the parts with presigned URLs from `/api/media/multipart/part-url`, then call `/complete` and register the file with `/api/media/confirm`. Requires a plan with video uploads.
POST /api/media/multipart/part-url — Presigned URL for one partPOST /api/media/multipart/complete — Complete multipart uploadAssembles the uploaded parts. Does not create a media item; register the file with `/api/media/confirm`.
POST /api/media/multipart/abort — Abort multipart uploadFrees the parts of an unfinished upload. An unknown or finished upload also returns 200.
POST /api/media/presign — Get presigned upload URLReturns a presigned S3 URL for direct upload. Upload the file via PUT to the returned URL, then call `/api/media/confirm`.
POST /api/media/confirm — Confirm S3 uploadSaves media metadata after a successful S3 upload via presigned URL.
POST /api/media/generate — Generate AI imageSession only (Dashboard). Generates an image using AI (OpenAI gpt-image-1) and stores it in S3.
GET /api/team — List team membersSession only (Dashboard). Returns the team members of the publisher by role, the publisher's pending (not accepted, not expired) team invitations and the roles that can currently be invited (active roles only).
POST /api/team — Invite team memberSession only (Dashboard). Invites a user by email. Existing and new addresses get the same pending invitation and the same answer; the person joins only after accepting it. Inactive roles are rejected. Limited per publisher (50 per hour, 300 per day).
DELETE /api/team/{id} — Remove team memberSession only (Dashboard).
GET /api/organizations — List organizationsSession only (Dashboard). Returns all organizations the authenticated user is a member of.
POST /api/organizations — Create organizationSession only (Dashboard).
GET /api/organizations/{id} — Get organization detailsSession only (Dashboard). Returns organization with members and campaign count.
PUT /api/organizations/{id} — Update organizationSession only (Dashboard).
DELETE /api/organizations/{id} — Delete organizationSession only (Dashboard). Deletes the organization. Fails if organization is used in active campaigns.
GET /api/orders — List ordersReturns publisher orders. Requires `orders:read`.
POST /api/orders — Create orderCreates an empty order wrapper for a customer organization. Requires `orders:write`.
GET /api/orders/{id} — Get order detailsReturns the order plus customer organization, linked content/campaign (incl. articles) and the Claribill invoice URL. Requires `orders:read`.
PATCH /api/orders/{id} — Update order / link contentUpdates order metadata or links a post. Linking behavior: the first `contentId` links directly; a second post automatically converts the order into an invisible container campaign; further posts append as articles. `contentExternalReference` sets the per-position reference for the newly linked post. Requires `orders:write`.
DELETE /api/orders/{id} — Delete order (soft delete)POST /api/orders/{id}/status — Change order statusMoves the order through its lifecycle (draft → in_progress → [pending_approval → approved] → published → invoiced → paid → completed; cancelled). `in_progress` requires linked content, a campaign or an external contribution line item. Requires `orders:write`.
GET /api/orders/{id}/line-items — List order line itemsPositions (posts/channels, external contributions, add-ons, discounts). Requires `orders:read`.
POST /api/orders/{id}/line-items — Add line item (external post, add-on, discount)Adds a position. For an off-platform post use `source: "external"` with `externalUrl`, the customer price as `unitAmount` (cents) and optionally `internalCost` (internal purchase cost, never on the invoice) and `externalReference`. Adding an external item to a draft order auto-starts it. Requires `orders:write`.
PATCH /api/orders/{id}/line-items/{itemId} — Update line itemDELETE /api/orders/{id}/line-items/{itemId} — Delete line itemPOST /api/orders/{id}/invoice — Create Claribill invoiceCreates the multi-line Claribill invoice from the order line items (internal costs are never included). Options: `draft: true` creates a draft only; `external: true` marks the order as externally invoiced. Without a stored Claribill customer, pass `claribillCustomerId` or `createCustomer: true` (confirm via the customer-match endpoint first). Requires `orders:invoice`.
POST /api/orders/{id}/invoice/create-customer — Resolve or create the Claribill customer for an order's organizationSeparate step before invoicing: links the order's customer organization to a Claribill customer and persists the id, serialized through an org-wide atomic claim (`claribill_customer_claim_token`/`_expires_at`, #631) so two concurrent requests for the same unlinked organization cannot create duplicate Claribill customers or overwrite each other's persisted id. Pass `claribillCustomerId` for a UI-confirmed match, or omit it to search by email and, after UI confirmation, create a new one. Requires `orders:invoice`.
GET /api/orders/{id}/invoice/customer-match — Match order customer against ClaribillChecks whether the order's customer organization is linked to / found in Claribill. Exactly one customer with the organization's email → `match`; several customers sharing that email (or the same name in the name fallback) → `matches` for explicit selection. Includes a `taxReview` block when the customer's EU VAT ID suggests reverse charge but the order is set up with full VAT. Requires `orders:invoice`.
GET /api/orders/{id}/tax-review — Review order VAT against the customer's VAT IDCompares the order's VAT snapshot with the authoritative customer VAT ID (organization, falling back to the linked Claribill customer). Returns a reverse-charge suggestion when the customer has an EU VAT ID from another country than the issuer but the order is set up with full VAT. Read-only. Requires `orders:invoice`.
POST /api/orders/{id}/tax-review — Apply reverse-charge suggestionApplies the reverse-charge suggestion after validating the VAT ID via VIES (invalid → 422 `vat_id_invalid`): updates the customer organization defaults (backfills an empty VAT ID from Claribill), fixes the order's VAT snapshot and recomputes the totals. Requires `orders:invoice`.
POST /api/orders/{id}/invoice/sync — Sync invoice status from ClaribillPulls the current invoice status (and final invoice number) from Claribill. Requires `orders:invoice`.
GET /api/orders/{id}/invoice/send-status — Read the stored invoice Send receiptReads the original CMM Send observation without provider calls. ACCEPTED is transport acceptance, not delivery. Aftercare is separate; resolved:true does not convert UNKNOWN into acceptance. Requires `orders:invoice`.
POST /api/orders/{id}/invoice/send-status — Check the original invoice Send statusOnly GETs Claribill's send-status using the original persisted key and verifies the current company binding. Never sends, retries, creates a key, or uses invoice status as acceptance evidence. Requires `orders:invoice`.
POST /api/orders/{id}/invoice/reconcile — Resolve an order stuck on invoiceStatus "unclear" (#619)Three escape hatches for an ambiguous Claribill invoice create/send result that the strict reconciliation could not uniquely resolve: `recheck` (re-run the same strict match), `link` (verify and attach a Claribill invoice id the caller names), or `reset` (restore the pre-claim status — requires `confirmNoInvoiceExists: true`, and is itself refused if a possible non-cancelled invoice for this order is still found in Claribill). Requires `orders:invoice`.
GET /api/orders/stats — Order KPI statisticsAggregated counts and revenue. Requires `orders:read`.
GET /api/publishing-channels — List publishing channelsChannels of the organization plus own legacy channels, with usage counts. With an API key that also holds `channels:write`, missing channel rows of connected sites are created first (self-heal).
POST /api/publishing-channels — Create publishing channelCreates a WordPress channel (`authToken` required, `username:applicationPassword`) or a custom API channel (`apiKey` and `apiEndpoint` required). Credentials are never returned in responses.
GET /api/dashboard/stats — Dashboard statisticsContent, site and order statistics for the caller's access scope (administrators: all content and sites; orders always of the caller's organization).
GET /api/social/accounts — List social accountsConnected social accounts grouped by publishing channel (no-channel group last). Tokens are never returned.
GET /api/social/posts — List social postsPOST /api/social/posts — Create social postCreates a draft (or `pending_approval` with `requiresApproval`). Without `publishingChannelId` the channel is derived when all target accounts share one.
POST /api/social/posts/validate — Validate social postValidates a post body like create, without writing. Blocking findings are a 200 result, not an error.
POST /api/social/posts/from-content/{contentId} — Create social post from articleCreates a draft for all active accounts of the article's channel (or the given ones). `linkUrl` may be the placeholder `{{articleUrl}}`, filled on publish.
GET /api/social/posts/{id} — Get social postPATCH /api/social/posts/{id} — Update social postPartial update with optimistic lock (`scheduleVersion`): omitted fields stay unchanged (no defaults are applied, #1264). `targets` replaces the whole list; `status` and `scheduledAt` are ignored here. A scheduled post is rescheduled.
DELETE /api/social/posts/{id} — Cancel social postSoft delete: the post becomes `cancelled` and open targets are cancelled. Nothing is deleted on the networks (see the remote target delete).
POST /api/social/posts/{id}/approve — Approve social postPOST /api/social/posts/{id}/reject — Reject social postBack to `draft` with `approvalStatus: rejected`; a scheduled post is unscheduled.
POST /api/social/posts/{id}/duplicate — Duplicate social postPOST /api/social/posts/{id}/publish-now — Publish social post nowQueues the post for immediate publishing (background job). X credits are checked here and charged by the job.
POST /api/social/posts/{id}/schedule — Schedule social postSchedules the post (at least 60 s, at most 365 days ahead). X credits are checked here and charged by the publish job.
POST /api/social/posts/{id}/unschedule — Unschedule social postDELETE /api/social/posts/{id}/targets/{targetId}/remote — Delete published post on the networkDeletes the live post of one target on the social network, then cancels the target. Only for networks that support deletion via API.
POST /api/social/posts/{id}/targets/{targetId}/retry — Retry failed targetQueues a failed target for another publish attempt (background job).
GET /api/orders/{id}/approval — Approval contextRecipients, contacts, signature and article titles for sending editorial approval requests for all articles of an order.
POST /api/orders/{id}/approval — Send approval requestsCreates approval tokens and sends editorial approval emails for all articles of the order. Status 502 carries the same body with `success: false` when at least one email failed.
POST /api/orders/{id}/line-items/sync-channels — Sync channel line itemsRebuilds the order's channel line items from the linked campaign's articles in one transaction and recomputes the totals. Idempotent.