Your first import, end to end
The overview gets you a healthy engine. This page is the rest of the first two hours: a file goes in, someone reviews it, and signed rows land on your backend.
You do not write a mapping UI, and you do not declare the schema in host code. The schema is a published catalog. The widget loads it. Apply is a signed webhook — that is the default.
your page Mildport engine your backend ───────── ─────────────── ──────────── <mildport-import ingest → match → review catalog-key="crm"> ─────────► ─────────► POST /apply HMAC-signed rows1. Keys from your account
Section titled “1. Keys from your account”You need two values, both from your Mildport account:
| What | Where | Goes on |
|---|---|---|
| License key | Licenses — shown once at mint | the widget’s license-key |
| Public key | Self-hosting (IMPORT_LICENSE_PUBLIC_KEY) |
the engine, already, if the overview’s preflight passed |
Confirm the engine accepts the key:
curl -H "Authorization: Bearer $LICENSE" \ https://imports.your-infra.example/api/import/v1/license/verifyA good response is "verified": true. Anything else — see
Deployment troubleshooting.
2. Publish a catalog, then embed it
Section titled “2. Publish a catalog, then embed it”The schema lives in the catalog editor — /admin/catalogs on
your engine, or <mildport-catalog-editor> in your own admin. Draft fields from an OpenAPI
spec or a sample record if you have one, mark the ones your API would reject as empty as
required, and publish. Give the catalog a stable key (crm, contacts, …).
Pin that key on the widget. Schema changes after this are a publish in the editor, not a host release:
<mildport-import api-base-url="https://imports.your-infra.example" license-key="SIGNED_TENANT_KEY" catalog-key="crm"></mildport-import>One published catalog and no catalog-key is also fine — the widget adopts it on its own.
Pin the key when a page must always import into a specific schema.
Need the schema computed per user at runtime, or versioned with your app? That’s the schema-as-code path. Skip it until you have a reason.
3. Register the apply webhook
Section titled “3. Register the apply webhook”Apply defaults to a signed POST to your backend. Register the URL once; the response carries the signing secret exactly once — store it.
curl -X POST https://imports.your-infra.example/api/import/v1/webhooks \ -H "Authorization: Bearer $LICENSE" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.your-app.example/mildport/apply" }'Want a first look without a backend? Set el.applyMode = 'browser' and read rows from
onResults. Production is the webhook. Details: Events & apply and the
webhook reference.
4. Run an import
Section titled “4. Run an import”Open the page, drop a CSV (or a messy spreadsheet, or a PDF with a table), confirm the suggested mapping, fix anything the review grid flags, and apply.
The widget talks to your engine for ingest and mapping. You do not call ingest yourself unless you are building a headless, no-widget flow.
5. Verify the signature, then write
Section titled “5. Verify the signature, then write”Every delivery is HMAC-signed. Verify the raw body before you trust it, reject stale
timestamps, and dedupe on x-import-delivery-id — retries are normal.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody: string, header: string, secret: string): boolean { const parts = Object.fromEntries(header.split(',').map(kv => kv.split('='))); const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex'); const got = Buffer.from(parts.v1 ?? '', 'hex'); const exp = Buffer.from(expected, 'hex'); return got.length === exp.length && timingSafeEqual(got, exp);}The body is rows shaped like your catalog (contact.email, …) plus a mapping. Return
2xx when you have persisted them. A 4xx is terminal; a 5xx is retried. Full payload
and headers: Apply webhooks.
6. Confirm it worked
Section titled “6. Confirm it worked”- The widget fired
import-appliedwith arowCount. - Your handler logged one
deliveryIdand wrote that many rows. - A second delivery with the same id is a no-op on your side.
Stuck? Deployment troubleshooting covers images that won’t pull, license 401s, CORS, and a pod that never becomes Ready.
Next: Configuration · Licensing · Target catalogs