Saltar al contenido principal

Enviar eventos

Un evento es algo que ocurrió, introducido por su sistema en el momento en que ocurrió. Reelwire lo renderiza con la plantilla que lleva el stream, para cada canal suscrito a ese stream, y entrega cada clip como indica su canal: por correo electrónico, por webhook o a través del conector de plataforma del canal, que es la única vía por la que Reelwire lo publica por sí mismo. Consulte Publicaciones y trabajos para ver lo que ocurre después. Es la integración que la mayoría de los clientes escribe primero y, para un productor o un sistema de trading, suele ser la única.

Dos rutas aceptan eventos. POST /v1/stream acepta sus propios datos, dirigidos por un id de stream que usted creó. POST /v1/ingest/{source}/{format} acepta los datos de un feed incluido para su cuenta, dirigidos por los slugs propios del feed. Ambas aceptan el mismo envoltorio salvo por la dirección (la ruta de feeds incluidos acepta además un origin y, para datos manuales, un schedule), ambas son idempotentes por eventId, y ambas tienen una prueba en seco al lado.

MétodoRutaPermiso
POST/v1/streamingest:write
POST/v1/stream/validateingest:write
GET/v1/streamingest:write
POST/v1/ingest/{source}/{format}ingest:write
POST/v1/ingest/{source}/{format}/validateingest:write
GET/v1/ingest-eventsreceived:read
GET/v1/ingest-events/{id}received:read

ingest es un área de solo escritura: no existe ingest:read, y GET /v1/stream también se alcanza con ingest:write, de modo que una clave de productor no necesita nada más para enviar. El registro de lo que llegó es el área Actividad, received:read, que es de solo lectura: una clave de informes puede ver lo que se envió sin poder enviar, y nada puede editar el recibo de algo que envió. Consulte Permisos.

Dos ayudas en la pantalla Feeds propios del panel acortan la primera hora. Obtener su código escribe un emisor que funciona para uno de sus streams, en Node.js, Python, PHP, C# o curl, a partir del ejemplo de plantilla del stream. La Herramienta de prueba envía un evento a un stream como usted, con la sesión iniciada y sin clave: su ruta, POST /v1/streams/{id}/test, es solo de una persona, y un sistema envía a POST /v1/stream con su propia clave, como se indica abajo.

Encontrar los streams en los que puede publicar una clave​

La primera llamada que debería hacer un productor. No necesita ningún permiso más allá del que ya tiene, y responde con la ruta y todos los streams que la clave puede direccionar.

curl http://localhost:4000/v1/stream \
-H "Authorization: Bearer rw_your_key_here"
{
"route": "http://localhost:4000/v1/stream",
"streams": [
{ "streamId": "st_30472168d941", "name": "Enterprise 2 Stream 1", "templateId": "tpl_dd7e6b264fb8", "enabled": true },
{ "streamId": "st_87cdd2aa7d2b", "name": "Enterprise 2 Stream 2", "templateId": "tpl_39bc5fecfa42", "enabled": true },
{ "streamId": "st_696a86f10c8a", "name": "Enterprise 2 Stream 3", "templateId": "tpl_71aae4d0157f", "enabled": true }
]
}

El envoltorio​

POST /v1/stream acepta cinco campos, uno de ellos opcional:

CampoObligatorioQué es
streamIdsíst_ y doce caracteres hexadecimales. Uno de sus propios streams, tal como figura en Feeds propios.
eventIdsíUn ULID, en base32 de Crockford. La clave de idempotencia, por espacio de trabajo.
occurredAtsíUn instante ISO 8601 con su zona horaria (Z o un desfase como +02:00): cuándo ocurrió el hecho, no cuándo lo envió.
localenoEl idioma del texto libre de data.
datasíUn objeto. Lo que va en él es lo que acepta la plantilla del stream.

No hay más campos. Un templateId o un templateVersion en el cuerpo se ignora: el stream lleva la plantilla, y un canal que se suscribe al stream elige el corte y la compilación.

