Posts
Publish a post
Live apps publish at once, schedule with a date or save a draft; test apps only simulate. With ?dry_run=true it checks everything and writes nothing.
A post belongs to an establishment and goes out to one or more networks, its targets. Each target has its own text, media, format and state.
Post type: now, scheduled or draft
Two body fields decide when, and whether, the post goes out:
| Case | Body | Behavior |
|---|---|---|
| Now | no scheduledAt | The post goes out right away. |
| Scheduled | scheduledAt: a future ISO 8601 date-time | It will go out at scheduledAt. |
| Draft | "status": "draft", no scheduledAt | It is saved and does not go out. |
The body of a post
| Field | Meaning |
|---|---|
establishmentId | The establishment that publishes (GET /v1/establishments) |
targets[] | One entry per network: platform, text, and optionally media, format and accountId |
targets[].media[] | A media file from the library by its id, or a public https url with its type (image or video) |
targets[].accountId | The account to use when the establishment has several on that network |
scheduledAt | A future ISO 8601 date-time; when absent, the post goes out right away |
status | draft to save a draft that does not go out (with no scheduledAt then) |
A target’s media
Each entry of targets[].media designates a media file from the library by its id, or a public https url with its type (image or video). Instagram and TikTok require a photo or a video. To add a media file to the library, see Import media.
Duplicating a post
There is no dedicated endpoint to duplicate a post: read it with `GET /v1/posts/{id}`, then send its content again in a new POST /v1/posts request.
Before sending it again, remove the fields that Sofia fills in herself. At a minimum:
- the post’s
idandcreatedAt; - the
statusof the post read (it summarizes the sending state): set the one you want, or omit it; - in each target:
state,publishedAt,permalinkanderror.
Keep establishmentId, and for each target platform and accountId. The read does not return the targets’ text or their media: supply them again. Finally set a new scheduledAt (or omit it to publish right away, or send "status": "draft").
Your first post
1. Check the connected networks
A post goes out through the accounts connected to the establishment. List them; if one is missing, create a connection link. See Connected accounts.
2. Add a media file
Instagram and TikTok require a photo or a video. Import one from a public https URL; Sofia downloads it into the library and returns its id. See Import media.
3. Check with a dry run
Add ?dry_run=true: Sofia judges each network by its real rules and the connected account, and writes nothing.
curl -X POST 'https://api.sofia-post.com/api/v1/posts?dry_run=true' \
-H "Authorization: Bearer $SOFIA_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"establishmentId": "'$ESTABLISHMENT_ID'",
"targets": [
{ "platform": "instagram", "text": "Tarte tatin du jour", "media": [{ "id": "'$MEDIA_ID'" }] },
{ "platform": "facebook", "text": "Tarte tatin du jour", "media": [{ "id": "'$MEDIA_ID'" }] }
]
}'{
"status": "ready",
"dryRun": true,
"targets": [
{ "platform": "instagram", "accountId": "fecd0904-…", "handle": "sof_ia_officiel", "state": "ready" },
{ "platform": "facebook", "accountId": "a423b3a4-…", "handle": "Sofia post", "state": "ready" }
]
}4. Publish
Send the same body without dry_run, with an Idempotency-Key so that a retry never publishes twice. Without scheduledAt, the post goes out right away; with a future scheduledAt, it is scheduled.
curl -X POST https://api.sofia-post.com/api/v1/posts \
-H "Authorization: Bearer $SOFIA_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d @post.json5. Track it
The post answers publishing, then each network moves to published with its permanent link, or to failed with its reason. Read it with `GET /v1/posts/{id}`, or register a webhook to be notified.
Dry run
POST /v1/posts?dry_run=true runs the checks of a real post: each network’s rules, the connected account, the media, the date and your plan. It saves nothing and sends nothing.
Ready
When every network would accept the post, the response is 200 with status: "ready" and, for each target, the account that would publish.
Refused
Otherwise, the response is 422 post_refused, with one detail per reason. Each detail names its network, its code, and says what to fix.
{
"error": {
"code": "post_refused",
"message": "The post would not be published: x: the text is too long (300 characters, at most 280)",
"details": [
{
"platform": "x",
"code": "caption_too_long",
"max": 280,
"actual": 300,
"message": "x: the text is too long (300 characters, at most 280)"
}
],
"docUrl": "https://www.sofia-post.com/developers/errors#post_refused"
}
}Each reason is listed in the errors.
For apps’ test mode, see Test mode and dry run. An Idempotency-Key also protects POST /v1/posts: see Idempotency.
Authorizations
AuthorizationstringheaderrequiredYour personal key or OAuth access token, sent as
Bearer <token>.
Query parameters
dry_runstringtrue: runs every check the send would run (network rules, accounts, media, creation, live subscription) and writes nothing. Returns 200readyor 422post_refused.Allowed values:
truefalse
Headers
Idempotency-KeystringReplayed with the same body, the request returns the same response without creating a second post. A dry run does not consume the key.
Body
establishmentIdstringrequiredEstablishment, in the Workspace of the token grant (
GET /v1/establishments)targetsobject[]requiredOne network per target
Show child attributes
platformstringrequiredTargeted network
Allowed values:
instagramfacebookxlinkedintiktoktextstringrequiredCaption published on this network
mediaobject[]Attached media
Show child attributes
idstringIdentifier of a media of the establishment library (otherwise
urlandtype)urlstringPublic address of the media (https in live); without an identifier
typestringKind of the media given by address
Allowed values:
imagevideo
formatstringFormat (
post,reel,story,carousel...); by default, the one the network would serveaccountIdstringLive: the connected account that publishes, when the establishment has several on this network (
GET /v1/establishments/:id/accounts)
scheduledAtstring<date-time>Scheduling date (ISO 8601), in the future; absent: publish immediately. Not allowed with
statusdraftstatusstringdraft: a draft (live only), created without a date, nothing is sent;PATCH /v1/posts/:idschedules itAllowed values:
draft
Response
Dry run: every check passed.
statusstringrequiredAlways
readyAllowed values:
readydryRunbooleanrequiredAlways
trueAllowed values:
truetargetsobject[]requiredThe checked targets
Show child attributes
platformstringrequiredNetwork of the target
statestringrequiredAlways
readyAllowed values:
readyaccountIdstringLive: the account that would publish
handlestringLive: its displayed handle