Recoup

Get started with Recoup.

API REFERENCE

Task callback

On this page
POST/api/content-agent/callback

Internal worker callback for a configured content agent. Requires the shared callback secret in x-callback-secret. Public content creation uses POST /api/content/create.

Internal callback endpoint for the poll-content-run Trigger.dev task. Receives content generation results and posts them back to the originating Slack thread. Authenticated via the x-callback-secret header.

This endpoint is not intended for external use — it is called automatically by the polling task when content runs complete, fail, or time out.

The receiver retains optional results[].imageUrl as a string, matching the existing videoUrl string validation; this does not add URL syntax validation. Only per-run results with status completed and a videoUrl enter media posting. An image without a completed video does not become an image-only job or still-only success. Existing video-only callbacks continue without an image read or image post.

For selected completed results, editorial images retain their per-result association and order; all editorial image posts precede all video posts, with videos also in result order. Images attach for normalized reported Content-Type image/png, image/jpeg, image/webp or image/gif. Missing or unsupported reported MIME, or failed image download, falls back to an Editorial Image URL message before video posting continues. For an attachment filename, keep the final URL path component when it contains a dot; otherwise use editorial.<reported MIME extension>, with jpg for image/jpeg. Existing dotted filenames are not rewritten to match the MIME extension. Reported MIME is not byte verification or a safety scan. Each supplied image adds a media HTTP read and a thread attachment/link post within existing outbound callback trust, buffering and upload/runtime limits. This callback does not start another generation step or change generation billing.

Existing caption/video labels, failed-run summaries and terminal-thread retry skipping remain. The thread-state skip is not a concurrency-safe or exactly-once delivery guarantee. Normal image delivery requires both this receiver support and a deployed polling mapper that forwards imageUrl (API #1097 and Tasks #162). Source tests and this reference do not establish worker deployment, hosted authorization, actual Slack file rendering or end-to-end image delivery.

If the content bot is not configured, the route returns 503 with {error: "Content agent not configured"} before bot initialization, callback-secret handling or body parsing. For a configured bot, initialization precedes the handler; the handler checks x-callback-secret before parsing JSON or accessing a thread or media. Missing or invalid secrets return 401 with {status: "error", error: "Unauthorized"}. Invalid JSON returns 400 with {status: "error", error: "Invalid JSON body"}. Schema-validation 400 responses use status/error plus missing_fields, the first issue path containing string field names and numeric array positions (for example ["results", 0, "imageUrl"]). missing_fields is absent for malformed JSON.

A normal handler acknowledgement is 200 {status: "ok"}; a callback for an already-terminal thread is 200 {status: "ok", skipped: true} without another media post. These acknowledgements do not prove a Slack file was delivered or rendered. Handler 200/400/401 responses include the existing shared CORS headers; the unconfigured-bot 503 has no shared CORS headers. Shared response headers do not promise browser preflight success: x-callback-secret is not in the shared Access-Control-Allow-Headers list, and this route exports no OPTIONS handler. No stable 500 response envelope or delivery guarantee is declared here.

Authentication

x-callback-secret in header

Request

cURL
curl --request POST \
  --url 'https://api.recoupable.dev/api/content-agent/callback' \
  --header 'x-callback-secret: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "threadId": "slack:C_FIXTURE:T_FIXTURE",
  "status": "completed",
  "results": [
    {
      "runId": "run-a",
      "status": "completed",
      "videoUrl": "https://example.invalid/video-a.mp4",
      "imageUrl": "https://example.invalid/editorial-a",
      "captionText": "First fixture caption"
    },
    {
      "runId": "run-b",
      "status": "completed",
      "videoUrl": "https://example.invalid/video-b.mp4",
      "imageUrl": "https://example.invalid/editorial-b",
      "captionText": "Second fixture caption"
    }
  ]
}'

Try it

Fill in the fields, send the request from your browser, and read the live response. The curl below updates as you type.

Kept in this browser tab only and cleared when it closes.

