Voholabs

    Voholabs Studio public API

    The Voholabs Studio public API is a REST API at https://studio.voholabs.com/api/public/v1, described in OpenAPI at /api/public/v1/openapi.json. Send your API key from Settings › Connect Agent in the Authorization header, with no "Bearer" prefix, to manage channels, media, posts, analytics, the brief, skills and your wallet.

    Last updated View as Markdown

    How do I authenticate?#

    Every request carries your API key in the Authorization header:

    Terminal
    curl https://studio.voholabs.com/api/public/v1/is-connected \
      -H "Authorization: YOUR_API_KEY"

    Find the key in Settings › Connect Agent, under API Key. The same key powers your agent connectors and the CLI. If you are building a product that posts for other Studio users, use an OAuth app instead: its pos_ tokens go in the same header.

    A missing or wrong key returns HTTP 401 with "No API Key found", "Invalid API key" or "Invalid OAuth token".

    How do I rotate my API key?#

    In Settings › Connect Agent, under API Key, choose Rotate Key. The old key stops working at once, including for agents you connected with a connector link, so update every integration with the new key.

    What can the API do?#

    All paths are relative to https://studio.voholabs.com/api/public/v1.

    AreaEndpointWhat it does
    AccountGET /is-connectedChecks that your key works.
    ChannelsGET /integrationsLists connected channels. Add ?group= to list one customer's channels.
    ChannelsGET /groupsLists customers, the labels that group channels per client.
    ChannelsGET /integration-settings/:idA channel's rules, maximum length and settings schema.
    ChannelsPOST /integration-trigger/:idRuns a channel helper, such as listing a Discord server's channels.
    ChannelsGET /social/:integrationReturns the address that starts connecting a new channel.
    ChannelsDELETE /integrations/:idRemoves a channel.
    SchedulingGET /find-slot/:idThe next free time slot for a channel.
    PostsGET /postsPosts between startDate and endDate, optionally for one customer.
    PostsPOST /postsCreates, schedules or publishes posts.
    PostsPUT /posts/:id/statusMoves a post between draft and schedule.
    PostsDELETE /posts/:idDeletes a post and the rest of its group.
    PostsDELETE /posts/group/:groupDeletes every post in a group.
    PostsGET /posts/:id/missingRecent content from the network, to match a post Studio lost track of.
    PostsPUT /posts/:id/release-idLinks a post to its published version on the network.
    MediaPOST /uploadUploads a file as multipart form data, field file.
    MediaPOST /upload-from-urlImports a file from a public address.
    MediaGET /mediaLists the media library, with page and search.
    MediaDELETE /media/:idDeletes a file from the library.
    MediaPOST /upload-ticketCreates a single-use upload address, valid for 15 minutes, that needs no key.
    AnalyticsGET /analytics/:integrationA channel's analytics for a date range. Add fresh=true to read the network again instead of the cache; the X-Cached-At header says when the numbers were read.
    AnalyticsGET /analytics/post/:postIdOne post's analytics.
    NotificationsGET /notificationsYour workspace's notifications, by page.
    BriefGET /brief/schemaThe brief's sections and the documents you can write to.
    BriefGET /briefEvery document in the brief.
    BriefGET /brief/:category/:keyOne brief document.
    BriefPATCH /brief/:category/:keyWrites one brief document.
    BriefDELETE /brief/:category/:keyDeletes a brief document. Add keepHistory=true to keep its revisions.
    BriefGET /brief/onboardingThe guided onboarding's status: whether it is available, open or finished, whether a new run is charged, and the Studio address where you start it.
    BriefPOST /brief-upload/:tokenUploads a brand file to the brief, with a ticket from POST /upload-ticket and "purpose": "brief".
    SkillsGET /skillsThe skills library catalogue, with optional tag and search. Returns summaries, not the methods.
    SkillsGET /skills/:slugOne skill in full, with the steps an agent follows.
    WalletGET /walletYour credit balance, pay-as-you-go state, auto top-up settings and whether upcoming scheduled posts are covered.
    WalletGET /wallet/pricesThe price list: what costs credits and what is included free. Add provider to see one network.
    WalletGET /wallet/transactionsTop-ups, charges, refunds and grants, newest first, with page, size and type.
    WalletPOST /wallet/estimateWhat a post would cost on a channel before you schedule it. Send provider and contents, the post and each reply. Nothing is charged.

    Uploads accept JPEG, PNG, GIF, WebP, AVIF, BMP and TIFF images up to 10 MB, and MP4 video up to 1 GB.

    The brief and skills endpoints are open on paid plans, and on the free plan after your first wallet top-up; before that they return HTTP 402. The wallet endpoints are read-only: topping up stays in the app. On a paid plan they answer with "usesWallet": false, because paid plans do not use credits.

    Is there an OpenAPI description?#

    Yes. The API is described in OpenAPI 3.1 at:

    Text
    https://studio.voholabs.com/api/public/v1/openapi.json

    It needs no key. Load it into Postman, Insomnia or a code generator, or give it to an AI agent so it can call the API correctly. It covers every endpoint on this page, with request bodies, responses and error shapes. The per-network settings for posts are listed in Channel settings in the API.

    How do I schedule a post?#

    Send POST /posts with the channel ID from GET /integrations:

    Terminal
    curl https://studio.voholabs.com/api/public/v1/posts \
      -H "Authorization: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "schedule",
        "date": "2026-11-02T09:00:00.000Z",
        "shortLink": false,
        "tags": [],
        "posts": [
          {
            "integration": { "id": "YOUR_CHANNEL_ID" },
            "value": [{ "content": "Hello from the Voholabs Studio API", "image": [] }],
            "settings": { "__type": "linkedin" }
          }
        ]
      }'
    • type is schedule, draft, now or update.
    • date is an ISO 8601 time, required even for drafts.
    • value holds the parts of the post. Extra entries become follow-up parts, such as a thread or comments.
    • image takes media you uploaded first, as { "id": "...", "path": "..." }.
    • settings holds the channel's own options. GET /integration-settings/:id returns the schema for each channel, and Channel settings in the API lists the fields for every network.

    A post with no text and no media returns "Your post should have at least one character or one image." Text over the channel's limit returns "post is too long, please fix it".

    Are there rate limits?#

    Creating posts with POST /posts is limited per workspace per hour. Over the limit, the API returns HTTP 429; wait and retry. Other endpoints are covered by fair use.

    What happens when an X post needs credits?#

    On the free plan, X posts are charged when they are scheduled. To check the cost first, send the post's text to POST /wallet/estimate. If your balance is short and auto top-up cannot cover it, POST /posts returns HTTP 402 with "wallet": true and a url pointing at the wallet in the Studio app. Top up, then retry. See Credits and wallet.

    Can Studio call my server when a post is published?#

    Yes, with a webhook. Add one in Settings › Webhooks and Studio sends a POST with the published post's details to your address each time a post goes out. See Webhooks for the payload.

    Frequently asked questions

    Is the API free?

    Yes. The public API is part of the free plan. Actions that are pay-per-use in the app, such as posting to X on the free plan, are charged the same way through the API.

    Should I send "Bearer" before my key?

    Not for the REST API. Send the key on its own, Authorization: YOUR_API_KEY. The MCP server is different and expects Bearer.

    Is there a quicker way to build the body for POST /posts?

    Yes. In Settings › Connect Agent, choose Open Wizard, build the post in the normal composer, and copy the JSON payload it generates.