Production workflow
This is the canonical production path for Conduit integrations.
Use Quickstart for the first successful report, then use this page to ship the workflow safely.
Default architecture​
- Create a report job with
webhook.urland anidempotencyKey. - Persist the returned
jobIdwith your request metadata. - Verify the webhook signature against the raw request body.
- Acknowledge the webhook quickly, then process it asynchronously.
- Fetch the final report on
report.completedor record the failure onreport.failed.
create report job
-> persist jobId, idempotencyKey, request metadata
-> Conduit processes async work
-> verified webhook delivery (recommended)
-> fetch report by reportId or jobId
-> persist completed output or terminal failure
Webhook-first by default​
Webhooks are the default production completion path.
Use verified webhooks when you need reliable completion handling, low API overhead, and clear correlation between accepted jobs and finished reports.
Polling is acceptable when:
- you are developing locally and do not have a public webhook endpoint yet
- you are running controlled internal tooling or one-off backfills
- you cannot expose an HTTPS webhook endpoint in the target environment
If you fall back to polling, keep it bounded and deliberate. Poll GET /v1/jobs/:jobId at a modest interval, or use GET /v1/jobs/:jobId/stream for controlled tooling. Do not build your main production path around aggressive polling loops.
Create jobs for retries you control​
Retry safety starts at job creation.
import { Conduit } from "@mappa-ai/conduit"
const conduit = new Conduit({ apiKey: process.env.CONDUIT_API_KEY! })
const receipt = await conduit.reports.create({
source: { mediaId: "media_abc123" },
output: { template: "sales_playbook" },
target: { strategy: "dominant" },
webhook: { url: process.env.CONDUIT_WEBHOOK_URL! },
idempotencyKey: "report:user-42:candidate-call:v1",
requestId: "req_candidate_call_42",
})
console.info(receipt.jobId)
- Reuse the same
idempotencyKeyonly for the same logical create request. - Persist
jobId, your own request correlation id, and the idempotency key together. - Treat idempotency as client discipline when you retry transient request failures.
Verify every delivery​
Never trust webhook JSON before signature verification.
Conduit signs each delivery with the conduit-signature header. Verification uses the exact raw request body, so do not run JSON body parsing before verification.
import { Conduit } from "@mappa-ai/conduit"
const conduit = new Conduit({ apiKey: process.env.CONDUIT_API_KEY! })
export async function handleConduitWebhook(req: Request): Promise<Response> {
const payload = await req.text()
await conduit.webhooks.verifySignature({
payload,
headers: Object.fromEntries(req.headers),
secret: process.env.CONDUIT_WEBHOOK_SECRET!,
})
const event = conduit.webhooks.parseEvent(payload)
queueWebhookWork(event)
return new Response("ok", { status: 200 })
}
- Keep
CONDUIT_WEBHOOK_SECRETserver-side only. - Return success quickly after verification and hand off heavier work asynchronously.
- Ignore or log unknown future event types safely instead of crashing the consumer.
For framework-specific examples, see Webhooks.
Handle retries and duplicates​
Conduit may retry failed webhook deliveries, so your consumer must be idempotent.
Use a durable dedupe key such as event.id, and make downstream writes safe to repeat.
async function processWebhookEvent(event: { id: string; type: string; data: Record<string, unknown> }) {
const alreadyProcessed = await db.webhookEvents.has(event.id)
if (alreadyProcessed) return
await db.webhookEvents.record(event.id)
if (event.type === "report.completed") {
const reportId = String(event.data.reportId)
const report = await conduit.reports.get(reportId)
await db.reports.upsert(report.id, report)
return
}
if (event.type === "report.failed") {
await db.failedJobs.upsert(String(event.data.jobId), event.data)
}
}
- Retries for request creation and retries for webhook delivery are separate concerns.
- Reuse the same idempotency key for intentional create retries.
- Make webhook processing safe if the same event arrives more than once.
- Do not assume every non-success response produces the same retry behavior. Build the consumer so duplicates are harmless.
Failure and recovery behavior​
There are two different failure paths to plan for.
For the short symptom-based recovery path, use Troubleshooting and FAQ.
Job failed​
If processing reaches a terminal failure, Conduit emits report.failed when a webhook is configured.
Recommended handling:
- persist
jobId, error code, and error message - alert your operator or workflow owner
- decide whether a new submission is appropriate instead of assuming an automatic replay
- surface the request id when you need support help
Delivery failed​
Webhook delivery problems are not the same as job failure. A report can complete successfully even if your endpoint misses or rejects the webhook.
Recommended recovery:
- log failed verification and non-success responses in your webhook service
- reconcile older accepted jobs that have no stored terminal event yet
- fetch the current job or report state before deciding whether work is actually missing
- use
GET /v1/reports/by-job/:jobIdafter success or reconciliation; it returnsnulluntil a completed report exists
Launch checklist​
- Keep
CONDUIT_API_KEYandCONDUIT_WEBHOOK_SECRETserver-side and rotate them on your normal secret schedule. - Expose an HTTPS webhook endpoint that preserves the raw request body for verification.
- Verify
conduit-signaturebefore parsing or trusting the payload. - Persist
jobId,idempotencyKey, and your own request correlation id for every async request. - Return a fast success response from the webhook handler and move heavy work off the request path.
- Make webhook processing idempotent with durable dedupe on
event.idor an equivalent key. - Monitor terminal failures, missing completions, and webhook verification failures.
- Use polling only as a bounded fallback, not as the default production completion strategy.
- Test the full flow in staging with a real public endpoint or tunnel before launch.