Qué va en data​

Con cada plantilla del catálogo, data tiene dos mitades. clipData es lo que se renderiza en el clip, y sus campos son los propios de la plantilla: para Cifra destacada, la plantilla de los ejemplos de esta página, un kicker, qué es la cifra (label), la cifra (value) con su prefijo y unidad, su variación, una línea de contexto, más cifras y una fuente. metaData es lo que dice la publicación junto al clip y nunca se renderiza, y es igual en todas las plantillas: title, el texto en cuatro longitudes (captionTiny, captionShort, captionMedium y captionLong, de hasta 200, 500, 1500 y 4000 caracteres) y hashtags. Cada canal publica la longitud que encaja con su plataforma, y a un canal en Sin plataforma específica se le entregan las cuatro. Una longitud que omite recurre a la siguiente más corta que haya escrito, y si no a la siguiente más larga. A un evento sin metaData se le escribe el texto de la publicación a partir de clipData. Un campo que la plantilla no tiene es un error que lo nombra (un 400 en un feed incluido, unmapped en un stream), de modo que una clave mal escrita nunca queda sin renderizar en silencio.

Los demás idiomas van en data.locales. locale nombra el idioma en que está escrito data; junto al cuerpo, data.locales puede contener, por código de idioma y con la misma forma que el cuerpo, solo lo que difiere en ese idioma, como "locales": { "de": { "clipData": { "label": "..." } } }. Un canal en alemán renderiza el cuerpo con el bloque de superpuesto, y un canal en un idioma para el que el evento no lleva bloque renderiza el cuerpo tal como está. Reelwire no traduce nada por sí mismo.

Una imagen es un archivo de su biblioteca multimedia y nada más. Un campo de imagen acepta { "url": "media://...", "alt": "..." }, donde la url es media:// seguido del id o del nombre del archivo. Una dirección web se rechaza y nombra el campo: en un stream el evento se registra como unmapped, y la prueba en seco responde valid: false; en un feed incluido es un 400. Suba primero la imagen a la biblioteca multimedia y luego haga referencia a ella. Una referencia a un archivo que la biblioteca de este espacio de trabajo no tiene se renderiza como imagen ausente; la prueba en seco del stream no mira en la biblioteca, y POST /v1/templates/{templateId}/validate sí, y nombra cada campo así.

Validar antes de enviar​

POST /v1/stream/validate acepta el cuerpo idéntico, ejecuta la misma asignación y validación y no guarda nada. Escriba primero su productor contra esta ruta y manténgala en su conjunto de pruebas.

Un evento asignado a cada canal que lo recibe, cada uno con su versión exacta de plantilla, y otro rechazado antes de renderizar nada porque un campo es demasiado largo

curl http://localhost:4000/v1/stream/validate \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"streamId": "st_30472168d941",
"eventId": "01M2SQMKK3G1NVFXV31DCBVB92",
"occurredAt": "2026-09-18T07:55:00Z",
"locale": "en",
"data": {
"clipData": {
"kicker": "At the close",
"label": "Ten-year Bund yield, a three-week high",
"value": 2.41,
"unit": "%",
"change": { "value": 0.06, "unit": "pts", "label": "on the day" },
"source": "Deutsche Finanzagentur"
},
"metaData": {
"title": "Bund yields close at a three-week high",
"captionShort": "The ten-year Bund settled at 2.41 per cent, its highest in three weeks.",
"hashtags": ["bunds", "rates"]
}
}
}'
{
"requestId": "01M2SQMVE23G7M81DJG9NZMAP2",
"streamId": "st_30472168d941",
"templateId": "tpl_dd7e6b264fb8",
"templateVersion": "1.0.0",
"valid": true,
"document": {
"clipData": {
"kicker": "At the close",
"label": "Ten-year Bund yield, a three-week high",
"value": 2.41,
"unit": "%",
"change": { "value": 0.06, "unit": "pts", "label": "on the day" },
"source": "Deutsche Finanzagentur"
},
"metaData": {
"title": "Bund yields close at a three-week high",
"captionShort": "The ten-year Bund settled at 2.41 per cent, its highest in three weeks.",
"hashtags": ["bunds", "rates"]
}
},
"warnings": [],
"errors": [],
"wouldMatch": [
{ "channelId": "01M2985NX4V0WA1GV9MC63DEPJ", "channelName": "Channel 4 de", "subscriptionId": "01M2985P0SKB0JK587C9ZZKRKN", "outputs": 1 },
{ "channelId": "01M2985NXJRXVE5HKCCGKASP8P", "channelName": "Channel 10 en", "subscriptionId": "01M2985P2HZGF9SGQCW7YX6QRC", "outputs": 1 }
]
}

