Most content APIs hand you a model and leave the hard parts — strategy, footage, captions, music licensing, scheduling — to you. This one hands you the finished artefact. Behind the endpoint is the same pipeline that runs the platform's own customers: a researcher that builds a content calendar from the brand's niche, a producer that renders 9:16 video, and a caption writer that emits a variant for all 11 platforms whether or not the brand has connected them.
The interface is deliberately small. Seventeen tools, one webhook event, one frozen package schema. Nothing expensive happens before payment: creating a brand writes a row and stops, so an agent that provisions a hundred brands and abandons them costs a hundred rows.
Today the server is deliver-only: we produce and hand over, and your agent publishes wherever it likes. Publish-through — approving a post and having us push it to the brand's connected accounts — is not yet exposed.
Quickstart
Every call below works against the live server. Replace the placeholders and run them in order; the whole funnel is nine calls.
-
1 Register
Unauthenticated. The key is returned in the response body and never sent by email — no email we send ever contains a credential.
curl -X POST https://api.myinfluenceai.com/mcp/register \ -H 'Content-Type: application/json' \ -d '{ "name": "Acme Agent", "contact_email": "[email protected]", "accepted_terms": true }' -
2 Verify the contact address
The key is live from the moment it is issued, but crippled: until you click the link we email, it reaches exactly three tools. Lost the mail?
POST /mcp/register/resendwith{"agent": "<slug>"}. -
3 Connect an MCP client
Streamable HTTP, stateless. Point any MCP-capable client — Claude, an SDK client, the MCP Inspector — at the endpoint with your key as a bearer token.
{ "mcpServers": { "myinfluenceai": { "type": "http", "url": "https://api.myinfluenceai.com/mcp", "headers": { "Authorization": "Bearer mi_live_<key_id>_<secret>" } } } } -
4 Find out what you hold
whoamianswers "what am I allowed to do" in one call — principal, scopes, rate limit, verification state and the brands this key can reach. Call it first whenever something is denied and you do not know why.platform_capabilitiesreturns the formats, pillars and caption platforms, so nothing has to be hardcoded. -
5 Analyse a website and pick a plan
website_analyzereads a public URL and proposes brand settings — niche, audience, tone, keywords.plan_listreturns the plans and how much they deliver per day. Both are read-only. -
6 Create the brand
brand_createwrites the brand row, grants it the MCP add-on and records the reseller consent — then stops. No research, no production, no spend. You must passaccepted_terms: truehaving actually read the linked documents on the brand's behalf. -
7 Send a human to checkout
checkout_createreturns a Stripe URL. An agent cannot pay. A person completes that checkout, Stripe calls us, and the brand activates. Until it does, every brand-scoped tool refuses withentitlement_required. -
8 Configure delivery and prove your verifier works
delivery_configuretakes your webhook URL and returns a signing secret once. Store it before you read the next line. Thendelivery_testsends a signed sample so you can confirm your HMAC check accepts a good signature — and, more importantly, rejects a bad one — before any real content depends on it. -
9 Receive the daily package
From then on, each produced item is POSTed to your endpoint as a
package.readyevent. If your endpoint was down,package_listandpackage_getreturn the same bytes, anddelivery_replayre-sends.
Authentication
Keys look like mi_live_<key_id>_<secret> and go in an Authorization: Bearer header. The secret half is shown once, at creation, and is stored only as a peppered hash — we cannot recover it for you, so a lost key is replaced, not retrieved.
Self-serve registration always grants the same thing: the developer tier and the provision preset. It ignores any tier, scope or brand grant you ask for. A self-serve key can never carry publish:write, outreach:send or pii:read — those take an irreversible or privacy-bearing action on someone's behalf, so granting them is a separate, deliberate act.
tools/list is filtered to what your key can actually call. A tool you would only ever be denied is not advertised, because it would waste your context and your calls.
Before your email is verified
Registration is unauthenticated, so a contact address is an unproven claim until someone opens the link we send. An unverified key reaches only the tools listed below; everything else returns email_verification_required. The key is issued before verification and crippled, rather than minted on click, so a live credential never has to travel through an inbox.
| Tool | What it gives you |
|---|---|
plan_list | List plans |
platform_capabilities | Platform capabilities |
whoami | Who am I |
Scopes
A key carries an explicit, literal list of scopes. Presets are a shortcut at minting time only — the stored key always holds the expanded list, so widening a preset later can never widen a key that already exists.
| Scope | Grants |
|---|---|
brands:read | Read brand profiles, schedules and pipeline status. |
content:read | Read hooks, strategy, calendar and produced content. |
content:write | Create or edit hooks, strategy and captions. |
produce:write | Spend the brand’s quota to research or produce content. |
analytics:read | Read post metrics and pillar performance. |
publish:write never self-serve | Approve content for publication to connected social accounts. |
social:read | List connected social accounts (names and platforms only). |
billing:write | Create checkout sessions and change plans. |
outreach:read | Read cold-outreach campaigns and prospects. |
outreach:send never self-serve | Send cold outreach on the brand’s behalf. |
pii:read never self-serve | Read end-customer personal data (emails, names). |
delivery:write | Configure webhook delivery and replay past deliveries. |
Presets
readonly—brands:read,content:read,analytics:read,social:readdeliver—brands:read,content:read,delivery:writeprovision—brands:read,content:read,billing:write,delivery:write
Self-serve registration always grants provision, and the scopes marked above are never included in any preset and never implied by another scope.
Tool reference
All 17 tools, generated from the running registry. Brand-scoped tools take a brand slug and require both the MCP add-on and an active subscription on that brand; UUIDs are refused. Writes change state and are audited.
brand_activate
Activate a brand
Idempotent. Normally fired automatically by the Stripe webhook — call it only if payment completed but the brand is still inactive. Safe to retry.
Input schema
{
"type": "object",
"properties": {
"brand": {
"type": "string",
"description": "Brand slug."
}
},
"required": [
"brand"
],
"additionalProperties": false
}
brand_create
Create a brand
Create a brand owned by this agent. All work — publishing profile, strategy, first production — is DEFERRED until payment completes, so this call is cheap and reversible. Requires accepted_terms: you accept as a reseller on the brand’s behalf, and that assertion is recorded against your agent id. Follow with checkout_create.
Input schema
{
"type": "object",
"properties": {
"brand_name": {
"type": "string",
"maxLength": 120,
"description": "Display name. The slug is derived from it unless you pass one."
},
"slug": {
"type": "string",
"maxLength": 63,
"pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$",
"description": "Optional explicit slug."
},
"website": {
"type": "string",
"maxLength": 2000
},
"niche": {
"type": "string",
"maxLength": 500
},
"tone": {
"type": "string",
"maxLength": 200
},
"plan": {
"type": "string",
"maxLength": 40,
"description": "Intended plan id; used by checkout_create."
},
"owner_email": {
"type": "string",
"maxLength": 320,
"description": "Optional. The end brand’s email, for the claim invite and consent record."
},
"accepted_terms": {
"type": "boolean",
"description": "Must be true. You are asserting authority to accept on the brand’s behalf."
}
},
"required": [
"brand_name",
"accepted_terms"
],
"additionalProperties": false
}
brand_get
Get a brand
Full profile for one brand: niche, tone, goal, keywords, pillar ratios, palette, schedule and subscription state.
Input schema
{
"type": "object",
"properties": {
"brand": {
"type": "string",
"description": "Brand slug."
}
},
"required": [
"brand"
],
"additionalProperties": false
}
brand_list
List brands
Every brand this key can reach, with plan and subscription status.
Input schema
{
"type": "object",
"properties": {},
"additionalProperties": false
}
checkout_create
Create a checkout session
Return a Stripe Checkout URL for a brand. PASSTHROUGH billing: the end brand pays directly — hand them the URL. On payment our webhook activates the brand automatically.
Input schema
{
"type": "object",
"properties": {
"brand": {
"type": "string",
"description": "Brand slug."
},
"plan": {
"type": "string",
"maxLength": 40,
"description": "Plan id from plan_list. Defaults to the brand’s current plan."
},
"success_url": {
"type": "string",
"maxLength": 2000
},
"cancel_url": {
"type": "string",
"maxLength": 2000
}
},
"required": [
"brand"
],
"additionalProperties": false
}
delivery_configure
Configure delivery
Set or update where and how this brand’s daily content package is delivered. Returns the signing secret ONCE, on first configuration or when rotate_secret is true — store it, it cannot be read back. telegram_enabled is FALSE by default: an agent-provisioned brand is webhook-only unless you deliberately turn the Telegram approval card back on, in which case BOTH fire.
Input schema
{
"type": "object",
"properties": {
"brand": {
"type": "string",
"description": "Brand slug."
},
"webhook_url": {
"type": "string",
"maxLength": 2000,
"description": "Public https:// endpoint. Private and loopback addresses are refused."
},
"mode": {
"type": "string",
"enum": [
"deliver_only",
"publish_through"
],
"description": "deliver_only is the Phase 1 product. publish_through is not yet available."
},
"platforms": {
"type": "array",
"description": "Caption variants to include. Omit for all 11."
},
"telegram_enabled": {
"type": "boolean",
"description": "Also send the Telegram approval card. Default false."
},
"paused": {
"type": "boolean",
"description": "Stop delivering without losing configuration."
},
"rotate_secret": {
"type": "boolean",
"description": "Mint a new signing secret. Breaks existing verification until you redeploy."
}
},
"required": [
"brand"
],
"additionalProperties": false
}
delivery_replay
Replay a delivery
Re-queue a past delivery. KEEPS THE ORIGINAL idempotency key, so if the first attempt did reach you, your own deduplication recognises the duplicate and drops it.
Input schema
{
"type": "object",
"properties": {
"brand": {
"type": "string",
"description": "Brand slug."
},
"delivery_id": {
"type": "string",
"maxLength": 40,
"description": "From package_list."
}
},
"required": [
"brand",
"delivery_id"
],
"additionalProperties": false
}
delivery_test
Send a test delivery
Queue a signed sample payload to the configured webhook so you can verify your HMAC check before real content depends on it. Verify X-MI-Signature as t=<unix>,v1=<hex hmac-sha256 of "<t>.<raw body>"> using your signing secret, with 5 minutes of clock tolerance.
Input schema
{
"type": "object",
"properties": {
"brand": {
"type": "string",
"description": "Brand slug."
}
},
"required": [
"brand"
],
"additionalProperties": false
}
job_get
Get a job
Status of one queued job by id. Scoped to the brand, so a job id from another tenant is simply not found.
Input schema
{
"type": "object",
"properties": {
"brand": {
"type": "string",
"description": "Brand slug."
},
"job_id": {
"type": "integer",
"description": "Job id returned by a mutating tool."
}
},
"required": [
"brand",
"job_id"
],
"additionalProperties": false
}
package_get
Get a content package
The full content package for one request_id — media URLs, all caption variants, and the music credit. Returns exactly what was PUSHED if it was delivered, so a pull and a push can never disagree. Works even for content produced before delivery was configured.
Input schema
{
"type": "object",
"properties": {
"brand": {
"type": "string",
"description": "Brand slug."
},
"request_id": {
"type": "string",
"maxLength": 120,
"description": "From a package.ready payload or pipeline_status."
}
},
"required": [
"brand",
"request_id"
],
"additionalProperties": false
}
package_list
List deliveries
Delivery attempts for a brand, newest first. THE CATCH-UP TOOL: after an outage, list with status "failed" or "dead" and replay what you missed.
Input schema
{
"type": "object",
"properties": {
"brand": {
"type": "string",
"description": "Brand slug."
},
"status": {
"type": "string",
"enum": [
"pending",
"sending",
"delivered",
"failed",
"dead"
]
},
"since": {
"type": "string",
"maxLength": 40,
"description": "ISO 8601 timestamp."
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"description": "Default 20."
},
"cursor": {
"type": "string",
"maxLength": 40,
"description": "next_cursor from a previous page."
}
},
"required": [
"brand"
],
"additionalProperties": false
}
pipeline_status
Pipeline status
Live production state for a brand: queued and recent jobs, video counts by status, unused hooks (the real runway figure), and the five most recent items.
Input schema
{
"type": "object",
"properties": {
"brand": {
"type": "string",
"description": "Brand slug."
}
},
"required": [
"brand"
],
"additionalProperties": false
}
plan_list
List plans
Available subscription plans with price and the DAILY DELIVERABLE VOLUME each one buys (videos_per_day, carousels_per_day, posts_per_day) — the numbers that decide how many packages a webhook will receive.
Input schema
{
"type": "object",
"properties": {},
"additionalProperties": false
}
platform_capabilities
Platform capabilities
What this platform can produce: content formats, content pillars, the caption platforms generated for every post, video statuses, and the available scopes. Read this once at integration time instead of hardcoding enums.
Input schema
{
"type": "object",
"properties": {},
"additionalProperties": false
}
subscription_status
Subscription status
Plan, status, renewal date and add-ons for a brand. Check this before assuming packages will keep arriving — delivery stops when a subscription is cancelled.
Input schema
{
"type": "object",
"properties": {
"brand": {
"type": "string",
"description": "Brand slug."
}
},
"required": [
"brand"
],
"additionalProperties": false
}
website_analyze
Analyze a website
Read a public website and return suggested brand settings (name, industry, tone, goal, audience, keywords) plus scraped brand colours. Step one of onboarding: feed the result into brand_create. Results are cached, so re-analysing the same URL is free.
Input schema
{
"type": "object",
"properties": {
"website": {
"type": "string",
"maxLength": 2000,
"description": "Public https:// URL of the brand’s site."
}
},
"required": [
"website"
],
"additionalProperties": false
}
whoami
Who am I
Identify the calling key: principal, scopes, rate limit, and the brands it can reach. Call this first when debugging a permission problem — it answers "what am I allowed to do" without guessing.
Input schema
{
"type": "object",
"properties": {},
"additionalProperties": false
}
Errors
A tool failure comes back as a normal result marked isError, not a protocol fault, so your agent can read it and correct itself. Every error carries a code, a message and a next_step written for a program rather than a person.
| Code | Meaning | Next step |
|---|---|---|
auth_required | Missing or malformed Authorization header. | Send `Authorization: Bearer mi_live_<key_id>_<secret>`. No key yet? POST /mcp/register, or read https://agents.myinfluenceai.com/developers/mcp/. |
key_invalid | That API key is not valid. | Check the key was copied whole. Secrets are shown once at creation and cannot be recovered. See https://agents.myinfluenceai.com/developers/mcp/. |
key_revoked | That API key has been revoked. | Mint a new key from the developer console. |
key_expired | That API key has expired. | Mint a new key from the developer console. |
agent_disabled | This agent account is disabled. | Contact [email protected]. |
email_verification_required | Confirm your contact email before using this tool. | Open the link we emailed when you registered. Lost it? POST /mcp/register/resend {"agent":"<slug>"}. See https://agents.myinfluenceai.com/developers/mcp/. |
scope_required | This key does not carry the scope required for that call. | Mint a key with the scope listed in `required_scope`. |
brand_not_found | No brand with that identifier is available to this key. | Call `brand_list` to see the brands this key can reach. |
brand_not_authorized | No brand with that identifier is available to this key. | Call `brand_list` to see the brands this key can reach. |
entitlement_required | The MCP add-on is not enabled for this brand. | Complete checkout for this brand, or contact support to enable the add-on. |
rate_limited | Too many calls. Slow down. | Wait `retry_after_seconds` and retry. Back off exponentially on repeats. |
quota_exceeded | This brand has reached its plan limit for that action. | Upgrade the plan with `plan_change`, or wait for the limit window to roll. |
quota_unavailable | Usage limits are temporarily unavailable, so the call was refused rather than allowed. | Retry in a minute. This is a transient server condition, not a limit you hit. |
invalid_input | One or more arguments were rejected. | Read `details` for the offending field, fix it, and retry. |
unsafe_url | That URL points somewhere we will not fetch or deliver to. | Supply a public https:// URL. Private, loopback and link-local addresses are refused. |
consent_required | Terms must be accepted before a brand can be created. | Read the documents in `legal_urls`, then retry with `accepted_terms: true`. |
not_found | No such record. | Check the identifier. List endpoints return the ids this key can use. |
conflict | That action conflicts with the current state. | Read `state` and retry only if it makes sense to. |
not_configured | Delivery is not configured for this brand. | Call `delivery_configure` with a webhook_url first. |
internal_error | Something went wrong on our side. | Retry once. If it persists, contact [email protected] with the `request_id`. |
brand_not_found and brand_not_authorized return the same message on purpose: a key must not be able to discover which brands exist by probing.
Webhook delivery
Every delivery carries an X-MI-Signature header in the form t=<unix>,v1=<hex>, where v1 is an HMAC-SHA256 of the literal string "<t>.<raw body>" under your signing secret. The timestamp is inside the signed value, so a captured body cannot be replayed with a fresh timestamp.
Three things a correct verifier does, and a plausible one does not: hash the raw request body (not a re-serialised object — key order changes the bytes), compare with a constant-time comparison, and reject anything outside a ±5 minute window. A verifier that skips the timestamp check silently accepts forged calls forever.
We accept any 2xx as delivered. Anything else retries on a fixed schedule and then stops. Respond fast and process asynchronously: a slow endpoint is indistinguishable from a broken one.
Headers
POST /your/webhook HTTP/1.1
Content-Type: application/json
X-MI-Event: package.ready
X-MI-Signature: t=1756512000,v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
Verify it — Node
import crypto from 'crypto';
// body must be the RAW request bytes, not a re-serialised object:
// JSON.stringify(JSON.parse(body)) can reorder keys and change the hash.
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
String(header || '').split(',').map(kv => kv.split('=').map(s => s.trim())),
);
const t = parseInt(parts.t, 10);
if (!Number.isFinite(t)) return false;
// Without this, a captured body replays forever.
if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;
const expected = crypto.createHmac('sha256', secret)
.update(`${t}.${rawBody}`, 'utf8').digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(String(parts.v1 || ''), 'hex');
if (a.length !== b.length || a.length === 0) return false;
return crypto.timingSafeEqual(a, b);
}
Verify it — Python
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
parts = dict(
kv.strip().split("=", 1) for kv in (header or "").split(",") if "=" in kv
)
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
# Without this, a captured body replays forever.
if abs(int(time.time()) - t) > tolerance_seconds:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Retries
A non-2xx response (or a timeout) is retried 6 times in total, then marked dead:
1m → 5m → 30m → 2h → 6h → 24h → dead
That is roughly 33 hours of tolerance for an endpoint that is down. A dead delivery is not lost — package_list still returns it and delivery_replay re-sends it.
The daily package
One frozen shape, assembled once and used identically by the webhook push and the package_get pull — so a consumer catching up after an outage gets exactly what it normally receives.
The one thing consumers get wrong: media.kind is "video" or "carousel". For a carousel, video_url is null and the ordered slide URLs are in media.slides. Branch on kind; a downloader that assumes a single URL breaks on the first carousel day.
Music credit is not optional. When music.credit_required is true, the ready-made music.credit_line must appear in the caption you publish — not in file metadata. Instagram, TikTok and YouTube all re-encode uploads and strip container tags, so metadata attribution is not attribution. Nearly every track in the library is CC BY.
Media URLs expire. expires_at is on every package: fetch and store the bytes, do not hotlink.
Captions
A variant is generated for all 11 platforms on every post, regardless of what the brand has connected, so a deliver-only consumer can publish anywhere: linkedin, tiktok, x, reddit, instagram, facebook, youtube, bluesky, pinterest, threads, mastodon. Narrow the set with the platforms argument to delivery_configure. A platform with no stored caption is absent rather than null, so "not generated" is distinguishable from "generated empty".
Shape
A real payload, assembled by the same code that builds yours. This one was configured for three platforms so the sample stays readable; by default captions holds all 11.
{
"package_version": "1",
"brand": "acme-dental",
"request_id": "req_9f4c21",
"date": "2026-08-30",
"format": "talking_points",
"pillar": "educational",
"hook": "Three things every new patient asks — answered in 30 seconds.",
"media": {
"kind": "video",
"video_url": "https://cdn.my-influence-ai.com/v/req_9f4c21.mp4",
"slides": null,
"thumbnail_url": "https://cdn.my-influence-ai.com/v/req_9f4c21.jpg",
"duration_seconds": 28
},
"captions": {
"linkedin": {
"text": "Patients ask the same three questions before every first appointment. Here is how we answer them.",
"hashtags": []
},
"tiktok": {
"text": "The 3 questions we get every single day",
"hashtags": [
"#dentaltok",
"#patienttips"
]
},
"instagram": {
"text": "Three things every new patient asks. Save this for later 🦷",
"hashtags": [
"#dentalcare",
"#patienttips"
]
}
},
"default_caption": "Three things every new patient asks. Save this for later.",
"music": {
"title": "Sunlit",
"artist": "Kirkoid",
"license": "CC BY 3.0",
"license_url": "https://creativecommons.org/licenses/by/3.0/",
"credit_required": true,
"credit_line": "🎵 Music: \"Sunlit\" by Kirkoid (ccMixter, CC BY 3.0)"
},
"expires_at": "2026-09-29T06:00:00.000Z"
}
Rate limits and quotas
Rate limits are per key and reported by whoami; a rate_limited error carries retry_after_seconds. Back off exponentially — a retry loop against a limit is the fastest way to get a key disabled.
Plan quotas are enforced per brand and fail closed: if we cannot determine your remaining quota, the call is refused rather than allowed. That is deliberate for an automated caller, and it is why quota_unavailable is a distinct code from quota_exceeded — the first is transient and worth retrying, the second is not.
Every response carries a request_id. Quote it when you contact support; it is how we find your call.
Versioning
The tool schemas and the package schema are frozen contracts, checked against a fixture in CI — an accidental change to either fails our build, not yours.
Additive fields do not bump a version. A breaking change mounts a new path (/mcp/v2); /mcp never silently becomes v2. Deprecations run 90 days, are flagged on the affected responses, and are emailed to the owners of affected keys.
Machine-readable discovery lives at GET https://api.myinfluenceai.com/mcp/info and https://agents.myinfluenceai.com/.well-known/mcp/server.json; both are unauthenticated and always describe the deployed server.
Terms
Creating a brand means accepting these on that brand's behalf. Read them before you pass accepted_terms: true.
Frequently asked questions
Does Agents by MyInfluence AI have an MCP server?
Yes. It is hosted at https://api.myinfluenceai.com/mcp, speaks Streamable HTTP, and authenticates with a bearer API key. Registration is self-serve at POST /mcp/register and returns a key immediately. There is nothing to install and no local process to run.
How does an AI agent subscribe programmatically?
Register for a key, verify the contact email, then call website_analyze, plan_list and brand_create to provision a brand, and checkout_create to get a Stripe URL. A human completes that checkout — an agent cannot pay on its own. Once Stripe confirms, the brand activates and daily delivery begins.
What is in a daily content package?
A rendered 9:16 short-form video (or an ordered set of carousel slides), a thumbnail, the hook and content pillar, a caption variant for each of 11 platforms, a default caption, and a music block with a ready-made credit line that must be published in the caption. Media URLs carry an explicit expiry.
How do I verify the webhook signature?
Compute an HMAC-SHA256 over the string "<timestamp>.<raw request body>" using your signing secret, and compare it in constant time to the v1 value in the X-MI-Signature header. Reject any request whose t is more than five minutes from now. Prove it works with delivery_test before real content depends on it.
What does it cost?
The same plans humans buy: from $149/month. The agent-facing interface itself carries no separate fee — you pay per brand, per plan, and the brand's human owner completes the checkout.
Can an agent publish to social accounts through the API?
Not today. The server is deliver-only: it hands over the finished content and your agent publishes it wherever it likes. Publish-through exists in the product for human-approved posts, but it is not exposed to agents, and no self-serve key can be granted the publish:write scope.
Can an agent create brands for other people's businesses?
Yes, and that is the intended reseller pattern — but brand_create records who accepted the terms and on whose behalf, and the human who pays is the one who owns the account. Creating brands without a real customer behind them just creates rows that never activate.