Import finished Webhook

Add MCP server to your AI tool

Allow AI tools and LLMs to interact with the API documentation portal through MCP.

MCP server URL

https://api-reference.usecommune.dev/mcp

Standard setup for AI tools providing an mcp.json file

mcp.json
{
  "REST API MCP server": {
    "url": "https://api-reference.usecommune.dev/mcp"
  }
}

Close
POST https://webhook.example.com

A bulk ingest run finished. data.kind says which of the three it was.

articles: an import a creator started against their connected provider finished the batch they picked. imported_count is the number that actually landed.

subscribers: a pull of every active contact from the connected provider finished. An uploaded CSV ends the same way, with source: csv.

migration: the newsletter was flipped onto native sending. That is the destructive step of the move off a provider, and after it Commune sends the newsletter itself.

What this topic is for is invalidation. Every one of these runs writes many rows at once, and none of them fans out into per-row events: an import does not emit one article.published or one subscriber.created per row it touched, because a provider migration of fifty thousand contacts would bury a consumer. This is the single event that says the shape of the newsletter just changed underneath you, so refetch.

Only a run that reached its end publishes here. Both import paths stream their progress and swallow per-row failures, so a partial run still finishes and still fires, with a lower imported_count and no separate count of what it dropped. A run whose connection died mid-stream produces nothing at all, and there is no import.failed to pair with this.

There is no run identifier on this payload and no /imports/{id} to look one up in. An import is a streamed request that ends by writing its rows and closing the response: nothing persists a job row, so an id here would reference something no consumer could ever fetch. What identifies a run is the newsletter, the kind and the source, and the only thing to do with one is refetch.

Every one of these is idempotent and safe to re-run, so a consumer should expect to see the same import kind more than once for one newsletter.

Delivered as a single HTTPS POST to the consumer's registered endpoint, with the message as the JSON request body. Respond 2xx to acknowledge. Anything else, or a timeout, is retried with backoff, so acknowledge fast and do the work afterwards.

Headers

  • Commune-Version string Required

    API version the payload conforms to. Same value as api_version in the envelope.

  • Commune-Event-Id string(uuid) Required

    Same value as id in the envelope. The dedupe key.

  • Commune-Event-Type string Required

    Same value as type in the envelope. The topic name.

  • Commune-Signature string Required

    HMAC over the raw request body. Verify before parsing.

  • Commune-Timestamp string Required

    Unix seconds the signature was produced at. Reject anything outside your tolerance window.

  • Commune-Delivery-Attempt integer

    1 on the first attempt, incremented on each retry. Informational only. A consumer must be idempotent regardless of its value.

    Minimum value is 1.

application/json

Body Required

An article, subscriber or migration run finished.

  • id string(uuid) Required

    Unique id for this event. Stable across redeliveries, so it is the dedupe key. It is the primary key of the row the publisher wrote, which means it is assigned when the state change is recorded, not when the message is dispatched.

  • type string Required

    The topic name, identical to the channel address. Route on this.

    Value is import.completed.

  • api_version string Required

    The Commune-Version value this payload conforms to. Present on the message itself, not only on the request header, so an event persisted to a consumer's own store stays self-describing.

  • occurred_at string(date-time) Required

    RFC 3339 timestamp of the state change, not of the delivery attempt. Use it to order events, since delivery order is not guaranteed.

  • newsletter_id string(uuid) | null Required

    The newsletter the change belongs to. This is the tenant boundary: a consumer only ever receives events for newsletters its credential can read. Null only for events that are genuinely not newsletter-scoped, which none of the 21 topics currently are.

  • actor object | null Required

    Who caused the change. ALWAYS null today and consumers must treat it that way: the public API is currently read-only, so nothing that reaches this catalog was triggered through it, and Commune does not backfill an actor for changes made in the product UI or by a cron. The field exists now so that populating it, once writes exist, is an additive change rather than a new envelope version.

    Additional properties are NOT allowed.

    Hide actor attributes Show actor attributes object | null
    • type string Required

      user for a change a person made through an authenticated session, api_key for a public API write, system for a cron or webhook ingest with no human behind it.

      Values are user, api_key, or system.

    • id string Required

      Identifier of the actor within its type. A user id for user, a key id (never the secret) for api_key, a stable job name for system.

    • label string | null

      Human-readable name for display. Best effort, may be null.

  • idempotency_key string | null Required

    ALWAYS null today. Once the public API accepts writes this carries the Idempotency-Key header of the write that caused the change, so a consumer can tie an event back to its own request and collapse the duplicates a retried write would otherwise produce. Null stays a legitimate value even then: a change made in the product UI or by a cron has no originating request to key on.

    Maximum length is 255.

  • data object Required

    Event-specific body. Narrowed by each concrete event schema below.

    Hide data attributes Show data attributes object
    • kind string Required

      articles and subscribers ingest rows. migration is the finalize step that moves a newsletter onto native sending, which ingests nothing.

      Values are articles, subscribers, or migration.

    • source string | null Required

      Where the rows came from: an ESP slug such as kit or beehiiv, csv for an uploaded list, or rss for a feed. Null when the run has no single source, which is the migration case.

    • imported_count integer | null Required

      Rows that actually landed. Not the size of what was offered: both import paths skip duplicates, and the subscriber upsert preserves the status of a row that already existed rather than overwriting it. Null for migration, which ingests nothing.

      Rows the run could not take are not reported. Per-row failures are swallowed so one bad row cannot end the run, and none of the ingest paths counts them apart from the duplicates it skipped on purpose, so a count here would be a guess.

      Minimum value is 0.

    • started_at string(date-time) | null

      When the run began. Null when it was not recorded, so treat the duration as best effort.

    • completed_at string(date-time) Required

      When the run ended. Equal to occurred_at.

Responses

  • 200

    The consumer accepted the delivery. Any 2xx acknowledges the message and it is not sent again.

  • 4XX

    The consumer rejected the delivery. Handled exactly like a 5xx: the message is retried with backoff, because a rejection cannot be told apart from a consumer that is briefly misconfigured.

  • 5XX

    The consumer failed to handle the delivery. Retried with backoff. A timeout is the same case and is retried too.

POST import.completed
Request examples
{
  "id": "018f2a92-3434-7000-8000-000000000034",
  "type": "import.completed",
  "api_version": "2026-08-26",
  "occurred_at": "2026-08-26T20:03:41Z",
  "newsletter_id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411",
  "actor": null,
  "idempotency_key": null,
  "data": {
    "kind": "subscribers",
    "source": "kit",
    "imported_count": 4820,
    "started_at": "2026-08-26T19:58:12Z",
    "completed_at": "2026-08-26T20:03:41Z"
  }
}
{
  "id": "018f2a92-3535-7000-8000-000000000035",
  "type": "import.completed",
  "api_version": "2026-08-26",
  "occurred_at": "2026-08-26T20:21:09Z",
  "newsletter_id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411",
  "actor": null,
  "idempotency_key": null,
  "data": {
    "kind": "articles",
    "source": "kit",
    "imported_count": 63,
    "started_at": "2026-08-26T20:19:44Z",
    "completed_at": "2026-08-26T20:21:09Z"
  }
}
{
  "id": "018f2a92-3636-7000-8000-000000000036",
  "type": "import.completed",
  "api_version": "2026-08-26",
  "occurred_at": "2026-08-26T20:30:00Z",
  "newsletter_id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411",
  "actor": null,
  "idempotency_key": null,
  "data": {
    "kind": "migration",
    "source": null,
    "imported_count": null,
    "started_at": null,
    "completed_at": "2026-08-26T20:30:00Z"
  }
}