Aquí hay dos cosas que leer. document es el evento después de aplicar el mapa de campos del stream: exactamente lo que se entregará a la plantilla. wouldMatch es cada canal que recibiría un clip, así que un array vacío significa que el evento es válido y nadie lo recibe.

Una validación fallida también responde 200. valid es el campo que dice si habría funcionado, no el código de estado:

{
"requestId": "01M2SQNGZ0NR3GZ9WV2W75XMSZ",
"streamId": "st_30472168d941",
"templateId": "tpl_dd7e6b264fb8",
"templateVersion": "1.0.0",
"valid": false,
"document": { "clipData": { "label": "Ten-year Bund yield", "valeu": 2.41 } },
"warnings": [],
"errors": [
{ "path": "/clipData", "message": "missing value" },
{ "path": "/clipData", "message": "has a field this template does not take: valeu" }
],
"wouldMatch": [
{ "channelId": "01M2985NX4V0WA1GV9MC63DEPJ", "channelName": "Channel 4 de", "subscriptionId": "01M2985P0SKB0JK587C9ZZKRKN", "outputs": 1 },
{ "channelId": "01M2985NXJRXVE5HKCCGKASP8P", "channelName": "Channel 10 en", "subscriptionId": "01M2985P2HZGF9SGQCW7YX6QRC", "outputs": 1 }
]
}

wouldMatch aparece en la lista incluso para un documento que falla, ya que depende de quién recibe y no de lo que usted envió. Cada error nombra su lugar como JSON Pointer y dice con palabras qué está mal: el campo que falta, el campo que esta plantilla no admite o, para una opción, los valores que acepta (se listan doce como máximo).

Un envoltorio mal formado es distinto: un eventId que no es un ULID, o un streamId ausente, es un 400 con un envoltorio de problema, porque no hay nada contra lo que validar. Un streamId que esta cuenta no tiene es un 404 unknown_stream, y un stream en pausa un 409 stream_disabled, tanto en la prueba en seco como en la llamada real.

Enviarlo​

El mismo cuerpo, a /v1/stream.

curl http://localhost:4000/v1/stream \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d @event.json
{
"ingestEventId": "01M2SQN507TDA0KNTJJABHBR9A",
"requestId": "01M2SQN4ZJNMXNV98HYXRST0WZ",
"status": "accepted",
"idempotent": false,
"streamId": "st_30472168d941",
"matched": [
{
"channelId": "01M2985NX4V0WA1GV9MC63DEPJ",
"channelName": "Channel 4 de",
"subscriptionId": "01M2985P0SKB0JK587C9ZZKRKN",
"templateId": "tpl_dd7e6b264fb8",
"templateVersion": "1.0.0",
"outputs": 1,
"postGroupId": "01M2SQN50JQFC00PZE93M9E33R"
},
{
"channelId": "01M2985NXJRXVE5HKCCGKASP8P",
"channelName": "Channel 10 en",
"subscriptionId": "01M2985P2HZGF9SGQCW7YX6QRC",
"templateId": "tpl_dd7e6b264fb8",
"templateVersion": "1.0.0",
"outputs": 1,
"postGroupId": "01M2SQN50TAP4BXAYDZRC9WMBC"
}
],
"warnings": [],
"normalized": { "clipData": { "kicker": "At the close", "label": "Ten-year Bund yield, a three-week high", "value": 2.41, "unit": "%", "change": { "value": 0.06, "unit": "pts", "label": "on the day" }, "source": "Deutsche Finanzagentur" }, "metaData": { "title": "Bund yields close at a three-week high", "captionShort": "The ten-year Bund settled at 2.41 per cent, its highest in three weeks.", "hashtags": ["bunds", "rates"] } }
}

