Zernio
Zernio
Resources

Migrations

Migrate from AyrshareMigrate from KapsoMigrate from Twilio
Dashboard
llms.txtOpenAPI
OverviewPlatformsAPI ReferenceResources
Migrations

Migrate from Ayrshare

Move social posting, profiles, media and scheduled posts from Ayrshare to Zernio, with a drop-in SDK for the social-media-api package or a field mapping for raw HTTP calls.


When you finish this page your Ayrshare integration creates posts through Zernio. You need your Ayrshare API key and Profile Keys (to export scheduled posts), a Zernio account and an API key. Two paths:

PathForEffort
Drop-in SDKCode that uses the Ayrshare social-media-api npm package5 minutes
API migrationRaw HTTP calls or a custom client1 to 2 hours

What changes

WhatAyrshareZernio
Base URLapi.ayrshare.com/apizernio.com/api/v1
Content fieldpostcontent
Platforms["twitter", "facebook"][{platform, accountId}]
MediamediaUrls: ["url"]mediaItems: [{type, url}]
SchedulescheduleDatescheduledFor
Publish nowOmit scheduleDatepublishNow: true
Multi-userProfile-Key headerProfiles as resources, accounts addressed by accountId
Google Business Profilegmbgooglebusiness

A profile groups accounts, one per brand or per user (profiles guide); each connected account has an accountId that goes in platforms[].

Step 1: Swap the SDK (drop-in path)

If your code uses the Ayrshare social-media-api package, @zernio/social-media-api has the same method signatures:

npm uninstall social-media-api
npm install @zernio/social-media-api
- import SocialMediaAPI from 'social-media-api';
+ import SocialMediaAPI from '@zernio/social-media-api';

const social = new SocialMediaAPI(process.env.ZERNIO_API_KEY);

Existing calls (post, history, upload, createProfile and the rest) keep working:

CategoryMethods
Postspost, delete, getPost, retryPost, updatePost
Historyhistory
Useruser
ProfilescreateProfile, deleteProfile, updateProfile, getProfiles
Mediaupload, media, mediaUploadUrl, verifyMediaExists
AnalyticsanalyticsPost, analyticsSocial
CommentspostComment, getComments, deleteComments, replyComment
WebhooksregisterWebhook, unregisterWebhook, listWebhooks
SchedulingsetAutoSchedule, deleteAutoSchedule, listAutoSchedule
Reviewsreviews, review, replyReview, deleteReplyReview

setProfileKey scopes the following calls to one profile, the same way it did with Ayrshare; getProfiles() lists the keys. AI generation (generatePost, generateRewrite), RSS feeds, URL shortening and some media and analytics utilities return 501; the compatibility list names each one. Source and issues: github.com/zernio-dev/social-media-api. This path still needs Steps 2 and 3, a profile with the accounts connected behind it; what it lets you skip is the request rewriting in Steps 4 to 6, and Step 6 has the shortcut for the scheduled queue.

Step 2: Create a profile

Call POST /v1/profiles with a name, once per Ayrshare Profile Key:

curl -X POST https://zernio.com/api/v1/profiles \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "My Brand"}'

Response (201):

{
  "message": "Profile created successfully",
  "profile": {
    "_id": "66a1f0c2a4b9d3e8f1a2b3c4",
    "name": "My Brand",
    "isDefault": false
  }
}

profile._id is the profileId for Step 3.

Step 3: Connect the accounts

Call GET /v1/connect/{platform} with profileId for each platform you had in Ayrshare, and send the user to the returned authUrl:

curl "https://zernio.com/api/v1/connect/twitter?profileId=66a1f0c2a4b9d3e8f1a2b3c4&redirect_url=https://yourapp.com/callback" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"

Response (200):

{
  "authUrl": "https://twitter.com/i/oauth2/authorize?client_id=...",
  "state": "..."
}

Platform values: twitter instagram facebook linkedin tiktok youtube pinterest reddit bluesky threads googlebusiness telegram snapchat discord slack whatsapp. Ayrshare's gmb is googlebusiness here. The connecting accounts guide covers the platforms with a selection step and the ones without OAuth.

Step 4: Get the account ids

Call GET /v1/accounts with profileId:

curl "https://zernio.com/api/v1/accounts?profileId=66a1f0c2a4b9d3e8f1a2b3c4" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"

Response (200):

{
  "accounts": [
    {
      "_id": "66b2e19d8c3f5a7e9d0b1c2d",
      "platform": "twitter",
      "username": "@yourhandle",
      "displayName": "Your Name",
      "isActive": true
    }
  ]
}

Store the _id per platform in your database: it replaces the Profile Key in every post call.

Step 5: Change the post calls

Call POST /v1/posts with content and platforms[], each entry carrying the accountId from Step 4. Ayrshare's post and its list of platform names become content and one object per account:

curl -X POST https://zernio.com/api/v1/posts \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Hello world",
    "platforms": [
      {"platform": "twitter", "accountId": "66b2e19d8c3f5a7e9d0b1c2d"},
      {"platform": "linkedin", "accountId": "66b2e19d8c3f5a7e9d0b1c2e"}
    ],
    "publishNow": true
  }'

