Encola un plan. No devuelve publicaciones: devuelve el plan en pending y el
presupuesto que se calculó para aceptarlo.
Falla ANTES de gastar nada si no hay créditos para el coste imprescindible (941), si no queda cupo de publicaciones (924), si alguna cuenta no publica —WhatsApp, Google Business— (942), o si con los días elegidos no queda ningún hueco futuro en la semana (2108).
const { ai_plan, estimate } = await pv.aiPlans.create(clientId, orgId, {
prompt: "Pan de masa madre, horno de leña, barrio",
accounts: [accountId],
options: { publish_days: [1, 3, 5], timezone: "Europe/Madrid" },
});
Con plantilla van además template y su source, que se valida aquí: la URL se descarga
y el catálogo se lee dentro de esta llamada, así que sus errores llegan mientras el usuario
sigue delante — 2111 (plantilla que no existe), 2112 (fuente que no cuadra con la plantilla),
2113 (la URL no se pudo leer), 2114 (la URL apunta a una dirección no pública), 2115 (la
cuenta no tiene un catálogo utilizable) y 2116 (la fuente se quedó sin unidades).
await pv.aiPlans.create(clientId, orgId, {
prompt: "Nuestra carta de otoño",
accounts: [accountId],
template: "from_images",
source: {
images: [
{ id_upload: primera, description: "Masa reposando en el banco" },
{ id_upload: segunda, description: "La hogaza saliendo del horno" },
],
},
});
El orden de images y de products es la historia: el orquestador se queda con la
posición de cada uno, así que la foto 3 puede ser el "antes" y la 7 el "después".
Con Pinterest en el plan, el tablero de cada cuenta va en destinations y es obligatorio
(2118, con todas las cuentas que fallan). Y un plan que dejaría pins sin imagen se rechaza
aquí con el 2119, antes de gastar un crédito. Ver AiPlanCreateRequest.
await pv.aiPlans.create(clientId, orgId, {
prompt: "Recetas de otoño",
accounts: [pinterestId, instagramId],
destinations: [{ id_account: pinterestId, destination: { id: board.id, name: board.name } }],
options: { link: "https://panaderia.example/otono" },
});
Optionaldestinations?: {Where each account publishes, for the networks that answer destinations: true in GET /social_capabilities — today pinterest alone, where it is the board of every pin of that account.
**On Pinterest it is required, one entry per Pinterest account of the plan.** A plan without it — or with a board id that is not a string of digits — is rejected with **2118**, listing EVERY account that fails in `data.accounts[]` with the same `reason` as the publication's 987 (`missing`, `invalid_id`, `invalid_section_id`), so your UI can mark them all at once. Without this check the plan would be created, charged, and leave a week of drafts in `withErrors` with the 987.
Entries for accounts on networks without destinations are dropped, not rejected. The board is **not** checked against Pinterest at creation — that would make creating a plan depend on Pinterest answering; a board that does not exist fails when the pin is published. Read the account's boards with `GET /organizations/{id_organization}/accounts/{id_account}/destinations`.
Optionaloptions?: {Optionalallow_images?: booleanOptionalgallery_uploads?: string[]Upload ids from the organization's gallery used as visual reference for the generated images. They are references for the images the model GENERATES, so a template that does not generate them does not accept them either (allows_gallery): sending them to from_images or from_catalog is a 2106.
Optionallanguage?: stringOptionallink?: stringThe destination link of the plan's publications — the pin's link, the same for every publication of the plan. Optional: a pin without a link is legitimate, it just takes nobody anywhere. On from_catalog, each pin leads to its product's page (permalink) when this is absent; when it is present, it wins.
It only reaches the publications of the networks that answer `link: true` in `GET /social_capabilities` (today `pinterest`); with none of them in the plan it is dropped. With one, it is validated **at creation**: a URL Pinterest would reject is a **994** now (`data.reason` `invalid_url` or `too_long`), not a week of pins failing at publish time.
Optionalmax_images?: numberOptionalpublish_days?: number[]Days of the week the plan publishes on, in ISO 8601 numbering (1 = Monday ... 7 = Sunday). Defaults to the whole week. There is still at most ONE publication per day and account, so this is what bounds the size and the cost of the plan: the number of generated posts is (selected days x accounts). Must be a non-empty array of unique integers between 1 and 7, or the request is rejected with 2106. The 7-day window starts at week_start, so each ISO day appears exactly once: with a week_start in mid-week, day 1 (Monday) is the FOLLOWING Monday. If the selected days leave no future slot at all, the request is rejected with 2108. Optional; defaults to [1,2,3,4,5,6,7].
Optionalshared?: booleanGenerate ONE piece of content per day and replicate it across every account, each scheduled at the best hour for ITS network, instead of one publication per account and day. Cheaper — one text and one image per day — and it caps images at 7. Optional; defaults to false. Not every template accepts it (allows_shared in GET /planner_templates): from_images and from_catalog do not, because each of their publications carries a photo of its own, and sending true to them is rejected with 2106.
Optionaltimezone?: stringOptionaltone?: stringOptionaluse_organization_context?: booleanOptionalweek_start?: stringFormat: date-time
Optionalsource?: {Optionalevent_date?: stringFormat: date
campaign. The day of the event. Send a calendar day, YYYY-MM-DD — never an ISO instant. A bare date is read in the plan's options.timezone, which is the whole point: 2026-09-15T00:00:00Z is midnight UTC, that is the 14th in the afternoon in New York — a whole day off in a countdown, for half of America, with no error anywhere. It cannot fall before the plan week (options.week_start) nor more than 60 days after it (2112). And it does not move publish_days: if the event lands on a day you did not choose, the plan respects your choice and it is up to you to say so.
Optionalevent_name?: stringOptionalid_account_catalog?: stringfrom_catalog, from a Meta catalogue. Which account's catalogue the products come from. It has to belong to the organization (2103) and be on a network that supports products (2115). It only chooses the catalogue: the publications still go to every account in accounts — a LinkedIn account can publish a product from a Facebook catalogue. Exclusive with id_integration_catalog (both at once is a 2112).
Optionalid_integration_catalog?: stringfrom_catalog, from a connected store. An integration of the organization whose provider has catalog: true (a WooCommerce store), enabled; another organization's is a 2200, without confirming it exists. The products are its external_ids, from GET .../integrations/{id_integration}/products. Like the account, it only chooses the catalogue: the publications go to every account in accounts. Exclusive with id_account_catalog.
Optionalimages?: { description: string; id_upload: string }[]from_images. Your own photos, each with its own description, in the order that tells the story: the orchestrator picks one per publication and keeps its position in source_index, so photo 3 can be the "before" and photo 7 the "after". Up to 20 (max_source_items), and each one has to be an image upload of this organization (806 otherwise).
Optionalproduct_catalog_id?: stringOptionalproducts?: string[]from_catalog. Ids of the chosen products, in the order they should tell the week. Up to 12 — a unit here is not an id already in the database: it is a live read plus a real download inside this request, with the user waiting. Repeated ids are deduplicated. They are ALL checked against the catalogue before a single picture is downloaded (2112 naming the missing one, or the one that is out of stock), and each picture is copied into an upload of the organization: a catalogue's picture URL expires or changes, and a plan published weeks later would carry a broken file. Those uploads count against the storage quota.
Optionaltext?: stringfrom_text. The article pasted by hand. It wins over url when both come: pasting is what a user does when the download did not work (a paywall, a page that needs JavaScript), so re-downloading to ignore what they wrote would take away their only way out. Truncated to 12.000 characters — the cap is what keeps the estimate honest, since the real charge is per use. Under 200 characters it is not an article, it is the theme, which is prompt and another field (2116).
Optionalurl?: stringFormat: uri
from_text. The article the plan is written from. It is downloaded at creation, and only http/https addresses that resolve to a public IP are accepted — checked again before every redirect (2114). A page that answers something that is not text or HTML, or that carries no usable text once stripped, is a 2113: paste the text in text instead.
Optionaltemplate?: "standard" | "from_images" | "from_text" | "from_catalog" | "campaign"What the plan is generated FROM. Optional; defaults to standard, which is exactly what every plan did before templates existed — send nothing and nothing changes.
A template is the **source** of the content, not a different flow: `shared`, `publish_days`, `language`, `tone` and the images stay cross-cutting options, and each template declares which of them it accepts. Sending one it does not accept is a 2106, not a silent ignore.
Read the list, the costs and the fields from `GET /planner_templates`; do not hardcode them.
Un plan, con sus publicaciones ya resueltas y con los ficheros de cada una.
Léelo cuando a tu app le llegue ai_plan_generated o ai_plan_failed. Sin webhook, es el
endpoint que se sondea mientras state sea pending o generating. El webhook no se
reintenta: si tu endpoint estaba caído cuando el plan terminó, esto sigue diciendo cómo acabó.
Los planes de la organización, encadenando páginas.
Los planes ACTIVOS de la organización, del más reciente al más antiguo.
Los cancelados no salen —ni los archivados, que se piden con archived: true— y aquí
publications son identificadores, no las publicaciones enteras: para eso está get.
Sin limit vuelven todos.
const activos = await pv.aiPlans.list(clientId, orgId);
const guardados = await pv.aiPlans.list(clientId, orgId, { archived: true });
Regenera con IA el texto o la imagen de UNA publicación del plan. Cuesta créditos cada vez.
credits_spent de la respuesta es el total del plan, no lo que costó esta llamada.
"image" depende de la plantilla del plan, no sólo de que el plan permitiera imágenes:
la que no generó la imagen tampoco la regenera. Míralo en regenerate.image de
CatalogResource.plannerTemplates antes de ofrecer el botón — en from_images y en
from_catalog sería cobrarle 70 créditos al usuario por sustituir su propia foto por una
inventada.
Borra el plan y sus publicaciones que aún no han salido: las drafts generadas y, si ya se había validado, las que quedaran programadas. Las ya publicadas se quedan —borrarlas aquí no las quitaría de la red, sólo perdería su historial— y la que se está publicando en ese instante tampoco se toca.
El plan en sí no se borra del todo: pasa a cancelled, desaparece de list y
get lo sigue devolviendo. Los créditos gastados no se devuelven y un plan
generating no se puede borrar (2102): hay que esperar a que el job termine.
Si lo que se quiere es dejar de verlo sin perder nada, archive.
Qué rindió cada plan con lo que publicó, el agregado por plantilla y el total: la respuesta a
«¿qué plan funcionó mejor?». Pide ai_plans:read y publication_stats:read.
Lo que sorprende:
week_start), no por cuándo se creó: un plan
creado hoy para la semana que viene todavía no ha publicado nada.ranked: true; los demás van detrás.ai_plan_results de
DashboardResource.summary.No envuelve la respuesta en un Paginated: además de la página trae totals y by_template,
que no dependen de ella.
const { by_template, ai_plans } = await pv.aiPlans.results(clientId, orgId, {
from_date: "2026-06-01T00:00:00Z",
social_network: ["instagram"],
});
const mejor = ai_plans.find((plan) => plan.ranked);
Vuelve a encolar un plan failed con los mismos datos. El estado regresa a pending y el
final vuelve a llegar como tras create(): por webhook, o sondeando.
Usa el contexto de marca copiado al crear el plan, no el actual: el plan es reproducible aunque alguien haya editado la configuración entretanto.
Devuelve el plan al listado activo. Sobre un plan que no estaba archivado no hace nada.
Acepta el plan: las drafts generadas SIN errores pasan a ready y a partir de ahí las publica
el robot como cualquier publicación programada.
Sólo desde generated (error 2102 en cualquier otro estado). Las drafts que sí tienen
errores se quedan en draft: se arreglan o se borran con pv.publications.
Archiva el plan: sale del listado y pasa al de archivados (
list(..., { archived: true })).Es sólo visibilidad. No toca ninguna publicación —lo que estuviera programado sigue publicándose—, no devuelve créditos y no cancela nada, así que vale en CUALQUIER estado,
generatingincluido: no interrumpe al job. Se deshace con unarchive.Es lo que se busca casi siempre que uno piensa en "quitar" un plan: remove se lleva por delante las publicaciones que aún no han salido, y esto no.