202, no 201. Renderizar tarda decenas de segundos y el productor no va a esperar. Cada postGroupId es un trabajo que puede seguir en Publicaciones y trabajos: es el id del canal allí, e ingestEventId es el id propio de la publicación (su postKey), ya que cada canal al que llega un evento es una publicación. Una vez renderizado, el clip de cada canal se entrega como indica el canal, o espera antes a un revisor donde el canal lo pide.

Los estados​

statusCódigo de estadoQué ha pasado
accepted202Validado, asignado al menos a un canal, trabajo en cola.
no_match202Válido, y ningún canal lo recibe: ninguna suscripción activada nombra este stream, o todos los canales que tienen una están en pausa, usan una marca en pausa o tienen un formato que la plantilla no renderiza. Guardado, nada renderizado.
unmapped202El documento, tras el mapa de campos del stream, no encaja en la plantilla. Guardado, nada renderizado. mappingErrors indica por qué, campo por campo.
duplicate200Este eventId ya llegó. Se mantiene el resultado original.
stale202Solo feeds incluidos: válido y más antiguo de lo que acepta el feed (su maxAgeSeconds en el catálogo, un día para los feeds predefinidos). Guardado, nada renderizado.

Solo accepted produce publicaciones. Trate no_match y stale como éxito, porque lo son: su evento estaba bien formado y se guardó, no justificaba una publicación. Trate unmapped como el error que es y pregunte a /v1/stream/validate qué quiere.

Idempotencia: eventId es la clave​

eventId es un ULID que usted genera, una vez, para una cosa que ocurrió. Es la clave de idempotencia, por espacio de trabajo, y es lo que hace seguro un reintento: una solicitud que agotó el tiempo puede enviarse de nuevo sin crear una segunda publicación.

Reenviar el mismo eventId con el mismo cuerpo no hace nada, y se responde 200 con status: "duplicate" e idempotent: true:

{
"ingestEventId": "01M2SQN507TDA0KNTJJABHBR9A",
"requestId": "01M2SQN52VVB9MDKGQ6AQ2TBRH",
"status": "duplicate",
"idempotent": true,
"streamId": "st_30472168d941",
"matched": [],
"warnings": []
}

ingestEventId es el mismo que en el primer envío, así que aún puede vincular la respuesta con el trabajo. matched está vacío, porque no se asignó nada nuevo; consulte el evento original si necesita la lista.

Reenviar el mismo eventId con un cuerpo distinto se rechaza, porque eso siempre es un error del productor y no un reintento:

{
"error": {
"type": "https://reelwire.io/errors/idempotency_conflict",
"code": "idempotency_conflict",
"message": "eventId 01M2SQMKK3G1NVFXV31DCBVB92 was already used with a different body",
"requestId": "01M2SQNGWMW5F5P9CEEHDEFM1B",
"details": [
{ "path": "/eventId", "message": "Use a fresh eventId for a different payload, or resend the original body" }
]
}
}

Genere el id donde se genera el evento y llévelo a través de sus reintentos. Un id creado dentro del bucle de reintentos convierte cada reintento en un evento nuevo, que es exactamente el fallo que el campo existe para evitar.

Feeds incluidos: POST /v1/ingest/{source}/{format}​