cURL for this request
curl --request POST \
  --url 'https://api.recoupable.dev/api/content-agent/callback' \
  --header 'Content-Type: application/json' \
  --data '{
  "threadId": "slack:C_FIXTURE:T_FIXTURE",
  "status": "completed",
  "results": [
    {
      "runId": "run-a",
      "status": "completed",
      "videoUrl": "https://example.invalid/video-a.mp4",
      "imageUrl": "https://example.invalid/editorial-a",
      "captionText": "First fixture caption"
    },
    {
      "runId": "run-b",
      "status": "completed",
      "videoUrl": "https://example.invalid/video-b.mp4",
      "imageUrl": "https://example.invalid/editorial-b",
      "captionText": "Second fixture caption"
    }
  ]
}'

Request body required

Content generation results from the polling task

application/json

threadIdstringrequired

Chat SDK thread identifier for the originating Slack thread

statusstring · enumrequired

Overall status of the content generation batch

Values: "completed", "failed", "timeout"

resultsarray<object>

Per-run results

Item properties for results
runIdstringrequired

Trigger.dev run ID

statusstring · enumrequired

Values: "completed", "failed", "timeout"

videoUrlstring

URL of the generated video (when completed)

captionTextstring

Generated caption text (when completed)

errorstring

Error message (when failed)

imageUrlstring

Optional editorial-image string associated with this run. Used only when this result is completed and also has videoUrl; not an image-only job. Attachment uses supported reported MIME, with URL fallback on download/MIME failure. No URL syntax validation is added by this field. For an attachment filename, keep the final URL path component when it contains a dot; otherwise use editorial.<reported MIME extension>, with jpg for image/jpeg. Existing dotted filenames are not rewritten to match the MIME extension.

messagestring

Optional human-readable message

Synthetic completed results with their already-produced editorial images. No generation or live delivery is exercised.

withEditorialImages
{
  "threadId": "slack:C_FIXTURE:T_FIXTURE",
  "status": "completed",
  "results": [
    {
      "runId": "run-a",
      "status": "completed",
      "videoUrl": "https://example.invalid/video-a.mp4",
      "imageUrl": "https://example.invalid/editorial-a",
      "captionText": "First fixture caption"
    },
    {
      "runId": "run-b",
      "status": "completed",
      "videoUrl": "https://example.invalid/video-b.mp4",
      "imageUrl": "https://example.invalid/editorial-b",
      "captionText": "Second fixture caption"
    }
  ]
}

Existing video-only callback without an editorial image.

videoOnly
{
  "threadId": "slack:C_FIXTURE:T_FIXTURE",
  "status": "completed",
  "results": [
    {
      "runId": "run-video",
      "status": "completed",
      "videoUrl": "https://example.invalid/video.mp4",
      "captionText": "Fixture caption"
    }
  ]
}

Responses

200Handler processing acknowledgement or terminal-thread retry skip. Neither response alone establishes that a Slack file was delivered or rendered.
Access-Control-Allow-Originresponse header

Existing shared response header; this does not establish browser preflight support.

Access-Control-Allow-Methodsresponse header

Existing shared response header; this does not establish browser preflight support.

Access-Control-Allow-Headersresponse header

Existing shared response header; this does not establish browser preflight support.

Access-Control-Expose-Headersresponse header

Existing shared response header; this does not establish browser preflight support.

application/json

statusstring · enumrequired

Values: "ok"

Example: "ok"

skippedboolean · enum

Present as true for an already-terminal thread; processing/media posting is skipped. This is not a concurrency-safe delivery guarantee.

Values: true

Normal processing acknowledgement, including non-file notifications.

processed
{
  "status": "ok"
}

Existing terminal-thread retry skip without another media post.

terminalRetry
{
  "status": "ok",
  "skipped": true
}

400Malformed JSON or schema validation failure after successful callback-secret validation. Schema failures include the first issue path in missing_fields; malformed JSON omits it.
Access-Control-Allow-Originresponse header

Existing shared response header; this does not establish browser preflight support.

Access-Control-Allow-Methodsresponse header

Existing shared response header; this does not establish browser preflight support.

Access-Control-Allow-Headersresponse header

Existing shared response header; this does not establish browser preflight support.

Access-Control-Expose-Headersresponse header

Existing shared response header; this does not establish browser preflight support.

application/json

allOf · ContentCallbackErrorResponse 1
statusstring · enumrequired

Values: "error"

errorstringrequired

Human-readable error. Callback-secret denial uses Unauthorized.

allOf · object 2
missing_fieldsarray<one of>

