> ## Documentation Index
> Fetch the complete documentation index at: https://repost.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Repost Send

> A complete webhook sending system: define your events in a schema, publish with generated type-safe SDKs, and let Repost handle signed delivery, retries, and your customers' self-service.

export const RepostHighlight = () => {
  useEffect(() => {
    const colors = {
      comment: ['#116329', '#6A9955'],
      constant: ['#0550AE', '#9CDCFE'],
      keyword: ['#CF222E', '#569CD6'],
      name: ['#953800', '#4EC9B0'],
      string: ['#0A3069', '#CE9178'],
      type: ['#0550AE', '#4EC9B0'],
      variable: ['#1F2328', '#9CDCFE']
    };
    const tokenPattern = /"[^"\n]*"|@[A-Za-z_]\w*|\b(?:String|Int|Float|Boolean|DateTime|Json)\b|\b(?:true|false)\b|\b\d+(?:\.\d+)?\b|\b(?:now|uuid|cuid)(?=\s*\()|\b[A-Z]\w*\b|\b[A-Za-z_]\w*\b/g;
    const append = (line, text, color) => {
      if (!text) return;
      if (!color) {
        line.append(text);
        return;
      }
      const span = document.createElement('span');
      span.textContent = text;
      span.style.color = color[0];
      span.style.setProperty('--shiki-dark', color[1]);
      line.append(span);
    };
    const commentStart = text => {
      let quoted = false;
      for (let index = 0; index < text.length - 1; index += 1) {
        if (text[index] === '"' && text[index - 1] !== '\\') quoted = !quoted;
        if (!quoted && text[index] === '/' && text[index + 1] === '/') return index;
      }
      return -1;
    };
    const highlightCode = code => {
      if (code.dataset.repostHighlighted !== undefined || code.querySelector('span[style*="--shiki-dark"]')) {
        return;
      }
      code.dataset.repostHighlighted = '';
      const source = code.textContent ?? '';
      const fragment = document.createDocumentFragment();
      for (const text of source.split('\n')) {
        const line = document.createElement('span');
        line.className = 'line';
        const declaration = text.match(/^(\s*)(generator|model|enum|type|event)(\s+)([A-Za-z_]\w*)(.*)$/);
        if (declaration) {
          append(line, declaration[1]);
          append(line, declaration[2], colors.keyword);
          append(line, declaration[3]);
          append(line, declaration[4], colors.name);
          append(line, declaration[5]);
        } else {
          const start = commentStart(text);
          const codeText = start === -1 ? text : text.slice(0, start);
          const leading = codeText.match(/^(\s*)([A-Za-z_]\w*)(?=\s+(?:@|[A-Z]))/);
          const bare = codeText.match(/^(\s*)([A-Za-z_]\w*)(\s*)$/);
          let offset = 0;
          for (const match of codeText.matchAll(tokenPattern)) {
            append(line, codeText.slice(offset, match.index));
            const token = match[0];
            let color = colors.constant;
            if (token.startsWith('"')) color = colors.string; else if (token.startsWith('@') || (/^(now|uuid|cuid)$/).test(token)) color = colors.name; else if ((/^(String|Int|Float|Boolean|DateTime|Json)$/).test(token)) color = colors.type; else if (leading && match.index === leading[1].length) color = colors.variable; else if (bare && match.index === bare[1].length) color = colors.constant; else if ((/^[A-Z]/).test(token)) color = colors.name; else if ((/^[A-Za-z_]/).test(token)) color = colors.variable;
            append(line, token, color);
            offset = match.index + token.length;
          }
          append(line, codeText.slice(offset));
          if (start !== -1) append(line, text.slice(start), colors.comment);
        }
        fragment.append(line, '\n');
      }
      code.replaceChildren(fragment);
    };
    const highlightRepostBlocks = () => {
      for (const block of document.querySelectorAll('.code-block')) {
        const code = block.querySelector('pre code');
        const filename = block.querySelector('[data-component-part="code-block-header-filename"] [title$=".repost"]');
        const looksLikeRepost = (/^\s*(generator|model|enum|type|event)\s+[A-Za-z_]\w*\s*\{/m).test(code?.textContent ?? '');
        if (code && (filename || looksLikeRepost)) highlightCode(code);
      }
    };
    let frame;
    const schedule = () => {
      if (frame !== undefined) return;
      frame = requestAnimationFrame(() => {
        frame = undefined;
        highlightRepostBlocks();
      });
    };
    const observer = new MutationObserver(schedule);
    observer.observe(document.documentElement, {
      childList: true,
      subtree: true
    });
    schedule();
    return () => {
      observer.disconnect();
      if (frame !== undefined) cancelAnimationFrame(frame);
    };
  }, []);
  return null;
};

<RepostHighlight />

Repost Send is a complete webhook sending system you plug into your application. You publish events; Repost handles delivery, signing, retries, documentation, and your customers' self-service. It consists of:

* [**The schema**](/docs/send/schema): define each event's shape once, in a file in your repo
* [**SDKs**](/docs/send/sdks): generated, type-safe publishing clients for TypeScript, Go, Python, Java, and Kotlin
* [**Delivery**](/docs/send/delivery): [signed](/docs/send/signing) webhooks with automatic retries, a dead-letter queue, and replay
* [**The customer portal**](/docs/send/portal): self-service endpoints, secrets, live delivery logs, and [generated event docs](/docs/send/event-docs)