Cuando un feed es uno que Reelwire incluye y usted envía sus datos para su propia cuenta, la dirección son los slugs de fuente y de formato del feed en la ruta, y el cuerpo no lleva streamId. Acepta eventId, occurredAt, origin (shipped, custom o manual, por defecto shipped), un locale opcional y data. Un schedule (publishAt y timezone) solo se acepta con origin: "manual"; los datos incluidos y propios salen cuando llegan. Lo que puede contener data es la asignación de claves de la plantilla con la que se renderiza el feed: pídala a GET /v1/formats/feed/{feedId}. El contenido que no encaja es un 400 que nombra el campo bajo /data, tanto en la prueba en seco como en la llamada real: a diferencia de la prueba en seco del stream, esta no responde valid: false. Un slug de fuente o de formato que no existe es un 404 (unknown_source, unknown_format, que lista los formatos de la fuente).

curl http://localhost:4000/v1/ingest/crypto-spot-and-dominance/crypto-board/validate \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"eventId": "01M2SQMKK3G1NVFXV31DCBVB96",
"occurredAt": "2026-09-18T08:00:00Z",
"data": {
"clipData": {
"title": "The eight largest coins and how they moved in 24 hours",
"items": [
{ "label": "Bitcoin", "value": 85788, "prefix": "$", "change": 0.42 },
{ "label": "Ethereum", "value": 2700.79, "prefix": "$", "change": -0.23 }
]
},
"metaData": { "title": "Crypto board" }
}
}'
{
"requestId": "01M2SQZHQ6K0Y55BA1MK2JG53B",
"valid": true,
"normalized": {
"clipData": {
"title": "The eight largest coins and how they moved in 24 hours",
"items": [
{ "label": "Bitcoin", "value": 85788, "prefix": "$", "change": 0.42 },
{ "label": "Ethereum", "value": 2700.79, "prefix": "$", "change": -0.23 }
]
},
"metaData": { "title": "Crypto board" }
},
"wouldMatch": [
{ "channelId": "01M1VFRWE7EWSY5D7QDQ0KNK1S", "channelName": "Channel 1 en", "subscriptionId": "01M29DSF6S2JYYXQQM4XHJMGK7", "templateId": "tpl_06dc2554e5ac", "templateVersion": "1.0.0", "outputs": 1 },
{ "channelId": "01M2985NX4V0WA1GV9MC63DEPJ", "channelName": "Channel 4 de", "subscriptionId": "01M29DSF7F2JT2N5FXSF126CSF", "templateId": "tpl_06dc2554e5ac", "templateVersion": "1.0.0", "outputs": 1 }
],
"warnings": [],
"stale": false
}

Observe que el formato no es del todo el del stream: esta ruta responde con normalized en lugar de document y lleva un indicador stale. wouldMatch está vacío, con una advertencia, cuando el feed está desactivado para este espacio de trabajo en Feeds predefinidos, y normalized se omite mientras ningún canal suyo reciba el feed. POST /v1/ingest/{source}/{format} en sí responde 202 con ingestEventId, status, idempotent, matched y normalized, sea cual sea el estado: a diferencia de la ruta del stream, aquí un duplicate también es un 202, y su matched lista los canales a los que llegó el primer envío.

Declarar para qué cuenta envía​

Un productor que atiende a varios clientes puede afirmar para cuál es esta solicitud con el encabezado x-reelwire-account. La clave bearer sigue decidiendo dónde llega el evento; el encabezado solo dice lo que usted cree, y una discrepancia se rechaza en lugar de archivarse en silencio bajo quien sea el titular de la clave.

curl http://localhost:4000/v1/ingest/crypto-spot-and-dominance/crypto-board \
-H "Authorization: Bearer rw_your_key_here" \
-H "x-reelwire-account: 01M1VFRWE3A35H892VAAEKKDA7" \
-H "Content-Type: application/json" \
-d @event.json
{
"error": {
"type": "https://reelwire.io/errors/forbidden",
"code": "forbidden",
"message": "This credential does not belong to the account this request declares",
"requestId": "01M2SR1WZW7Q0N6A1E039XG2YT",
"details": []
}
}

