Publishing a scheduled post (curl)
This guide walks a complete happy path: pick media, create a scheduled post, publish it, and confirm the webhook fired.
Prerequisites
Section titled “Prerequisites”- An API key exported as
NATIVPOST_API_KEY. - At least one connected social account. This guide targets X, whose platform id is
twitter. jqfor reading JSON out of the responses.
Two setup steps happen in the dashboard, not over the API:
- Media — the API cannot upload. Add files under Content → Media, or host them yourself and use your own URLs.
- Webhooks — the API cannot create endpoints. Add one under Settings → Webhooks, subscribe it to
content.publishedandcontent.publish_failed, and copy its signing secret from there.
1. Pick a media URL
Section titled “1. Pick a media URL”MEDIA_URL=$(curl -sS "https://app.nativpost.com/api/v1/media-library?asset_type=image&limit=1" \ -H "Authorization: Bearer $NATIVPOST_API_KEY" \ | jq -r ".data[0].url")
echo "Using $MEDIA_URL"Content references media by URL, not by asset id, so url is the field you want. Any publicly reachable HTTPS URL works here — the library is just the convenient source.
2. Confirm the webhook endpoint is live
Section titled “2. Confirm the webhook endpoint is live”curl -sS https://app.nativpost.com/api/v1/webhooks \ -H "Authorization: Bearer $NATIVPOST_API_KEY" | jq{ "object": "list", "data": [ { "id": "c81f0d3a-52b7-4e19-9f26-8a7b1c0d4e35", "object": "webhook_endpoint", "url": "https://webhook.site/your-uuid-here", "events": ["content.published", "content.publish_failed"], "description": null, "enabled": true, "created_at": "2026-07-19T11:40:00Z", "updated_at": "2026-07-19T11:40:00Z" } ], "has_more": false, "next_cursor": null}Signing secrets are never returned by the API — read yours from the dashboard.
If events is an empty array, the endpoint receives every event rather than none.
3. Create the post, scheduled for one hour from now
Section titled “3. Create the post, scheduled for one hour from now”SCHEDULED_FOR=$(date -u -d "+1 hour" +"%Y-%m-%dT%H:%M:%SZ")
CONTENT_ID=$(curl -sS -X POST https://app.nativpost.com/api/v1/content \ -H "Authorization: Bearer $NATIVPOST_API_KEY" \ -H "Content-Type: application/json" \ -d "$(jq -n \ --arg m "$MEDIA_URL" \ --arg s "$SCHEDULED_FOR" \ '{ caption: "Launch day.", content_type: "single_image", target_platforms: ["twitter"], media_urls: [$m], status: "scheduled", scheduled_for: $s }')" \ | jq -r ".id")
echo "Created $CONTENT_ID for $SCHEDULED_FOR"The field names matter: caption for the copy, target_platforms for destinations, media_urls for media, scheduled_for for the time. content_type is single_image — there is no photo type.
CONTENT_ID comes back as a plain UUID.
4. Confirm the state
Section titled “4. Confirm the state”curl -sS https://app.nativpost.com/api/v1/content/$CONTENT_ID \ -H "Authorization: Bearer $NATIVPOST_API_KEY" | jq '.status, .scheduled_for'Expect "scheduled" and the timestamp from step 3.
5. Publish now instead of waiting
Section titled “5. Publish now instead of waiting”curl -sS -X POST https://app.nativpost.com/api/v1/content/$CONTENT_ID/publish \ -H "Authorization: Bearer $NATIVPOST_API_KEY" | jqReturns 202 Accepted: the serialized content with status flipped to approved and one extra flag.
{ "id": "9f2c8e7b-1a3d-4f60-b2c1-7e5a9d3c4b81", "object": "content", "status": "approved", "scheduled_for": "2026-07-19T12:31:00Z", "publish_queued": true}This queues the post — it does not call X inline, so there are no per-platform results in the response. The cron picks it up on its next tick, typically under a minute.
If target_platforms were empty, this call would fail instead:
{ "error": { "code": "no_target_platforms", "message": "Set target_platforms on the content item before publishing.", "docs_url": "https://docs.nativpost.com/errors" }}6. Watch for the outcome
Section titled “6. Watch for the outcome”Poll until the cron has run:
watch -n 15 "curl -sS https://app.nativpost.com/api/v1/content/$CONTENT_ID \ -H 'Authorization: Bearer $NATIVPOST_API_KEY' | jq -r '.status'"approved → publishing → published. If every platform rejects the post, the status returns to approved rather than moving to a failure state — so approved after a publish attempt means “failed, try again”.
Meanwhile your endpoint receives the real answer:
{ "id": "b3d1f0a6-77c2-4e88-9a41-2f6e8c5d0b19", "event": "content.published", "created_at": "2026-07-19T12:31:41Z", "data": { "content": { "id": "9f2c8e7b-1a3d-4f60-b2c1-7e5a9d3c4b81", "object": "content" }, "published_at": "2026-07-19T12:31:40Z", "platforms": [ { "platform": "twitter", "success": true, "platform_post_id": "1789...", "error": null } ] }}7. Verify the delivery
Section titled “7. Verify the delivery”There is no delivery-log endpoint on /api/v1. Check deliveries in the dashboard under Settings → Webhooks, which shows status code, response body, and duration per attempt.
Worth knowing: each event is attempted once, with a 10-second timeout. There is no retry on failure. After 20 consecutive failures the endpoint is disabled automatically.
Wrap steps 3 to 5 in your own script, or move to the Zapier and n8n guides for no-code versions of the same flow.