Convex

Start a generation from a Convex action and receive the signed webhook on a Convex HTTP route with mynthWebhookAction.

Generate from a Convex action with the normal SDK, and receive the result on an HTTP route with mynthWebhookAction from @mynthio/sdk/convex. The API key stays in the Convex environment.

Setup#

npm install @mynthio/sdk

@mynthio/sdk/convex imports convex/server, which every Convex project already has. Set two environment variables on the Convex deployment:

VariableValue
MYNTH_API_KEYA mak_ key
MYNTH_WEBHOOK_SECRETThe wbs_ secret printed when you register the endpoint

Generate from an action#

Queries and mutations cannot make network calls, so use an action. Create the task with generateAsync() so the action returns right away, and collect the file in the webhook.

// convex/generate.ts
import { v } from "convex/values";
import Mynth from "@mynthio/sdk";

import { action } from "./_generated/server";

export const generate = action({
  args: { prompt: v.string() },
  handler: async (_ctx, args) => {
    const mynth = new Mynth(); // reads MYNTH_API_KEY

    const pending = await mynth.image.generateAsync({
      model: "black-forest-labs/flux.2-pro",
      prompt: args.prompt,
      metadata: { source: "convex" },
    });

    return { taskId: pending.id, publicAccessToken: pending.access.publicAccessToken };
  },
});

Return the task id to the client if the UI needs it. publicAccessToken lets the browser poll that one task. It expires after an hour. Never return the API key.

Receive the webhook#

Register your deployment's HTTP actions URL plus the route path, for example https://<deployment>.convex.site/mynth-webhook, as a webhook endpoint.

// convex/http.ts
import { httpRouter } from "convex/server";
import { mynthWebhookAction } from "@mynthio/sdk/convex";

import { internal } from "./_generated/api";

const http = httpRouter();

export const mynthWebhook = mynthWebhookAction({
  imageTaskCompleted: async (payload, { context }) => {
    await context.runMutation(internal.images.save, {
      taskId: payload.task.id,
      images: payload.result.images,
    });
  },
  imageTaskFailed: async (payload) => {
    console.error(payload.task.id, payload.errors);
  },
});

http.route({ path: "/mynth-webhook", method: "POST", handler: mynthWebhook });

export default http;

internal.images.save is a mutation you write. Each handler gets the payload and { context, request, deliveryId }. context is the Convex action context, so context.runQuery and context.runMutation are how it touches your data. deliveryId is the X-Mynth-Delivery value, the same on every retry, so store it to skip deliveries you have already handled. The handler names are the same as in every SDK helper. See the table on webhooks.

Behavior#

mynthWebhookAction returns a Convex HTTP action, so pass it straight to http.route without wrapping it in httpAction. It runs the same checks as the Next.js and TanStack Start helpers:

  • It reads MYNTH_WEBHOOK_SECRET when a request arrives, so deploying works before the variable is set. Pass { webhookSecret } as the second argument to use another source.
  • A missing or bad signature, a timestamp more than five minutes off, a missing X-Mynth-Delivery, or an X-Mynth-Event header that does not match the body answers 400.
  • A signed event with no handler answers 200. An error thrown by a handler fails the request, and Mynth retries the delivery.
  • Unsigned deliveries from per-request webhook.custom URLs are rejected. Register the endpoint instead.

Next steps#

  • Webhooks: registering the endpoint, events, sources, and retries.
  • Tasks: statuses and how long the file lasts.