Sin el encabezado, tomar la clave equivocada escribe los datos de un cliente en el espacio de trabajo de otro y devuelve 202, y el error solo se ve más tarde, en el panel de otra persona. El encabezado no cuesta nada y lo evita.

Leer un evento de vuelta​

GET /v1/ingest-events lista lo que llegó, lo más reciente primero, paginado como todo registro (consulte Listas y paginación).

curl "http://localhost:4000/v1/ingest-events?perPage=1" \
-H "Authorization: Bearer rw_your_key_here"
{
"events": [
{
"id": "01M2SQN507TDA0KNTJJABHBR9A",
"origin": "custom",
"details": "Enterprise 2 Stream 1",
"postTitle": null,
"eventId": "01M2SQMKK3G1NVFXV31DCBVB92",
"status": "ACCEPTED",
"occurredAt": "2026-09-18T07:55:00.000Z",
"receivedAt": "2026-09-18T07:45:50.088Z"
}
],
"total": 1,
"all": 1,
"page": 1,
"perPage": 1,
"facets": { "status": ["ACCEPTED"], "origin": ["custom"] }
}

Filtra por status (ACCEPTED, NO_MATCH, UNMAPPED o STALE) y origin (shipped, custom o manual), y busca en los nombres que imprime la fila: el feed, el stream o la lista de publicaciones.

GET /v1/ingest-events/{id} devuelve un evento completo, con su documento normalized y cada grupo de publicaciones que produjo.

Lo que conviene saber sobre POST /v1/broadcast​

/v1/broadcast, /v1/broadcast/validate, /v1/broadcast/{id}, /v1/broadcast/pictures, /v1/broadcast/schedules, /v1/broadcast/log y /v1/broadcasts aparecen en la lista de rutas, y no son suyas: figuran bajo «No disponible para claves de API». Las dos primeras son la forma en que el servidor de feeds propio de Reelwire entrega los feeds predefinidos, un evento a todos los espacios de trabajo con un canal que lo recibe a la vez, y necesitan una credencial emitida exactamente para eso; una clave de espacio de trabajo es rechazada:

{
"error": {
"type": "https://reelwire.io/errors/forbidden",
"code": "forbidden",
"message": "This credential lacks the ingest:shipped scope",
"requestId": "01M2SR5FVDBNWWB37MYY9DVABF",
"details": []
}
}

Las otras dos son el registro propio de la plataforma de lo que alcanzó una difusión. No construya sobre ellas. Su cuenta recibe difusiones suscribiendo un canal al feed; consulte Canales.

Errores frecuentes​

Generar el eventId dentro del bucle de reintentos. Entonces cada reintento es un evento nuevo, y un fallo de red entrega la misma historia tres veces. Genérelo una vez, donde se genera el hecho.

Enviar occurredAt como «ahora». Es cuándo ocurrió el hecho. Un feed con una antigüedad máxima lo usa para decidir si el evento aún merece una publicación, y enviar el reloj en su lugar hace que una repetición de hace dos horas parezca reciente.

Construir el cuerpo a mano. Pregunte a /v1/formats/template/{templateId} qué acepta la plantilla y genere un cliente a partir del jsonSchema que devuelve. Consulte Plantillas y formatos.

Tratar no_match como un fallo. Significa que ningún canal activo recibe aún ese stream. Revise las suscripciones de los canales y si esos canales están en pausa, no su productor.

Leer el código de estado de /v1/stream/validate en lugar de valid. Un documento que no encaja en la plantilla responde 200 ahí igualmente. (La prueba en seco de un feed incluido es la excepción: responde 400.)

Poner en el cuerpo un id de stream de otra cuenta. Responde 404 unknown_stream, exactamente igual que un id que no existe en ninguna parte.