Response (201):

{
  "message": "Post published successfully",
  "post": {
    "_id": "65f1c0a9e2b5af0012ab34cd",
    "content": "Hello world",
    "status": "published",
    "platforms": [
      {
        "platform": "twitter",
        "status": "published",
        "platformPostUrl": "https://twitter.com/acmecorp/status/1852634789012345678"
      }
    ]
  }
}

Ayrshare's scheduleDate becomes scheduledFor plus a timezone, in place of publishNow:

{
  "scheduledFor": "2027-01-01T12:00:00",
  "timezone": "America/New_York"
}

publishNow defaults to false, so a post with scheduledFor is scheduled. Ayrshare's mediaUrls becomes mediaItems, where each item names its own type:

{
  "mediaItems": [
    {"type": "image", "url": "https://cdn.example.com/image.jpg"}
  ]
}

For files you host nowhere, request a presigned URL with POST /v1/media/presign, PUT the file to uploadUrl and use publicUrl in the post; the media uploads guide has the size limits and the accepted types. Ayrshare's post statuses map to status on the post (post lifecycle).

The same function in Node.js:

const createPost = async (content, accounts, media = [], scheduledFor = null) => {
  const response = await fetch('https://zernio.com/api/v1/posts', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.ZERNIO_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      content,
      platforms: accounts, // [{platform: "twitter", accountId: "..."}]
      mediaItems: media.map(url => ({
        type: url.match(/\.(mp4|mov|webm)$/i) ? 'video' : 'image',
        url
      })),
      ...(scheduledFor ? { scheduledFor, timezone: 'UTC' } : { publishNow: true })
    }),
  });

  const data = await response.json();
  return data.post._id;
};

Step 6: Move scheduled posts

Export the queue from Ayrshare with its GET /api/history (the Ayrshare API key plus the Profile-Key header).

On the drop-in SDK path there is nothing to remap: social.post() keeps Ayrshare's own field names, so call setProfileKey and replay each exported row through it, and skip the rest of this step. Calling the API directly, each row becomes one POST /v1/posts call, which needs the accountIds from Step 4:

Ayrshare history fieldZernio field
postcontent
platforms[], a list of namesone platforms[] entry per account: { platform, accountId } from Step 4
scheduleDatescheduledFor, with timezone alongside it
mediaUrls[]mediaItems[], each item naming its own type
const accountByPlatform = {
  twitter: '66b2e19d8c3f5a7e9d0b1c2d',
  linkedin: '66b2e19d8c3f5a7e9d0b1c2e',
};

// scheduledRows: the pending posts from Ayrshare's GET /api/history
for (const row of scheduledRows) {
  await fetch('https://zernio.com/api/v1/posts', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.ZERNIO_API_KEY}`,
      'Content-Type': 'application/json',
      'x-request-id': `ayrshare-${row.id}`, // same key on every retry of this row
    },
    body: JSON.stringify({
      content: row.post,
      platforms: row.platforms.map((p) => ({ platform: p, accountId: accountByPlatform[p] })),
      mediaItems: (row.mediaUrls ?? []).map((url) => ({
        type: /\.(mp4|mov|webm)$/i.test(url) ? 'video' : 'image',
        url,
      })),
      scheduledFor: row.scheduleDate,
      timezone: 'UTC',
    }),
  });
}

The x-request-id header makes a retried export idempotent, so a rerun replays the original response instead of creating a second post (idempotency). Delete or pause each row in Ayrshare only after its Zernio post exists: in that order a crash leaves a duplicate you can find and cancel, and in the other order it leaves a post nobody sends.

Step 7: Cut over

PhaseActions
PrepCreate profiles, connect accounts, ship the new calls behind a flag
PilotRun internal users on Zernio for a few days
RolloutEnable for 10%, then 50%, then 100% of users
CutoffDisable Ayrshare; keep its keys for 30 days as a fallback

If it fails

A 400 with Invalid accountId means a platforms[].accountId is not a Zernio account id, which happens when a Profile Key or an Ayrshare id is still in the mapping. Use the _id from GET /v1/accounts (Step 4). Other errors you will meet on the first calls:

ErrorCauseFix
Platform not supportedWrong platform valuegooglebusiness, not gmb
Media not foundThe URL is not reachableHTTPS and publicly accessible (media uploads)
Post not publishingWrong date formatISO 8601, scheduledFor read in timezone

Every error uses the envelope in error handling.

Related

  • Profiles and connecting accounts
  • Rate limits and idempotency
  • Webhooks replace polling history
  • Drop-in SDK source
  • Support: support@zernio.com
Was this page helpful?

Open Source

Generate a client from the Zernio OpenAPI spec, use one of the platform specs, or start from an open-source project built on the API.

Migrate from Kapso

Move a WhatsApp integration from Kapso to Zernio, keeping the WABA, templates and numbers, with a mapping for every send call and webhook event that changes.

On this page

What changesStep 1: Swap the SDK (drop-in path)Step 2: Create a profileStep 3: Connect the accountsStep 4: Get the account idsStep 5: Change the post callsStep 6: Move scheduled postsStep 7: Cut overIf it failsRelated