First schema issue path; omitted for malformed JSON. Path elements can be field names or numeric array positions.

Example: ["results",0,"imageUrl"]

Item properties for missing_fields
oneOf · string 1

string

oneOf · number 2

number

invalidJson
{
  "status": "error",
  "error": "Invalid JSON body"
}

Synthetic invalid imageUrl type; error wording follows the validator.

invalidImageType
{
  "status": "error",
  "error": "Invalid input: expected string, received number",
  "missing_fields": [
    "results",
    0,
    "imageUrl"
  ]
}

401Missing or invalid shared callback secret, rejected before JSON parsing, thread access or media access in the configured handler.
Access-Control-Allow-Originresponse header

Existing shared response header; this does not establish browser preflight support.

Access-Control-Allow-Methodsresponse header

Existing shared response header; this does not establish browser preflight support.

Access-Control-Allow-Headersresponse header

Existing shared response header; this does not establish browser preflight support.

Access-Control-Expose-Headersresponse header

Existing shared response header; this does not establish browser preflight support.

application/json

statusstring · enumrequired

Values: "error"

errorstringrequired

Human-readable error. Callback-secret denial uses Unauthorized.

denied
{
  "status": "error",
  "error": "Unauthorized"
}

503Content bot is not configured. Returned before initialization/authentication/body parsing, without shared CORS headers; this envelope has only error, without status.

application/json

errorstringrequired
unconfigured
{
  "error": "Content agent not configured"
}

Full specification

Download the OpenAPI file for complete schemas, constraints, and examples.

