Posting
One request, every network. Each target can carry its own text, media and options.
Now
curl -X POST "https://api.usecrier.com/v1/posts" \
-H "Authorization: Bearer $CRIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Our autumn collection is here 🍂",
"targets": [
{
"accountId": "acc_ig"
},
{
"accountId": "acc_li"
}
]
}'const res = await fetch('https://api.usecrier.com/v1/posts', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRIER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"content": "Our autumn collection is here 🍂",
"targets": [
{
"accountId": "acc_ig"
},
{
"accountId": "acc_li"
}
]
}),
});
const { data } = await res.json();import os, requests
res = requests.post(
"https://api.usecrier.com/v1/posts",
headers={"Authorization": f"Bearer {os.environ['CRIER_API_KEY']}"},
json={
"content": "Our autumn collection is here 🍂",
"targets": [
{
"accountId": "acc_ig"
},
{
"accountId": "acc_li"
}
]
},
)
data = res.json()["data"]Scheduled
Add scheduledFor in ISO 8601 with a timezone offset.
curl -X POST "https://api.usecrier.com/v1/posts" \
-H "Authorization: Bearer $CRIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "We open Friday at 9",
"scheduledFor": "2026-10-30T09:00:00+01:00",
"targets": [
{
"accountId": "acc_ig"
}
]
}'const res = await fetch('https://api.usecrier.com/v1/posts', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRIER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"content": "We open Friday at 9",
"scheduledFor": "2026-10-30T09:00:00+01:00",
"targets": [
{
"accountId": "acc_ig"
}
]
}),
});
const { data } = await res.json();import os, requests
res = requests.post(
"https://api.usecrier.com/v1/posts",
headers={"Authorization": f"Bearer {os.environ['CRIER_API_KEY']}"},
json={
"content": "We open Friday at 9",
"scheduledFor": "2026-10-30T09:00:00+01:00",
"targets": [
{
"accountId": "acc_ig"
}
]
},
)
data = res.json()["data"]You can cancel it before it goes out with POST /v1/posts/{id}/cancel.
Queued
With queue: true it takes the next free slot of the profile’s queue. Every account must belong to the same profile.
curl -X POST "https://api.usecrier.com/v1/posts" \
-H "Authorization: Bearer $CRIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Tip of the day",
"queue": true,
"targets": [
{
"accountId": "acc_ig"
}
]
}'const res = await fetch('https://api.usecrier.com/v1/posts', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRIER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"content": "Tip of the day",
"queue": true,
"targets": [
{
"accountId": "acc_ig"
}
]
}),
});
const { data } = await res.json();import os, requests
res = requests.post(
"https://api.usecrier.com/v1/posts",
headers={"Authorization": f"Bearer {os.environ['CRIER_API_KEY']}"},
json={
"content": "Tip of the day",
"queue": True,
"targets": [
{
"accountId": "acc_ig"
}
]
},
)
data = res.json()["data"]Draft
With draft: true it is saved without posting or scheduling.
Text and media per network
Each target can replace the post’s text (content) and media (media), and carry its network’s options under the network’s key (instagram, youtube, tiktok…). See Network options.
curl -X POST "https://api.usecrier.com/v1/posts" \
-H "Authorization: Bearer $CRIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "How our jacket is made",
"media": [
{
"type": "video",
"url": "https://cdn.acme.com/jacket.mp4"
}
],
"targets": [
{
"accountId": "acc_ig",
"instagram": {
"format": "reel",
"firstComment": "#autumn #fashion"
}
},
{
"accountId": "acc_yt",
"youtube": {
"title": "How our jacket is made",
"visibility": "public"
}
},
{
"accountId": "acc_x",
"content": "How our jacket is made 🧵",
"x": {
"thread": [
"1/ It starts in the workshop…"
]
}
}
]
}'const res = await fetch('https://api.usecrier.com/v1/posts', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRIER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"content": "How our jacket is made",
"media": [
{
"type": "video",
"url": "https://cdn.acme.com/jacket.mp4"
}
],
"targets": [
{
"accountId": "acc_ig",
"instagram": {
"format": "reel",
"firstComment": "#autumn #fashion"
}
},
{
"accountId": "acc_yt",
"youtube": {
"title": "How our jacket is made",
"visibility": "public"
}
},
{
"accountId": "acc_x",
"content": "How our jacket is made 🧵",
"x": {
"thread": [
"1/ It starts in the workshop…"
]
}
}
]
}),
});
const { data } = await res.json();import os, requests
res = requests.post(
"https://api.usecrier.com/v1/posts",
headers={"Authorization": f"Bearer {os.environ['CRIER_API_KEY']}"},
json={
"content": "How our jacket is made",
"media": [
{
"type": "video",
"url": "https://cdn.acme.com/jacket.mp4"
}
],
"targets": [
{
"accountId": "acc_ig",
"instagram": {
"format": "reel",
"firstComment": "#autumn #fashion"
}
},
{
"accountId": "acc_yt",
"youtube": {
"title": "How our jacket is made",
"visibility": "public"
}
},
{
"accountId": "acc_x",
"content": "How our jacket is made 🧵",
"x": {
"thread": [
"1/ It starts in the workshop…"
]
}
}
]
},
)
data = res.json()["data"]Statuses
| Status | Meaning |
|---|---|
draft | Draft, no date. |
pending_approval | Waiting for an owner or admin to approve it. It does not go out until then. |
scheduled | Scheduled or queued, waiting for its time. |
publishing | Being sent to the networks. |
published | Live everywhere. Each target has its url. |
partial | Live on some, failed on others. Retry the failed ones with POST /v1/posts/{id}/retry. |
failed | Live nowhere. Each target explains why in error. |
cancelled | Cancelled before going out, or taken down everywhere. |
Editing
While it is not live (draft, scheduled, failed or cancelled) you can change anything with PATCH /v1/posts/{id}. What you leave out stays; targets replaces the whole list.
curl -X PATCH "https://api.usecrier.com/v1/posts/pst_7q1w0e" \
-H "Authorization: Bearer $CRIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "We open Friday at 10",
"scheduledFor": "2026-10-30T10:00:00+01:00"
}'const res = await fetch('https://api.usecrier.com/v1/posts/pst_7q1w0e', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.CRIER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"content": "We open Friday at 10",
"scheduledFor": "2026-10-30T10:00:00+01:00"
}),
});
const { data } = await res.json();import os, requests
res = requests.patch(
"https://api.usecrier.com/v1/posts/pst_7q1w0e",
headers={"Authorization": f"Bearer {os.environ['CRIER_API_KEY']}"},
json={
"content": "We open Friday at 10",
"scheduledFor": "2026-10-30T10:00:00+01:00"
},
)
data = res.json()["data"]With draft: false a draft goes out: now, at scheduledFor or in the queue.
Once live, only the text can change, and only on networks that allow it: X (Premium accounts, within the first hour), Facebook, LinkedIn, YouTube, Pinterest, Google Business, Telegram, Discord and Reddit.
curl -X POST "https://api.usecrier.com/v1/posts/pst_7q1w0e/edit" \
-H "Authorization: Bearer $CRIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Corrected text"
}'const res = await fetch('https://api.usecrier.com/v1/posts/pst_7q1w0e/edit', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRIER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"content": "Corrected text"
}),
});
const { data } = await res.json();import os, requests
res = requests.post(
"https://api.usecrier.com/v1/posts/pst_7q1w0e/edit",
headers={"Authorization": f"Bearer {os.environ['CRIER_API_KEY']}"},
json={
"content": "Corrected text"
},
)
data = res.json()["data"]Approval
With requiresApproval: true the post stays in pending_approval: it does not go out until someone approves it. If the workspace requires approval (PATCH /v1/workspace with requireApproval: true, or from Team in the dashboard), members’ posts always go through it. Owners, admins and API keys skip it unless they ask for it.
curl -X POST "https://api.usecrier.com/v1/posts" \
-H "Authorization: Bearer $CRIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "New collection",
"scheduledFor": "2026-11-06T09:00:00+01:00",
"requiresApproval": true,
"targets": [
{
"accountId": "acc_ig"
}
]
}'const res = await fetch('https://api.usecrier.com/v1/posts', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRIER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"content": "New collection",
"scheduledFor": "2026-11-06T09:00:00+01:00",
"requiresApproval": true,
"targets": [
{
"accountId": "acc_ig"
}
]
}),
});
const { data } = await res.json();import os, requests
res = requests.post(
"https://api.usecrier.com/v1/posts",
headers={"Authorization": f"Bearer {os.environ['CRIER_API_KEY']}"},
json={
"content": "New collection",
"scheduledFor": "2026-11-06T09:00:00+01:00",
"requiresApproval": True,
"targets": [
{
"accountId": "acc_ig"
}
]
},
)
data = res.json()["data"]An owner, an admin or an API key decides with POST /v1/posts/{id}/review. Approved, it is scheduled for its date, goes to the queue if it asked for it, or goes out now if its date passed while it waited. Rejected, it goes back to draft with the note, and your webhook gets post.rejected.
curl -X POST "https://api.usecrier.com/v1/posts/pst_7q1w0e/review" \
-H "Authorization: Bearer $CRIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"decision": "reject",
"note": "Change the cover photo"
}'const res = await fetch('https://api.usecrier.com/v1/posts/pst_7q1w0e/review', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRIER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"decision": "reject",
"note": "Change the cover photo"
}),
});
const { data } = await res.json();import os, requests
res = requests.post(
"https://api.usecrier.com/v1/posts/pst_7q1w0e/review",
headers={"Authorization": f"Bearer {os.environ['CRIER_API_KEY']}"},
json={
"decision": "reject",
"note": "Change the cover photo"
},
)
data = res.json()["data"]Editing it while it waits does not publish it: it stays pending. Its author can withdraw it with POST /v1/posts/{id}/cancel.
Recycle
A post that works can go out again on its own: every N weeks or months, as a new post. It stops at maxCopies or after until, and variations are used in turn so the text is not always the same.
curl -X PUT "https://api.usecrier.com/v1/posts/pst_7q1w0e/recycling" \
-H "Authorization: Bearer $CRIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"every": 2,
"unit": "month",
"maxCopies": 6,
"variations": [
"The autumn classic is back 🍂",
"In case you missed it: our autumn classic"
]
}'const res = await fetch('https://api.usecrier.com/v1/posts/pst_7q1w0e/recycling', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.CRIER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"every": 2,
"unit": "month",
"maxCopies": 6,
"variations": [
"The autumn classic is back 🍂",
"In case you missed it: our autumn classic"
]
}),
});
const { data } = await res.json();import os, requests
res = requests.put(
"https://api.usecrier.com/v1/posts/pst_7q1w0e/recycling",
headers={"Authorization": f"Bearer {os.environ['CRIER_API_KEY']}"},
json={
"every": 2,
"unit": "month",
"maxCopies": 6,
"variations": [
"The autumn classic is back 🍂",
"In case you missed it: our autumn classic"
]
},
)
data = res.json()["data"]- Each copy is a post of its own with
recycledFrom, and your webhook getspost.recycled. - YouTube and TikTok do not allow repeated content: those accounts are left out of the copies.
- Each account can have up to 10 posts recycling at once.
DELETE /v1/posts/{id}/recyclingstops it; copies already created stay.
Metrics
GET /v1/posts/{id}/metrics returns impressions, reach, views, likes, comments, shares, saves and clicks, in total and per account. They arrive a few minutes after posting; until then it returns null. Each network reports its own: the rest are 0.
Account groups
Save account selections (for example "All of Acme") with POST /v1/account-groups and use them when importing a CSV or from the dashboard.
curl -X POST "https://api.usecrier.com/v1/account-groups" \
-H "Authorization: Bearer $CRIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "All of Acme",
"accountIds": [
"acc_ig",
"acc_li",
"acc_x"
]
}'const res = await fetch('https://api.usecrier.com/v1/account-groups', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRIER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "All of Acme",
"accountIds": [
"acc_ig",
"acc_li",
"acc_x"
]
}),
});
const { data } = await res.json();import os, requests
res = requests.post(
"https://api.usecrier.com/v1/account-groups",
headers={"Authorization": f"Bearer {os.environ['CRIER_API_KEY']}"},
json={
"name": "All of Acme",
"accountIds": [
"acc_ig",
"acc_li",
"acc_x"
]
},
)
data = res.json()["data"]Taking down and deleting
POST /v1/posts/{id}/unpublishtakes it down from every network, or only fromaccountId. Instagram and TikTok do not allow it through the API: delete it in the app.DELETE /v1/posts/{id}deletes it from Crier. If it is still live, take it down first.