How to verify a post actually published (not just that your API call succeeded)
A practical, step-by-step guide to programmatically confirming a post really landed on a platform: reading target status, checking for a permalink, and subscribing to terminal webhooks instead of trusting a 200.
Don't trust the create call's 200 — it only confirms the post is queued. Read GET /api/posts/{id} and check each entry in the targets array: a target is genuinely published only when the destination platform returns its own platform_post_id and permalink. Subscribe to post.published, post.partial, and post.failed webhooks instead of polling tightly.
"It returned 200, so it published" is the assumption behind a whole category of support tickets — see Scheduler says published, but the post never appeared for why that assumption breaks down. This post is the practical follow-up: the concrete, programmatic steps to confirm a post actually landed on a platform, instead of trusting the response of the call that started the process.
PostGate handles OAuth, publishing and delivery evidence for every network through one API. Start free or read the docs.
1. Stop treating the create response as an answer
POST /api/posts returns as soon as PostGate has accepted the request — it confirms the post is queued, not that any destination has it yet. Delivery to each platform target happens asynchronously afterward. The only honest source of truth is reading the post back, or waiting for the terminal event that says it's done. See Delivery lifecycle for the full model.
2. Read target status, not aggregate status
GET /api/posts/{id} returns both an aggregate status and a targets array with an independent status per destination. Don't stop at the aggregate — a partial post has real published targets mixed with real failed ones, and only the target array tells you which is which.
curl https://api.postgate.studio/api/posts/post_01JXYZ \
-H "Authorization: Bearer $POSTGATE_API_KEY"
{
"id": "post_01JXYZ",
"status": "partial",
"targets": [
{ "profile_id": "pro_instagram123", "status": "published", "platform_post_id": "17895...", "permalink": "https://instagram.com/p/Cxyz.../" },
{ "profile_id": "pro_linkedin456", "status": "failed", "error": { "code": "platform_transient", "retryable": true } }
]
}3. Treat permalink presence as the actual proof
A target is only published once the destination network returns its own platform_post_id and permalink — that's the platform confirming something addressable exists, not PostGate marking a job complete. If you need to hand a customer proof, the permalink is it; it resolves to real content on the destination platform, which is a stronger claim than any status string.
4. Don't poll blind — subscribe or back off
If you poll GET /api/posts/{id}, back off between reads and stop once every target is terminal — polling tightly just adds load without changing when the platform actually responds. The better default is subscribing to post.published, post.partial, and post.failed via webhooks: PostGate pushes the same target-level evidence the moment every target resolves, so your product finds out as fast as it can rather than on your next poll interval. See Webhooks vs. polling for the full tradeoff.
5. If you retry, use the same Idempotency-Key
A client-side timeout on the original create call doesn't mean the post was never queued — it might already be in flight. Retry the same logical request with the same Idempotency-Key instead of issuing a fresh one, and PostGate returns the original post instead of creating a second one. See Idempotency & safe retries. Verifying delivery and avoiding accidental duplicates are two sides of the same discipline — see Preventing duplicate posts.
Frequently asked questions
Does a 200 response mean my post was published?
No. POST /api/posts returns as soon as PostGate has accepted the request — it confirms the post is queued, not that any destination has it yet. Delivery to each platform target happens asynchronously afterward. The only honest source of truth is reading the post back with GET /api/posts/{id}, or waiting for the terminal webhook event that says delivery finished.
What is the difference between a post’s aggregate status and its target status?
GET /api/posts/{id} returns both an aggregate status and a targets array with an independent status per destination. Don’t stop at the aggregate: a partial post has real published targets mixed with real failed ones, and only the target array tells you which is which — including each failure’s error code and whether it is retryable.
What counts as proof that a social media post is actually live?
A target is only published once the destination network returns its own platform_post_id and permalink — that is the platform confirming something addressable exists, not PostGate marking a job complete. If you need to hand a customer proof, the permalink is it: it resolves to real content on the destination platform, which is a stronger claim than any status string.
Should I poll for post status or use webhooks?
The better default is subscribing to post.published, post.partial, and post.failed webhooks — PostGate pushes the same target-level evidence the moment every target resolves, so your product finds out as fast as it can rather than on your next poll interval. If you do poll GET /api/posts/{id}, back off between reads and stop once every target is terminal.
My create request timed out — is it safe to retry?
A client-side timeout doesn’t mean the post was never queued — it might already be in flight. Retry the same logical request with the same Idempotency-Key instead of issuing a fresh one, and PostGate returns the original post instead of creating a second one. That keeps verification and duplicate prevention consistent.