Download content.json
View operation source
json
{
  "operationId": "postContentAgentCallback",
  "description": "Internal worker callback for a configured content agent. Requires the shared callback secret in x-callback-secret. Public content creation uses POST /api/content/create.\n\nInternal callback endpoint for the `poll-content-run` Trigger.dev task. Receives content generation results and posts them back to the originating Slack thread. Authenticated via the `x-callback-secret` header.\n\nThis endpoint is not intended for external use — it is called automatically by the polling task when content runs complete, fail, or time out.\n\nThe receiver retains optional results[].imageUrl as a string, matching the existing videoUrl string validation; this does not add URL syntax validation. Only per-run results with status completed and a videoUrl enter media posting. An image without a completed video does not become an image-only job or still-only success. Existing video-only callbacks continue without an image read or image post.\n\nFor selected completed results, editorial images retain their per-result association and order; all editorial image posts precede all video posts, with videos also in result order. Images attach for normalized reported Content-Type image/png, image/jpeg, image/webp or image/gif. Missing or unsupported reported MIME, or failed image download, falls back to an Editorial Image URL message before video posting continues. For an attachment filename, keep the final URL path component when it contains a dot; otherwise use editorial.<reported MIME extension>, with jpg for image/jpeg. Existing dotted filenames are not rewritten to match the MIME extension. Reported MIME is not byte verification or a safety scan. Each supplied image adds a media HTTP read and a thread attachment/link post within existing outbound callback trust, buffering and upload/runtime limits. This callback does not start another generation step or change generation billing.\n\nExisting caption/video labels, failed-run summaries and terminal-thread retry skipping remain. The thread-state skip is not a concurrency-safe or exactly-once delivery guarantee. Normal image delivery requires both this receiver support and a deployed polling mapper that forwards imageUrl (API #1097 and Tasks #162). Source tests and this reference do not establish worker deployment, hosted authorization, actual Slack file rendering or end-to-end image delivery.\n\nIf the content bot is not configured, the route returns 503 with {error: \"Content agent not configured\"} before bot initialization, callback-secret handling or body parsing. For a configured bot, initialization precedes the handler; the handler checks x-callback-secret before parsing JSON or accessing a thread or media. Missing or invalid secrets return 401 with {status: \"error\", error: \"Unauthorized\"}. Invalid JSON returns 400 with {status: \"error\", error: \"Invalid JSON body\"}. Schema-validation 400 responses use status/error plus missing_fields, the first issue path containing string field names and numeric array positions (for example [\"results\", 0, \"imageUrl\"]). missing_fields is absent for malformed JSON.\n\nA normal handler acknowledgement is 200 {status: \"ok\"}; a callback for an already-terminal thread is 200 {status: \"ok\", skipped: true} without another media post. These acknowledgements do not prove a Slack file was delivered or rendered. Handler 200/400/401 responses include the existing shared CORS headers; the unconfigured-bot 503 has no shared CORS headers. Shared response headers do not promise browser preflight success: x-callback-secret is not in the shared Access-Control-Allow-Headers list, and this route exports no OPTIONS handler. No stable 500 response envelope or delivery guarantee is declared here.",
  "requestBody": {
    "description": "Content generation results from the polling task",
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "required": [
            "threadId",
            "status"
          ],
          "properties": {
            "threadId": {
              "type": "string",
              "description": "Chat SDK thread identifier for the originating Slack thread"
            },
            "status": {
              "type": "string",
              "enum": [
                "completed",
                "failed",
                "timeout"
              ],
              "description": "Overall status of the content generation batch"
            },
            "results": {
              "type": "array",
              "description": "Per-run results",
              "items": {
                "type": "object",
                "required": [
                  "runId",
                  "status"
                ],
                "properties": {
                  "runId": {
                    "type": "string",
                    "description": "Trigger.dev run ID"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "completed",
                      "failed",
                      "timeout"
                    ]
                  },
                  "videoUrl": {
                    "type": "string",
                    "description": "URL of the generated video (when completed)"
                  },
                  "captionText": {
                    "type": "string",
                    "description": "Generated caption text (when completed)"
                  },
                  "error": {
                    "type": "string",
                    "description": "Error message (when failed)"
                  },
                  "imageUrl": {
                    "type": "string",
                    "description": "Optional editorial-image string associated with this run. Used only when this result is completed and also has videoUrl; not an image-only job. Attachment uses supported reported MIME, with URL fallback on download/MIME failure. No URL syntax validation is added by this field. For an attachment filename, keep the final URL path component when it contains a dot; otherwise use editorial.<reported MIME extension>, with jpg for image/jpeg. Existing dotted filenames are not rewritten to match the MIME extension."
                  }
                }
              }
            },
            "message": {
              "type": "string",
              "description": "Optional human-readable message"
            }
          }
        },
        "examples": {
          "withEditorialImages": {
            "summary": "Synthetic completed results with their already-produced editorial images. No generation or live delivery is exercised.",
            "value": {
              "threadId": "slack:C_FIXTURE:T_FIXTURE",
              "status": "completed",
              "results": [
                {
                  "runId": "run-a",
                  "status": "completed",
                  "videoUrl": "https://example.invalid/video-a.mp4",
                  "imageUrl": "https://example.invalid/editorial-a",
                  "captionText": "First fixture caption"
                },
                {
                  "runId": "run-b",
                  "status": "completed",
                  "videoUrl": "https://example.invalid/video-b.mp4",
                  "imageUrl": "https://example.invalid/editorial-b",
                  "captionText": "Second fixture caption"
                }
              ]
            }
          },
          "videoOnly": {
            "summary": "Existing video-only callback without an editorial image.",
            "value": {
              "threadId": "slack:C_FIXTURE:T_FIXTURE",
              "status": "completed",
              "results": [
                {
                  "runId": "run-video",
                  "status": "completed",
                  "videoUrl": "https://example.invalid/video.mp4",
                  "captionText": "Fixture caption"
                }
              ]
            }
          }
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Handler processing acknowledgement or terminal-thread retry skip. Neither response alone establishes that a Slack file was delivered or rendered.",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "example": "ok",
                "enum": [
                  "ok"
                ]
              },
              "skipped": {
                "type": "boolean",
                "enum": [
                  true
                ],
                "description": "Present as true for an already-terminal thread; processing/media posting is skipped. This is not a concurrency-safe delivery guarantee."
              }
            },
            "required": [
              "status"
            ]
          },
          "examples": {
            "processed": {
              "summary": "Normal processing acknowledgement, including non-file notifications.",
              "value": {
                "status": "ok"
              }
            },
            "terminalRetry": {
              "summary": "Existing terminal-thread retry skip without another media post.",
              "value": {
                "status": "ok",
                "skipped": true
              }
            }
          }
        }
      },
      "headers": {
        "Access-Control-Allow-Origin": {
          "description": "Existing shared response header; this does not establish browser preflight support.",
          "schema": {
            "type": "string",
            "enum": [
              "*"
            ]
          }
        },
        "Access-Control-Allow-Methods": {
          "description": "Existing shared response header; this does not establish browser preflight support.",
          "schema": {
            "type": "string",
            "enum": [
              "GET, POST, PUT, DELETE, OPTIONS, PATCH"
            ]
          }
        },
        "Access-Control-Allow-Headers": {
          "description": "Existing shared response header; this does not establish browser preflight support.",
          "schema": {
            "type": "string",
            "enum": [
              "Content-Type, Authorization, X-Requested-With, x-api-key"
            ]
          }
        },
        "Access-Control-Expose-Headers": {
          "description": "Existing shared response header; this does not establish browser preflight support.",
          "schema": {
            "type": "string",
            "enum": [
              "x-workflow-run-id, x-workflow-stream-tail-index"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Malformed JSON or schema validation failure after successful callback-secret validation. Schema failures include the first issue path in missing_fields; malformed JSON omits it.",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/ContentCallbackValidationErrorResponse"
          },
          "examples": {
            "invalidJson": {
              "value": {
                "status": "error",
                "error": "Invalid JSON body"
              }
            },
            "invalidImageType": {
              "summary": "Synthetic invalid imageUrl type; error wording follows the validator.",
              "value": {
                "status": "error",
                "error": "Invalid input: expected string, received number",
                "missing_fields": [
                  "results",
                  0,
                  "imageUrl"
                ]
              }
            }
          }
        }
      },
      "headers": {
        "Access-Control-Allow-Origin": {
          "description": "Existing shared response header; this does not establish browser preflight support.",
          "schema": {
            "type": "string",
            "enum": [
              "*"
            ]
          }
        },
        "Access-Control-Allow-Methods": {
          "description": "Existing shared response header; this does not establish browser preflight support.",
          "schema": {
            "type": "string",
            "enum": [
              "GET, POST, PUT, DELETE, OPTIONS, PATCH"
            ]
          }
        },
        "Access-Control-Allow-Headers": {
          "description": "Existing shared response header; this does not establish browser preflight support.",
          "schema": {
            "type": "string",
            "enum": [
              "Content-Type, Authorization, X-Requested-With, x-api-key"
            ]
          }
        },
        "Access-Control-Expose-Headers": {
          "description": "Existing shared response header; this does not establish browser preflight support.",
          "schema": {
            "type": "string",
            "enum": [
              "x-workflow-run-id, x-workflow-stream-tail-index"
            ]
          }
        }
      }
    },
    "401": {
      "description": "Missing or invalid shared callback secret, rejected before JSON parsing, thread access or media access in the configured handler.",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/ContentCallbackErrorResponse"
          },
          "examples": {
            "denied": {
              "value": {
                "status": "error",
                "error": "Unauthorized"
              }
            }
          }
        }
      },
      "headers": {
        "Access-Control-Allow-Origin": {
          "description": "Existing shared response header; this does not establish browser preflight support.",
          "schema": {
            "type": "string",
            "enum": [
              "*"
            ]
          }
        },
        "Access-Control-Allow-Methods": {
          "description": "Existing shared response header; this does not establish browser preflight support.",
          "schema": {
            "type": "string",
            "enum": [
              "GET, POST, PUT, DELETE, OPTIONS, PATCH"
            ]
          }
        },
        "Access-Control-Allow-Headers": {
          "description": "Existing shared response header; this does not establish browser preflight support.",
          "schema": {
            "type": "string",
            "enum": [
              "Content-Type, Authorization, X-Requested-With, x-api-key"
            ]
          }
        },
        "Access-Control-Expose-Headers": {
          "description": "Existing shared response header; this does not establish browser preflight support.",
          "schema": {
            "type": "string",
            "enum": [
              "x-workflow-run-id, x-workflow-stream-tail-index"
            ]
          }
        }
      }
    },
    "503": {
      "description": "Content bot is not configured. Returned before initialization/authentication/body parsing, without shared CORS headers; this envelope has only error, without status.",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "required": [
              "error"
            ],
            "properties": {
              "error": {
                "type": "string"
              }
            }
          },
          "examples": {
            "unconfigured": {
              "value": {
                "error": "Content agent not configured"
              }
            }
          }
        }
      }
    }
  },
  "security": [
    {
      "callbackSecret": []
    }
  ]
}