## Why Repost Send

Webhooks are an API your customers build against, and they are usually shipped without types, versioning, or reliable documentation. The typical setup is a `POST` helper in a queue worker, a payload shaped by whatever the serializer produces, and an events page someone updates by hand. It works until a customer integrates against docs that no longer match production.

Send avoids the problem by generating everything from the schema:

* The SDK, the payloads, and the event docs come from the same file, so they cannot disagree
* A payload that doesn't match its event definition doesn't compile
* Schema changes are recorded as migrations, reviewed in pull requests, and checked in CI
* Delivery comes with signatures, about a day of retries, a dead-letter queue, replay, and per-attempt logs
* Customers manage their own endpoints, secrets, and delivery history in a hosted portal

## When to use Send

Send is a good fit if you run a platform that delivers webhooks to many customers, if you want event docs and SDKs that stay current without maintenance, or if you would rather not operate retry queues, signing, and secret rotation yourself.

It may not be what you need if you have a single internal consumer (a message queue is simpler), if you need full control over delivery infrastructure, or if you only receive webhooks. Receiving is [Repost Ingest](/docs/), the other half of this product.

## How it works

### 1. Define your events

The [schema](/docs/send/schema) describes your payload models and event types:

```repost repost/schema.repost theme={"languages":{"custom":["/languages/repost.json"]}}
generator sdk {
  language = "typescript"
}

model Order {
  id       String
  currency String
}

type Order {
  /// An order was placed
  created
}

event OrderCreated {
  type      @type(Order.created)
  data      Order
  timestamp DateTime
}
```

### 2. Version and deploy them

Schema changes are recorded as [migrations](/docs/send/deployments) and deployed to an environment:

```bash theme={"languages":{"custom":["/languages/repost.json"]}}
repost schema migrate dev --name init
repost schema migrate deploy
```

Each deploy also rebuilds your [public event docs](/docs/send/event-docs) and the portal's event catalog.

### 3. Publish from your code

Generation produces [a typed client](/docs/send/sdks) with one method per event type. The [HTTP API](/docs/send/publishing) is available when you'd rather not use one:

<CodeGroup>
  ```ts TypeScript theme={"languages":{"custom":["/languages/repost.json"]}}
  import { createRepostClient } from "@repost/client";

  const repost = createRepostClient();

  await repost.webhooks.order.created({
    customerId: "acme",
    data: { id: "order-1001", currency: "EUR" },
    idempotencyKey: "order-1001-created",
  });
  ```

  ```go Go theme={"languages":{"custom":["/languages/repost.json"]}}
  client, err := repostclient.NewClient(repost.ClientOptions{})
  if err != nil { log.Fatal(err) }

  result, err := client.Webhooks.Order.Created(ctx, repostclient.OrderCreatedInput{
      CustomerID: "acme",
      Data:       order,
  })
  ```

  ```python Python theme={"languages":{"custom":["/languages/repost.json"]}}
  client = RepostClient()

  result = client.webhooks.order.created(customer_id="acme", data=order)
  ```

  ```bash curl theme={"languages":{"custom":["/languages/repost.json"]}}
  curl -X POST https://api.repost.sh/v1/messages \
    -H "Authorization: Bearer $REPOST_SEND_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: order-1001-created" \
    -d '{"type": "order.created", "customerId": "acme", "data": {"id": "order-1001", "currency": "EUR"}}'
  ```
</CodeGroup>

Every client behaves the same way: deterministic payloads, automatic retries, and an idempotency key that makes repeats safe.

### 4. Repost delivers

Each customer endpoint subscribed to the event receives a [signed](/docs/send/signing) request. If the endpoint is down, Repost [retries for about a day](/docs/send/delivery) and then parks the delivery in a dead-letter queue for replay:

```http theme={"languages":{"custom":["/languages/repost.json"]}}
POST /webhooks HTTP/1.1
content-type: application/json
user-agent: Repost-Webhooks/1
webhook-id: msg_01JZX3NV7Q9WD9F2M6K4T8RSAB
webhook-timestamp: 1784289600
webhook-signature: v1,K5oZfzN95Z9UVu1EsPQqUIoRQOU...

{"type":"order.created","timestamp":"2026-07-17T12:00:00.000Z","data":{"id":"order-1001","currency":"EUR"}}
```

### 5. Your customers manage themselves

Mint a [portal](/docs/send/portal) link from your backend. Your customer gets their endpoints, signing secrets, live delivery logs, and your event catalog, [in your branding](/docs/send/customization), with no account to create:

```bash theme={"languages":{"custom":["/languages/repost.json"]}}
curl -X POST https://api.repost.sh/v1/customers/acme/portal-access \
  -H "Authorization: Bearer $REPOST_TOKEN" \
  -d '{}'
```

## Next steps

* [**Quickstart**](/docs/send/quickstart): run the whole flow yourself, from an empty directory
* [**Core concepts**](/docs/send/concepts): the six ideas behind everything above
* [**Defining events**](/docs/send/schema): the schema language in full
* [**Customer portal**](/docs/send/portal): what your customers get, and how they reach it
