Management API
UmbSocial’s backoffice API is part of Umbraco’s management API, under:
/umbraco/management/api/v1/umbsocial/It is documented in its own OpenAPI document, umbsocial (titled “UmbSocial API”), alongside Umbraco’s own in the Swagger UI. The OpenAPI document is the definitive description of each request and response; this page summarises it.
Every endpoint needs a signed-in backoffice user with access to the Social section and the permission in the table (see Permissions). Times are in UTC.
| Method | Route | Permission | Use |
|---|---|---|---|
| GET | posts |
View | List posts. Query: fromUtc, toUtc, status, channel (a channel key). At most 500. |
| GET | posts/{key} |
View | Get one post. 404 when it doesn’t exist or is hidden. |
| POST | posts |
Write (+ Schedule) | Create a post. |
| PUT | posts/{key} |
Write (+ Schedule) | Change a post. |
| DELETE | posts/{key} |
Write | Delete a post (from Social only). |
| POST | posts/check |
Write | Check a post against its channels without saving: live counts. |
Composer helpers
Section titled “Composer helpers”| Method | Route | Permission | Use |
|---|---|---|---|
| GET | links/preview |
Write | A link card read from the page. Query: url. Returns title, description, imageUrl, or a refusal (link.https_required, link.not_allowed, link.unreachable). Rate limited: 30 a minute per user (429). See Links and link cards. |
| POST | media/preview |
Write | Each network’s cut of an image at preview size. Body: { "mediaKey": …, "imageFraming": …, "networks": ["bluesky", "instagram"] } (at most 20 networks). Returns { "previews": [ { "network": "bluesky", "url": "…" } ] }; url is null when there’s none. The URLs come from the site’s image URL generator, so they are signed when the site has an imaging HMAC key. Refused with media.unknown or image.framing_invalid. |
Creating or changing a post with any when other than Draft also needs Schedule (403 without it).
A post request (PostRequest):
{ "text": "Our new release is out.", "linkUrl": "https://www.example.com/news/release", "linkTitle": "What's new", "linkDescription": null, "linkImageUrl": "https://www.example.com/images/release-card.png", "mediaKey": null, "imageAltText": null, "imageFraming": null, "channels": { "6f1c4c1e-3b0a-4f53-9a51-2d7e0b2f6a10": null, "0b9d7a52-8f3e-4c1d-b6a4-7e5f3c2d1a90": "Our new release is out. Shorter for X." }, "when": "Queue", "atUtc": null}channels maps each channel key to that channel’s own text, or null for the main text. when is Draft, Queue, At (with atUtc) or Now. A mediaKey that isn’t a media item is refused (media.unknown). linkImageUrl is the link card’s image, an absolute https URL.
imageFraming frames the image for each network (see Images), as fractions from 0 to 1. A crop gives how much to trim from each side, Umbraco’s crop coordinates, and a network has at most one. Anything outside the image is refused (image.framing_invalid):
"imageFraming": { "focalPoint": { "left": 0.7, "top": 0.4 }, "crops": [ { "network": "instagram", "left": 0.2, "top": 0, "right": 0.2, "bottom": 0 } ]}Create and update return { "key": "<post key>" }. posts/check always returns 200 with errors (about the post itself) and, per channel, channelKey, network, count, max and errors.
Targets
Section titled “Targets”| Method | Route | Permission | Use |
|---|---|---|---|
| POST | targets/{id}/retry |
Resolve | Send a Failed or Unknown target again, as a new attempt. |
| POST | targets/{id}/mark-published |
Resolve | Record that a Failed or Unknown target is live. Body: { "postUrl": "https://…" } (or null). |
{id} is the target’s id from a post’s targets. Both return { "key": "<post key>" }.
Channels
Section titled “Channels”| Method | Route | Permission | Use |
|---|---|---|---|
| GET | channels |
View | The channels a post can go to, each with its network’s limits. |
| GET | channels/networks |
ManageSettings | The networks whose Automate provider is installed, with their connection type and limits. |
| GET | channels/connections |
ManageSettings | The Automate connections a new channel on a network can use. Query: network. |
| POST | channels |
ManageSettings | Add a channel. Body: network, name, automateConnectionId, options. |
| PUT | channels/{key} |
ManageSettings | Change a channel. Body: name, isEnabled, options. |
| DELETE | channels/{key} |
ManageSettings | Remove a channel. |
| POST | channels/{key}/automation |
ManageSettings | Create the channel’s managed automation if it’s missing. |
| POST | channels/{key}/connection-test |
ManageSettings | Test the channel’s Automate connection. |
options are the network’s channel options, as a string-to-string object, for example { "organizationId": "12345" }.
A channel’s limits carry what a client needs to count as you type: maxCharacters, urlWeight, countsGraphemes, weightsWideCharacters, requiresImage, requiresLinkTitle, supportsAltText, link (Card, InText or NotSent) and linkOrImage. No response carries connection credentials or Automate settings: channels expose only Automate ids.
Schedule and settings
Section titled “Schedule and settings”| Method | Route | Permission | Use |
|---|---|---|---|
| GET | schedule |
View | The time zone and the shared slots. |
| PUT | schedule |
ManageSettings | Replace the shared slots, and set the time zone with them when timeZoneId is given (both or neither are saved). Body: { "slots": [ { "day": …, "localTime": "09:00:00" } ], "timeZoneId": "Europe/London" }. |
| GET | schedule/slots |
View | The shared slots’ times in a range, and whether a post has taken each. Query: fromUtc, toUtc, at most 42 days apart (schedule.range_too_long). |
| GET | schedule/next-free-slot |
View | The next free slot, { "utc": … } (null when there is none). |
| GET | settings |
ManageSettings | { "timeZoneId": "Europe/London", "publishingEnabled": true, "environmentName": "Production", "includePostTextInWebhooks": false, "includeAuthorInWebhooks": false }. |
| PUT | settings |
ManageSettings | Set the webhook opt-ins and, when timeZoneId is given, the time zone (both or neither are saved). Body: { "timeZoneId": null, "includePostTextInWebhooks": false, "includeAuthorInWebhooks": false }. Every request sets both opt-ins: one left out is saved as false. |
Edition
Section titled “Edition”| Method | Route | Permission | Use |
|---|---|---|---|
| GET | edition |
View | The allowance in use: { "isLicensed": false, "maxChannels": 3, "historyDays": 90, "channelsUsed": 1 }. maxChannels and historyDays are null when there is no limit. |
Responses
Section titled “Responses”| Status | When |
|---|---|
| 200 | Done. |
| 400 | Refused. The body is problem details with the error codes in its codes extension (ProblemDetails.Extensions["codes"], written as a top-level codes property). |
| 403 | The user lacks the permission, or isn’t a backoffice user. |
| 404 | Something wasn’t found: a code ending .not_found (problem details), or a post that doesn’t exist or is hidden (GET posts/{key}, no body). |
| 429 | Rate limited (see below). |
A refusal looks like this:
{ "status": 400, "title": "The request was refused.", "codes": [ "text.too_long:x", "link.title_required:linkedin" ]}Every problem found is listed, not only the first.
Channel responses are { "key": "<channel key>", "warnings": [ … ] }.
- After adding a channel,
warningscan holdchannel.no_workspace(or another automation problem): the channel was saved, but without its automation. Fix the workspace and callPOST channels/{key}/automation. - A refused change — adding, changing, removing a channel or retrying its automation (for example
channel.settings_invalid,channel.cancelled:<message>orpost.in_flight) — comes back as a problem response (400, or 404 for an unknown channel), and the channel is left as it was.
Rate limit
Section titled “Rate limit”To stop a compromised editor account flooding your social accounts, each user can make at most 10 requests a minute that send posts out at once:
- creating or changing a post with
whenset toNow, orAta time within the next 5 minutes; - retrying a target.
Over the limit, the request is refused with 429, and isn’t counted. Queued posts aren’t counted, because each takes its own slot. Marking as published isn’t counted. The limit is counted on each server: with several servers behind a load balancer, it applies on each.