--- title: Managing Environments --- # Managing Environments This section explains how to add and use multiple environments (e.g. dev, staging, and prod) from the same account. ## What are environments It's common practice for engineering teams to have multiple environments, such as development, staging, and production. The idea behind having multiple environments is to be able to test changes in isolation and catch issues before they hit production. So for example, a developer may make some changes locally, test them, and push them for testing in CI/CD. Following that, the change will be moved to a shared `development` environment where it would be tested by the wider team. Changes that pass this level of testing will move to `staging`, where they are deployed to an environment that's as similar as possible to the `production` environment, and after all testing pass there they would be deployed to `production` and rolled out to users. Not all teams go through all of the steps above (and some go through more), but the idea remains the same: you want to isolate your `production` environment, where real user data lies, from your non-production environments that may contain bugs and regressions. ## Managing environments Your Svix environments are completely isolated and have separate data, settings, and API keys. You can create as many environments as you want from the [Environment Management page](https://dashboard.svix.com/environments) on the dashboard. ### Add environments Click on `New Environment`, choose a name, mark the environment as either `Development` or `Production`, and choose the wanted region. ![New environment](./environments/env-add.png) Development and Production environments behave almost exactly the same. The main difference is the associated tag and the visual indicator, as well as some other minor differences. ![Preview App Portal](/img/dashboard-preview-button.png) ### View environments This is how it looks like after adding multiple environments: ![Manage environments](./environments/env-manage.png) You can then switch between the active environment using the switcher on the top left corner and just start using that environment like you used your account before. ### Environment-specific settings When configuring your environments, it's important to know which settings are shared across your environments and which are kept separate. On the settings page, there is a separate section for "environment-specific" settings. ![environment specific settings](/img/env-specific-settings.png) #### App Portal White Labeling The App Portal [White Labeling](../app-portal#white-labeling) is configured separately for each environment. This allows you to experiment with how the App Portal will look in your development environment without affecting your production users. #### HTTPS Only Endpoints It is strongly recommended that all registered endpoints in production environments are secured with HTTPS. If you want to allow unsecure endpoints (HTTP) on any environment, you may do so on the "General Settings" page. As of October 1st, 2021, HTTPS Only Endpoints is enabled by default on newly created environments. If your environment was created before then, the default was disabled and your environment was left unchanged. ## Environment import & export Import and export makes it very easy to clone (or sync) environments, and thus make sure that your staging and production environments are identical. It also makes it possible to save your environment settings in version control in order to make sure you keep track of changes to your environment. Export currently supports the environment settings as well as all of the event types. To import and export, all you need to do is to go to the [environment management](https://dashboard.svix.com/environments) page on the dashboard, choose the wanted environment from the list, and then click either import or export. ![Import and export from an environment](/img/import-export.png) --- title: Managing Member Access --- # Managing Member Access This section explains how to manage access to your organization's account. You can easily give your teammates access, manage it, and invite new team members. ## Managing members There are currently four supported roles: `Viewer`, `Support Agent`, `Member` and `Admin`. - A `Viewer` has basic privileges and can view statistics and basic configurations but can't access API keys. - A `Support Agent` can preview [Consumer App Portals](../app-portal), in addition to having the same permissions as a `Viewer`. - A `Member` can control most aspects of the account, including applications, endpoints and event types. - An `Admin` can do all of that, and also manage members. You can invite as many members as you want to your organization, so feel free to invite all of your team. To do it, go to the [organization members page](https://dashboard.svix.com/settings/organization-group/members) on the dashboard. ### Invite a user Click on `Invite Teammates`, put in the email address of the teammate you would like to invite, and choose the appropriate role. ![New member](./org-members/add-member.png) Svix will then send an invitation to the provided email address. ### Accept an invitation Once your teammate receives the invitation via email, they can follow the link to decide whether to accept or decline the invitation. Accepting the invitation will remove the recipient from their current account and add them to the new account. Declining the invitation will expire it. The recipient's email must be verified before they can accept an invitation. ### View members This is how it looks like after you've sent some invitations: ![Manage members](./org-members/manage-members.png) You can then add more members, manage access and manage invitations directly from this page. --- title: Google BigQuery --- # Google BigQuery Svix can deliver webhooks directly to a Google BigQuery table, without your customers having to set up any listener endpoint or write any glue code. When **Advanced Endpoint Types** is [enabled](/advanced-endpoints#enabling-advanced-endpoint-types), your customers will see the option to use a BigQuery destination in the [App Portal](/app-portal). ![BigQuery Endpoint Create](/img/advanced-endpoints/bigquery-create.png) They will be able to configure the connection right in the App Portal: - `projectId` — the GCP project that owns the dataset. - `datasetId` — the BigQuery dataset that contains the table. - `tableId` — the table that receives the rows. - `credentials` — a Google Cloud service account credentials JSON object, provided as a string. Every batch of webhooks received by the endpoint is inserted into the configured BigQuery table. ## Destination table Without a transformation, Svix inserts each webhook into the table identified by `projectId`, `datasetId`, and `tableId` using two columns: `id` and `payload`. Svix generates a unique `id` for each row and writes the raw payload to `payload`. The table must already exist before you enable the endpoint. For the default behavior, create it with: ```sql CREATE TABLE `my-gcp-project.my_dataset.events` ( id STRING, payload STRING ); ``` If you use a transformation (below), you control the columns each row contains, so your table can use any schema you like — as long as it matches the rows your transformation returns. # Transformations Each webhook is inserted into BigQuery as a row. A transformation returns the `rows` to insert, where each row is an object whose keys match your table's columns. ```JavaScript /** * @param input - The input object * @param input.events - The array of webhooks in the batch. The number of webhooks in the batch is capped by the endpoint's batch size. * @param input.events[].payload - The message payload (string or JSON) * @param input.events[].eventType - The message event type (string) * * @returns Object describing the rows to insert. * @returns returns.rows - The array of rows to insert. Each row is an object whose keys match the columns of your BigQuery table. */ function handler(input) { const rows = input.events.map((event) => ({ id: crypto.randomUUID(), payload: JSON.stringify(event.payload) })); return { rows }; } ``` `input.events` is the list of webhooks received by the endpoint, processed in batches. Each entry in the returned `rows` array is inserted as a separate row. The object keys must match the column names of your table. To write different columns, adjust the objects in `rows` and your table schema to match. For example, if the endpoint receives the following messages: ```json { "eventType": "user.created", "payload": "{\"email\": \"joe@enterprise.io\"}" } ``` ```json { "eventType": "user.login", "payload": "{\"id\": 12, \"timestamp\": \"2025-07-21T14:23:17.861Z\"}" } ``` The transformation above inserts two rows into your table. | `id` | `payload` | | --- | --- | | `1f0a8c1e-...` | `{"email":"joe@enterprise.io"}` | | `4b9d2e7a-...` | `{"id":12,"timestamp":"2025-07-21T14:23:17.861Z"}` | --- title: ClickHouse --- # ClickHouse Svix can deliver webhooks directly to a ClickHouse table, without your customers having to set up any listener endpoint or write any glue code. When **Advanced Endpoint Types** is [enabled](/advanced-endpoints#enabling-advanced-endpoint-types), your customers will see the option to use a ClickHouse destination in the [App Portal](/app-portal). ![ClickHouse Endpoint Create](/img/advanced-endpoints/clickhouse-create.png) They will be able to configure the connection right in the App Portal: - `url` — the HTTP URL of your ClickHouse server (e.g. `https://my-clickhouse.example.com:8443`). - `username`, `password` — the credentials used to authenticate. - `tableName` — the table that receives the rows. - `database` — the database that contains the table. Defaults to `default`. Every batch of webhooks received by the endpoint is inserted into the configured ClickHouse table. ## Destination table Without a transformation, Svix inserts each webhook's payload directly into the table using ClickHouse's [`JSONEachRow`](https://clickhouse.com/docs/interfaces/formats/JSONEachRow) format. The top-level fields of each payload must match the columns of your table. The table must already exist before you enable the endpoint. For example, for payloads shaped like `{"email": "...", "username": "..."}`, create the table with: ```sql CREATE TABLE events ( email String, username String ) ENGINE = MergeTree() ORDER BY tuple(); ``` Unlike some other warehouse destinations, ClickHouse does not add any columns of its own — you define the full schema, and the payload fields are matched to it by name. # Transformations If your payloads don't already match your table's columns, add a transformation that returns the `rows` to insert. Each row is an object whose keys match your table's columns. ```JavaScript /** * @param input - The input object * @param input.events - The array of webhooks in the batch. The number of webhooks in the batch is capped by the endpoint's batch size. * @param input.events[].payload - The message payload (string or JSON) * @param input.events[].eventType - The message event type (string) * * @returns Object describing the rows to insert. * @returns returns.rows - The array of rows to insert. Each row is an object whose keys match the columns of your ClickHouse table. */ function handler(input) { const rows = input.events.map((event) => ({ email: event.payload.address, username: event.payload.handle })); return { rows }; } ``` `input.events` is the list of webhooks received by the endpoint, processed in batches. Each entry in the returned `rows` array is inserted as a separate row using `JSONEachRow`, so the object keys must match the column names of your table. For example, if the endpoint receives the following messages: ```json { "eventType": "user.created", "payload": "{\"address\": \"joe@enterprise.io\", \"handle\": \"joe\"}" } ``` ```json { "eventType": "user.created", "payload": "{\"address\": \"amy@enterprise.io\", \"handle\": \"amy\"}" } ``` The transformation above inserts two rows into your table, mapping each payload's `address` and `handle` fields onto the `email` and `username` columns. | `email` | `username` | | --- | --- | | `joe@enterprise.io` | `joe` | | `amy@enterprise.io` | `amy` | --- title: Amazon EventBridge --- # Amazon EventBridge Svix can deliver webhooks directly to Amazon EventBridge, without your customers having to set up any listener endpoint or write any glue code. When **Advanced Endpoint Types** is [enabled](/advanced-endpoints#enabling-advanced-endpoint-types), your customers will see the option to use an EventBridge destination in the [App Portal](/app-portal). ![EventBridge Endpoint Create](/img/advanced-endpoints/eventbridge-create.png) They will be able to configure the connection right in the App Portal: - `eventBusName` — the name or ARN of the event bus that receives the events. - `detailType` — a free-form string (max 128 characters) used as the `detail-type` of each event. Defaults to `application/json`. - `region`, `accessKeyId`, `secretAccessKey` — the AWS region and credentials used to authenticate. Every webhook in the batch is sent to EventBridge as a separate entry. Each event is published with its `source` set to `svix-webhooks-`, its `detail-type` set to the configured `detailType`, and its `detail` set to the message produced by the transformation. You can match on the `source` and `detail-type` when writing EventBridge rules. # Transformations By default, all EventBridge Endpoints come bundled with the following transformation code. ```JavaScript /** * @param input - The input object * @param input.events - The array of webhooks in the batch. The number of webhooks in the batch is capped by the endpoint's batch size. * @param input.events[].payload - The message payload (string or JSON) * @param input.events[].eventType - The message event type (string) * * @returns Object containing the request body * @returns returns.payloads - The array of messages (strings) to send to the endpoint. Each payload is a distinct message sent to EventBridge. */ function handler(input) { const payloads = input.events.map((event) => JSON.stringify(event)) return { payloads } } ``` `input.events` is the list of webhooks received by the endpoint, processed in batches. Each entry in the returned `payloads` array becomes the `detail` of a separate EventBridge event. By default, each webhook is serialized to a JSON string containing its `payload` and `eventType`. For example, if the endpoint receives the following messages: ```json { "eventType": "user.created", "payload": "{\"email\": \"joe@enterprise.io\"}" } ``` ```json { "eventType": "user.login", "payload": "{\"id\": 12, \"timestamp\": \"2025-07-21T14:23:17.861Z\"}" } ``` The default transformation code would send two events to your event bus, with the following `detail` bodies. ```json {"payload":{"email":"joe@enterprise.io"},"eventType":"user.created"} ``` ```json {"payload":{"id":12,"timestamp":"2025-07-21T14:23:17.861Z"},"eventType":"user.login"} ``` To control the `detail` of each event, return your own array of strings in `payloads`. Each string becomes the `detail` of one EventBridge event. EventBridge accepts at most 10 entries per request, so larger batches are automatically split across multiple `PutEvents` calls. --- title: FIFO Endpoints --- # FIFO Endpoints FIFO endpoints let your webhook consumers receive webhooks in strict FIFO ordering (first in first out), unlike regular webhooks, which are delivered independently and order is on a best effort basis. Svix supports sending webhooks to both FIFO and regular endpoints, with no code changes required on your end. **Why not make all webhooks FIFO?** Ensuring strict FIFO ordering comes with some tradeoffs. Since every call to the receiver endpoint is blocked until the previous one is successful, throughput is limited compared to regular webhook endpoints. Read more about [how FIFO endpoints work](https://www.svix.com/blog/fifo-ordered-webhooks-delivery/) and [the challenges with guaranteeing webhook ordering](https://www.svix.com/blog/guaranteeing-webhook-ordering/). ## Enabling FIFO Endpoints FIFO Endpoints can be enabled at the environment level in the [Svix Dashboard](https://dashboard.svix.com/settings/organization/general-settings) by enabling **Advanced Endpoint Types**. ![Enable FIFO Endpoints](/img/advanced-endpoints/advanced-endpoints-setting.png) When you enable FIFO Endpoints, your users will be able to create them in the [App Portal](/app-portal). ![Polling Endpoint Create](/img/advanced-endpoints/fifo-endpoint-create.png) ## Message Batching Because of the strict ordering, Svix has to wait for a successful acknowledgement of delivery before sending any other messages. To address throughput constraints FIFO endpoints deliver webhooks in configurable batch sizes. The batching parameters can be configured in the [App Portal](/app-portal) for each endpoint. ![FIFO Endpoint Batching](/img/advanced-endpoints/fifo-endpoint-batching.png) ## Transformations [Transformations](/transformations) are also supported on FIFO endpoints. For FIFO endpoints, transformations are applied to each batch of messages. ### How to write a FIFO endpoint transformation Svix expects a Transformation to declare a function named `handler`. Svix will pass a `InputObject` to this function as its only argument, and expects the function to always return an `OutObject`. `InputObject` is a JSON object containing one property: - `events`, an array of objects containing `payload` and `eventType` properties. There is one element in the array for each message in the batch. `OutObject` is a JSON object containing one property: - `requestBody`, a string with the raw request body to be sent to the endpoint. ### An example Transformation You can write a transformation that transforms certain messages in the batch and forwards them. ```js function handler(input) { const events = input.events.map((evt) => { if (evt.eventType === "user.created") { return { ...evt.payload, name: evt.payload.firstName + " " + evt.payload.lastName } } }); return { requestBody: JSON.stringify({ data: events }) } } ``` Or you can write transformations that collect messages and returns a summarized response. ```js function handler(input) { const total = input.events.reduce((acc, evt) => { if (evt.eventType === "invoice.created") { return acc + evt.payload.amount; } return acc; }, 0); return { requestBody: JSON.stringify({ total }) } } ``` --- title: Advanced Endpoint Types asIndexPage: true --- # Advanced Endpoint Types Svix supports multiple types of endpoints in addition to regular webhook endpoints, to support different use cases. - [Polling Endpoints](/advanced-endpoints/polling-endpoints) - [FIFO Endpoints](/advanced-endpoints/fifo-endpoints) - [Object Storage](/advanced-endpoints/object-storage) - [OpenTelemetry Tracing](/advanced-endpoints/otel-tracing) - [Snowflake](/advanced-endpoints/snowflake) - [RabbitMQ](/advanced-endpoints/rabbitmq) - [Amazon EventBridge](/advanced-endpoints/eventbridge) - [Amazon SNS](/advanced-endpoints/sns) - [Amazon SQS](/advanced-endpoints/sqs) - [Google BigQuery](/advanced-endpoints/bigquery) - [ClickHouse](/advanced-endpoints/clickhouse) - [Amazon Redshift](/advanced-endpoints/redshift) ## Enabling Advanced Endpoint Types Advanced Endpoint Types can be enabled at the environment level in the [Svix Dashboard](https://dashboard.svix.com/settings/organization/general-settings). ![Enable Advanced Endpoint Types](/img/advanced-endpoints/advanced-endpoints-setting.png) When you enable Advanced Endpoint Types, your users will be able to create them in the [App Portal](/app-portal). ![Advanced Endpoint Types](/img/advanced-endpoints/object-storage-endpoints.png) --- title: Object Storage --- # Object Storage Svix supports sending messages directly to [AWS S3](https://aws.amazon.com/s3/), [Google Cloud Storage](https://cloud.google.com/storage) and [Azure Blob Storage](https://azure.microsoft.com/en-us/products/storage/blobs). This is useful for storing payloads in a cloud storage bucket without your customers having to set up any listener endpoint or write any glue code. When **Advanced Endpoint Types** is [enabled](/advanced-endpoints#enabling-advanced-endpoint-types), your customers will see the option to use object storage destinations in the [App Portal](/app-portal). ![Object Storage Endpoint Create](/img/advanced-endpoints/object-storage-endpoints.png) They will be able to configure the connection right in the App Portal. ![Object Storage Endpoint Create](/img/advanced-endpoints/s3-configuration.png) ## Usage By default, all Object Storage Endpoints come bundled with the following transformation code. ```JavaScript /** * @param input - The input object * @param input.events - The array of events in the batch. The number of events in the batch is capped by the Object Storage Endpoint's batch size. * @param input.events[].payload - The message payload (string or JSON) * @param input.events[].eventType - The message event type (string) * * @returns Object describing what will be put to the bucket. * @returns returns.config * @returns returns.config.format - The format of the request object put to the bucket. Valid values are "jsonl", "json", or "raw" (Defaults to jsonl). * @returns returns.config.key - The name of the object that will be put to the bucket. This will be suffixed with a timestamp to avoid duplicate object names. * @returns returns.data - The array of events to send to the bucket. This will be formatted according to the format. */ function handler(input) { return { config: { format: "jsonl", key: "object-generated-by-svix" }, data: input.events } } ``` `input.events` is the list of webhooks received by the endpoint, processed in batches. `config` describes the object put in the destination - the key name of the object, and the format of the object saved to the bucket. By default, the actual object key is always suffixed with a timestamp after the transformations are run. This ensures each event batch is saved as a unique object in the bucket. For example, if the endpoint receives the following events: ```json { "eventType": "user.created", "payload": "{\"email\": \"joe@enterprise.io\"}" } ``` ```json { "eventType": "user.login", "payload": "{\"id\": 12, \"timestamp\": \"2025-07-21T14:23:17.861Z\"}" } ``` The default transformation code would result in the following object being uploaded to the bucket. ![s3-recent-events](/img/stream/s3-example.png) And the files contents would match the jsonl format. ```jsonl {"payload":{"email":"joe@enterprise.io"},"eventType":"user.created"} {"payload":{"id":12,"timestamp":"2025-07-21T14:23:17.861Z"},"eventType":"user.login"} ``` If you want to control the format of the object more precisely, you can use `config.format = "raw"`, and set `data` to a string of the exact file contents you want. --- title: OpenTelemetry Tracing --- import OpenTelemetryProviderNotes from '../_common/otel-provider-notes.mdx' # OpenTelemetry Tracing Svix can deliver webhooks directly to an OpenTelemetry tracing collector as spans, without your customers having to set up any listener endpoint or write any glue code. When **Advanced Endpoint Types** is [enabled](/advanced-endpoints#enabling-advanced-endpoint-types), your customers will see the option to use an OpenTelemetry destination in the [App Portal](/app-portal). ![OpenTelemetry Endpoint Create](/img/advanced-endpoints/otel-create.png) They will be able to configure the connection right in the App Portal: - `url` — the collector endpoint. Typically this is the `OTEL_EXPORTER_OTLP_ENDPOINT` URL given by your OpenTelemetry provider, appended with `/v1/traces`, following the [conventions of OTLP exporters](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_endpoint). Just like with regular webhook endpoints, you can configure the OpenTelemetry endpoint with custom headers that will be injected on each request. ![otel-headers](/img/stream/otel-headers.png) # Transformations By default, all OpenTelemetry Endpoints come bundled with the following transformation code. ```JavaScript /** * @param input - The input object * @param input.events - The array of webhooks in the batch. The number of webhooks in the batch is capped by the endpoint's batch size. * @param input.events[].payload - The message payload (string or JSON) * @param input.events[].eventType - The message event type (string) * * @returns Object describing the otel spans to be created. * @returns returns.config * @returns returns.config.serviceName - The name of the otel service. * @returns returns.config.scope - The scope of the otel service. Includes a name and version string. * @returns returns.spans - Array of objects, each describing an otel span. * @returns returns.spans[].kind - The kind of span. Can be one of "SERVER", "CLIENT", "PRODUCER", "CONSUMER", or "INTERNAL". (Defaults to "INTERNAL") * @returns returns.spans[].traceIdKey - An optional string. Attaches the span to traces with the same traceIdKey. * @returns returns.spans[].spanIdKey - An optional string. Identifies the span. * @returns returns.spans[].parentSpanIdKey - An optional string. Attaches the span to a parent with the same spanIdKey. * @returns returns.spans[].startTime - The start time of the span. Must be a string in ISO 8601 format. * @returns returns.spans[].endTime - The end time of the span. Must be a string in ISO 8601 format. * @returns returns.spans[].name - The name of the span. * @returns returns.spans[].attributes - An optional object. Contains key-value pairs of attributes. */ function handler(input) { const spans = input.events.map((event) => { return { // The start and end times of the span. // You'll likely want to override these based on values from the event.payload. startTime: new Date().toISOString(), endTime: new Date().toISOString(), name: event.eventType, attributes: event.payload } }) return { config: { serviceName: "svix.webhooks", scope: { name: "svix.webhooks", version: "0.0.1" }, }, spans } } ``` `input.events` is the list of webhooks received by the endpoint, processed in batches. `config` describes the [service](https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/#service-name) and [scope attributes and definitions](https://opentelemetry.io/docs/concepts/instrumentation-scope/) that all tracing data are grouped under. `spans` is an array of [OpenTelemetry spans](https://opentelemetry.io/docs/concepts/signals/traces/#spans). Each span has the following properties: * `startTime` and `endTime`: These mark the start and end of the span. By default, they are set to the current timestamp, so you'll likely want to derive these from your events directly. * `kind`: One of `"SERVER"`, `"CLIENT"`, `"PRODUCER"`, `"CONSUMER"`, or `"INTERNAL"` (`"INTERNAL"` is the default). For more details on the meaning behind these values, see [OpenTelemetry - Span Kinds](https://opentelemetry.io/docs/concepts/signals/traces/#span-kind) * `name`: A string, denoting the name of the span. * `attributes`: key-value pairing of attributes for the span. These can be any values you want associated with your span. * `traceIdKey`: By default, each span is assigned it's own trace ID that is randomly generated by Svix. However, you can specify a `traceIdKey` to derive a fixed key for the given span. This allows you to correlate multiple spans to the same trace. * `spanIdKey`: Uniquely identifies a `span` with a fixed key. By default, Svix will use a randomly generated ID if left unspecified. * `parentSpanIdKey`: Attaches the span as a child, to another span with the `spanIdKey` set to the same value. This allows you to set child spans. # Example To better understand how you can use OpenTelemetry Endpoints, let's walk through an example use case where you want to send webhook dispatch telemetry to a cloud observability platform like Grafana. Your service may produce webhooks that look like the following: ```json { "eventType": "message.attempt", "payload": { "appId": "app_379", "msgId": "msg_1234", "attemptId": "atmpt_9876", "startTime": "2025-08-19T15:24:53.291Z", "endTime": "2025-08-19T15:24:56.291Z" } } ``` ```json { "eventType": "http.attempt", "payload": { "msgId": "msg_1234", "attemptId": "atmpt_9876", "startTime": "2025-08-19T15:24:54.291Z", "endTime": "2025-08-19T15:24:55.291Z" } } ``` ```json { "eventType": "message.attempt", "payload": { "appId": "app_379", "msgId": "msg_5678", "attemptId": "atmpt_1224", "startTime": "2025-08-19T15:24:51.291Z", "endTime": "2025-08-19T15:24:54.291Z" } } ``` ```json { "eventType": "http.attempt", "payload": { "msgId": "msg_5678", "attemptId": "atmpt_1234", "startTime": "2025-08-19T15:24:52.291Z", "endTime": "2025-08-19T15:24:53.291Z" } } ``` Here, the webhooks model message deliveries. Note that: * We have two distinct messages being sent (distinct `msgId`s), each with their own attempt. Each attempt has a clear start and end time. * The `http.attempt` happens within a `message.attempt`. In other words, `http.attempt` is a child span of `message.attempt`. In order to ensure that each message is grouped into its own distinct trace, and that `http.attempt`s are correctly marked as child spans of `message.attempt`s, we'll use the following transformation code. ```JavaScript function handler(input) { const spans = input.events.map((event) => { const payload = event.payload return { // start and end times are extracted from the event directly. startTime: payload.startTime, endTime: payload.endTime, // We use the eventType as the name of the span, // as it makes it easier to identify which events map to // which spans in your OpenTelemetry platform. name: event.eventType, // We use the msgId as the traceIdKey, to ensure that all spans for the same // message are correctly grouped into the same trace. traceIdKey: payload.msgId, // Because we want to `http.attempt` to be a child span of the `message.attempt`, we // use the `attemptId` to mark the parent and child spans, respectively. spanIdKey: event.eventType === "message.attempt" ? payload.attemptId : undefined, parentSpanIdKey: event.eventType === "http.attempt" ? payload.attemptId : undefined, // Attributes can be anything you want. Here, we extract the important ID's from the event payload. attributes: { msgId: payload.msgId, appId: payload.appId, attemptId: payload.attemptId } } }) // Note that your `service.name` has been configured to `my.webhook.service`, and each individual webhook received by the endpoint is treated as its own span. return { config: { serviceName: "my.webhook.service", scope: { name: "my.webhook.scope", version: "2.0" }, }, spans } } ``` When the endpoint receives these webhooks, they'll be dispatched to your observability platform. In this Grafana view, we can see `msg_1234` as its own isolated trace, with the `message.attempt` and `http.attempt` spans correctly grouped together, with the expected attributes. ![grafana-example](/img/stream/grafana-dashboard.png) --- title: Polling Endpoints --- # Polling Endpoints Polling Endpoints are a way for your users to get a stream of events by polling instead of listening to webhooks. ## When is polling better? There are a few examples where polling for events works better than webhooks. One example is when testing webhooks locally. It's much easier to poll for events than exposing a public HTTP endpoint (even with tools like [Svix Play](/play) or [ngrok](/integrations/ngrok)). Another example is when your users don't care about getting events in real-time and prefer getting them all at once at the end of a day. One place where this use-case comes up, is when your users need to store all events for compliance reasons. In that case batching and saving them all at once is easier and more cost efficient for them. ## Enabling Polling Endpoints Polling Endpoints can be enabled at the environment level in the [Svix Dashboard](https://dashboard.svix.com/settings/organization/general-settings) by enabling **Advanced Endpoint Types**. ![Enable Polling Endpoints](/img/advanced-endpoints/advanced-endpoints-setting.png) When you enable Polling Endpoints, your users will be able to create them in the [App Portal](/app-portal). ![Polling Endpoint Create](/img/advanced-endpoints/polling-endpoint-create.png) ## Usage Like with webhook endpoints, Polling Endpoints support filtering messages by [event types](/event-types) and [channels](/channels). In the App Portal, consumers will get a unique URL and an API key to iterate through the full list of events sent to their [Svix Application](/overview#consumer-applications) since the endpoint was created. When creating a Polling Endpoint, consumers will get a unique URL like `https://api.us.svix.com/api/v1/app/app_2mG6DgUaGlwCNdM5oRCUJec2kQC/polling-endpoint/poll_59q/consumer/{consumer_id}` This URL can be polled to receive messages for a given `Consumer ID`. Each consumer tracks its own progress independently. ```bash curl \ -X GET "https://api.us.svix.com/api/v1/app/app_2mG6DgUaGlwCNdM5oRCUJec2kQC/polling-endpoint/poll_59q/consumer/MY_CONSUMER_ID" \ -H 'Accept: application/json' \ -H 'Authorization: Bearer sk_poll_*****.eu' ``` Each message includes an `offset`. Messages are returned in the order they were received. ```json { "data": [{ "id": "msg_2K2N9Qk...", "eventType": "invoice.created", "payload": { "hello": "world" }, "timestamp": "2025-01-17T00:00:00.000Z", "offset": 0 }], "done": true } ``` After processing a batch, commit the last message's `offset` so the next poll continues from there. ```bash curl \ -X POST "https://api.us.svix.com/api/v1/app/app_2mG6DgUaGlwCNdM5oRCUJec2kQC/polling-endpoint/poll_59q/consumer/MY_CONSUMER_ID/commit" \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer sk_poll_*****.eu' \ -d '{ "offset": 0 }' ``` Until you commit, the same messages may be returned again after the lease expires. Use a stable `Consumer ID` per worker so progress is preserved across restarts. > Consumer IDs are arbitrary strings used by a client to self-identify. > > Note that Consumer IDs should be _unique per client_ and should not be shared. Put another way, Consumer IDs track > exclusive sequential access through the stream of messages. > Shared concurrent access through a given Consumer ID will result in errors as mutual clients drift in their positions > in the stream. ## Usage with Svix Bridge If you have a service that must not be exposed on the public internet and you want it to receive webhooks from Svix, you can deploy a [Svix Bridge] instance inside the same network that forwards messages from a polling endpoint to the destination service. Svix Bridge will take care of retrying in case the connection the Svix API or the destination service is temporarily lost. The app portal page for the Polling Endpoint includes a basic example config for Svix Bridge, with the app ID and sink ID already filled in. It looks like this: ```yaml receivers: - name: "msg-poller-to-http" input: type: "svix-message-poller" consumer_id: "svix-bridge-1" app_id: "app_xxxxxxxxxxxxxxxxxxxxxxxxxxx" sink_id: "poll_yyy" token: "sk_poll_*****.eu" output: type: "http" url: "http://example.local" ``` The http output is not the only destination Svix Bridge can send webhooks to, it can also store them to a messaging system like RabbitMQ or Kafka. Further, you can set a transformation - a small JS program that processes each message before it gets forwarded. For the full list of supported configuration options and how to configure each of the output types, see the [receivers example config file][example-recv-cfg]. [Svix Bridge]: https://github.com/svix/svix-webhooks/tree/main/bridge#readme [example-recv-cfg]: https://github.com/svix/svix-webhooks/blob/main/bridge/svix-bridge.example.receivers.yaml --- Polling Endpoints can be used in parallel with regular Svix webhooks, and you don't need to make any changes to how you [create messages](/quickstart#send-a-message). --- title: RabbitMQ --- # RabbitMQ Svix can deliver webhooks directly to RabbitMQ, without your customers having to set up any listener endpoint or write any glue code. When **Advanced Endpoint Types** is [enabled](/advanced-endpoints#enabling-advanced-endpoint-types), your customers will see the option to use a RabbitMQ destination in the [App Portal](/app-portal). ![RabbitMQ Endpoint Create](/img/advanced-endpoints/rabbitmq-create.png) They will be able to configure the connection right in the App Portal: - `uri` — the AMQP connection URI for your RabbitMQ instance. - `routingKey` — the routing key each message is published with. Every webhook in the batch is published to RabbitMQ as a separate message, using the `routingKey` from the config. # Transformations By default, all RabbitMQ Endpoints come bundled with the following transformation code. ```JavaScript /** * @param input - The input object * @param input.events - The array of webhooks in the batch. The number of webhooks in the batch is capped by the endpoint's batch size. * @param input.events[].payload - The message payload (string or JSON) * @param input.events[].eventType - The message event type (string) * * @returns Object containing the request body * @returns returns.payloads - The array of messages (strings) to send to the endpoint. Each payload is a distinct message published to RabbitMQ. */ function handler(input) { const payloads = input.events.map((event) => JSON.stringify(event)) return { payloads } } ``` `input.events` is the list of webhooks received by the endpoint, processed in batches. Each entry in the returned `payloads` array is published to RabbitMQ as a separate message. By default, each webhook is serialized to a JSON string containing its `payload` and `eventType`. For example, if the endpoint receives the following messages: ```json { "eventType": "user.created", "payload": "{\"email\": \"joe@enterprise.io\"}" } ``` ```json { "eventType": "user.login", "payload": "{\"id\": 12, \"timestamp\": \"2025-07-21T14:23:17.861Z\"}" } ``` The default transformation code would publish two messages to your `routingKey`, with the following bodies. ```json {"payload":{"email":"joe@enterprise.io"},"eventType":"user.created"} ``` ```json {"payload":{"id":12,"timestamp":"2025-07-21T14:23:17.861Z"},"eventType":"user.login"} ``` To control the message bodies, return your own array of strings in `payloads`. Each string becomes one message. --- title: Amazon Redshift --- # Amazon Redshift Svix can deliver webhooks directly to an Amazon Redshift table, without your customers having to set up any listener endpoint or write any glue code. Svix writes to Redshift through the [Redshift Data API](https://docs.aws.amazon.com/redshift-data/latest/APIReference/Welcome.html), and supports both Redshift Serverless and provisioned clusters. When **Advanced Endpoint Types** is [enabled](/advanced-endpoints#enabling-advanced-endpoint-types), your customers will see the option to use a Redshift destination in the [App Portal](/app-portal). ![Redshift Endpoint Create](/img/advanced-endpoints/redshift-create.png) They will be able to configure the connection right in the App Portal: - `region`, `accessKeyId`, `secretAccessKey` — the AWS region and credentials used to authenticate. - `dbName` — the database to write to. - `schemaName` — the schema that contains the table (optional). - `tableName` — the table that receives the rows. Every batch of webhooks received by the endpoint is inserted into the configured Redshift table. ## Connection How you point Svix at your Redshift depends on the deployment type: - **Redshift Serverless** — set `workgroupName` to the name of your workgroup. - **Provisioned clusters** — set `clusterIdentifier` to your cluster's identifier and `dbUser` to the database user to connect as. ## Destination table Without a transformation, Svix inserts each webhook into the table identified by `dbName`, `schemaName`, and `tableName` using two columns: `created_at` and `payload`. Svix sets `created_at` to the insert time and writes the raw payload to `payload`. The table must already exist before you enable the endpoint. For the default behavior, create it with: ```sql CREATE TABLE events ( created_at TIMESTAMP, payload VARCHAR(65535) ); ``` At the time of writing, `VARCHAR(65535)` is the largest allowable `VARCHAR` size in Redshift. If webhooks with larger payloads are received by the endpoint, the endpoint will be disabled since these payloads can't be written to Redshift. The `dbName`, `schemaName`, and `tableName` fields are only required when you're not using a transformation. With a transformation, the target table is named directly in your statement. # Transformations Redshift transformations build a parameterized SQL statement. The transformation returns the `statement` to run and the `bindings` it references. ```JavaScript /** * @param input - The input object * @param input.events - The array of webhooks in the batch. The number of webhooks in the batch is capped by the endpoint's batch size. * @param input.events[].payload - The message payload (string or JSON) * @param input.events[].eventType - The message event type (string) * * @returns Object describing the SQL to run against Redshift. * @returns returns.statement - The SQL statement to execute. Reference parameters by name (e.g. :payload0). * @returns returns.bindings - The parameters referenced by the statement. Each binding is an object with a `name` and a `value`. */ function handler(input) { let bindings = []; let values = []; input.events.forEach((event, i) => { const name = `payload${i}`; bindings.push({ name: name, value: JSON.stringify(event.payload) }); values.push(`(CURRENT_TIMESTAMP, :${name})`); }); return { bindings: bindings, statement: `INSERT INTO events (created_at, payload) VALUES ${values.join(", ")};` }; } ``` `input.events` is the list of webhooks received by the endpoint, processed in batches. `bindings` is an array of `{ name, value }` parameters, and the `statement` references them by name (e.g. `:payload0`). The statement is run against your database through the Redshift Data API. To write different columns, adjust the `bindings`, the `statement`, and your table to match. For example, if the endpoint receives the following messages: ```json { "eventType": "user.created", "payload": "{\"email\": \"joe@enterprise.io\"}" } ``` ```json { "eventType": "user.login", "payload": "{\"id\": 12, \"timestamp\": \"2025-07-21T14:23:17.861Z\"}" } ``` The transformation above inserts two rows into your table. | `created_at` | `payload` | | --- | --- | | `2025-07-21 14:23:18` | `{"email":"joe@enterprise.io"}` | | `2025-07-21 14:23:18` | `{"id":12,"timestamp":"2025-07-21T14:23:17.861Z"}` | --- title: Snowflake --- # Snowflake Svix can deliver webhooks directly to a Snowflake table, without your customers having to set up any listener endpoint or write any glue code. When **Advanced Endpoint Types** is [enabled](/advanced-endpoints#enabling-advanced-endpoint-types), your customers will see the option to use a Snowflake destination in the [App Portal](/app-portal). ![Snowflake Endpoint Create](/img/advanced-endpoints/snowflake-create.png) They will be able to configure the connection right in the App Portal: - `accountIdentifier` — your Snowflake account identifier in `-` form (e.g. `ab12345-xs67890`). - `userId` — the Snowflake user the public key is assigned to. - `privateKey` — the PEM-encoded private key. - `dbName`, `schemaName`, `tableName` — the database, schema, and table that receive the rows. Every batch of webhooks received by the endpoint is inserted into the configured Snowflake table. ## Authentication Svix authenticates to Snowflake using [key-pair (JWT) authentication](https://docs.snowflake.com/en/user-guide/key-pair-auth). Generate an RSA key pair, assign the public key to a Snowflake user, and provide the matching private key in the endpoint config. ## Destination table Without a transformation, Svix inserts each webhook into the table identified by `dbName`, `schemaName`, and `tableName` using three columns: `id`, `created_at`, and `payload`. Svix generates a unique `id` for each row, sets `created_at` to the insert time, and writes the raw payload to `payload`. The table must already exist before you enable the endpoint, which you can create with the following sql: ```sql CREATE TABLE my_database.my_schema.my_table ( id TEXT, created_at TIMESTAMP, payload TEXT ); ``` If you use a transformation (below), you control the SQL that runs, so `dbName`, `schemaName`, and `tableName` become optional — the target table is named directly in your statement. # Transformations Snowflake Endpoints shape each batch of webhooks into a SQL `INSERT` statement. The transformation returns the `statement` to run and the `bindings` it references. For example, suppose you have a table with the following structure: ```sql CREATE TABLE testdb.testschema.users ( name TEXT, age INT ); ``` If the endpoint receives the following messages: ```json { "eventType": "user.created", "payload": "{\"name\": \"John Smith\", \"age\": 34}" } ``` ```json { "eventType": "user.created", "payload": "{\"name\": \"Jane Doe\", \"age\": 47}" } ``` To insert the new users into your `testdb.testschema.users` table, you'd write transformation code as follows: ```JavaScript /** * @param input - The input object * @param input.events - The array of webhooks in the batch. The number of webhooks in the batch is capped by the endpoint's batch size. * @param input.events[].payload - The message payload (string or JSON) * @param input.events[].eventType - The message event type (string) * * @returns Object describing the SQL to run against Snowflake. * @returns returns.statement - The SQL statement to execute. Reference bindings by name (e.g. :id), matching the keys in `bindings`. * @returns returns.bindings - The bindings referenced by the statement. Each binding has a Snowflake `type` (e.g. "TEXT") and a column-oriented `value` array with one entry per row in the batch. */ function handler(input) { let bindings = { "name": { "type": "TEXT", "value": [] }, "age": { "type": "FIXED", "value": [] }, }; input.events.forEach((event) => { let name = event.payload.name; let age = String(event.payload.age); // Note that Snowflake requires all values be sent as Strings bindings.name.value.push(name); bindings.age.value.push(age); }); return { bindings: bindings, statement: "INSERT INTO TESTDB.TESTSCHEMA.users (name, age) VALUES (:name, :age);" }; } ``` `input.events` is the list of webhooks received by the endpoint, processed in batches. `bindings` are column-oriented: each binding lists a Snowflake `type` and a `value` array holding one entry per row in the batch. The `statement` references them by name (`:name` and `:age`). For more information which types are allowed, see [Using bind variables in a statement.](https://docs.snowflake.com/en/developer-guide/sql-api/submitting-requests#using-bind-variables-in-a-statement) Because the statement names the table directly, `dbName`, `schemaName`, and `tableName` are optional when a transformation is set. To write different columns, adjust the `bindings`, the `statement`, and your table to match. The transformation above would insert two rows into your table. ![snowflake-output](/img/stream/snowflake-output.png) --- title: Amazon SNS --- # Amazon SNS Svix can deliver webhooks directly to an Amazon SNS topic, without your customers having to set up any listener endpoint or write any glue code. When **Advanced Endpoint Types** is [enabled](/advanced-endpoints#enabling-advanced-endpoint-types), your customers will see the option to use an SNS destination in the [App Portal](/app-portal). ![SNS Endpoint Create](/img/advanced-endpoints/sns-create.png) They will be able to configure the connection right in the App Portal: - `topicArn` — the ARN of the SNS topic to publish to. - `region`, `accessKeyId`, `secretAccessKey` — the AWS region and credentials used to authenticate. Every webhook in the batch is published to the topic as a separate message. # Transformations By default, all SNS Endpoints come bundled with the following transformation code. ```JavaScript /** * @param input - The input object * @param input.events - The array of webhooks in the batch. The number of webhooks in the batch is capped by the endpoint's batch size. * @param input.events[].payload - The message payload (string or JSON). * @param input.events[].eventType - The message event type (string). * * @returns Object containing the response. * @returns returns.messages - The array of SNS messages to send to the SNS topic. * @returns returns.messages[].payload - The content of the message (string). * @returns returns.messages[].subject - An optional subject of the message (string). */ function handler(input) { const messages = input.events.map((event) => ({ payload: event, })); return { messages, }; } ``` `input.events` is the list of webhooks received by the endpoint, processed in batches. Each entry in the returned `messages` array is published as a separate SNS message. The `payload` becomes the message body, and the optional `subject` becomes the SNS subject. By default, each webhook is published as its serialized JSON, with no subject. For example, if the endpoint receives the following messages: ```json { "eventType": "user.created", "payload": "{\"email\": \"joe@enterprise.io\"}" } ``` ```json { "eventType": "user.login", "payload": "{\"id\": 12, \"timestamp\": \"2025-07-21T14:23:17.861Z\"}" } ``` The default transformation code would publish two messages to your topic, with the following bodies. ```json {"payload":{"email":"joe@enterprise.io"},"eventType":"user.created"} ``` ```json {"payload":{"id":12,"timestamp":"2025-07-21T14:23:17.861Z"},"eventType":"user.login"} ``` To control the message bodies, or to set a subject, return your own array of `messages`. Each message's `payload` becomes the body of one SNS message, and `subject` is published as that message's SNS subject. ```JavaScript function handler(input) { const messages = input.events.map((event) => ({ payload: JSON.stringify(event.payload), subject: event.eventType })); return { messages, }; } ``` SNS accepts at most 10 messages per batch request, so larger batches are automatically split across multiple `PublishBatch` calls. --- title: Amazon SQS --- # Amazon SQS Svix can deliver webhooks directly to an Amazon SQS queue, without your customers having to set up any listener endpoint or write any glue code. When **Advanced Endpoint Types** is [enabled](/advanced-endpoints#enabling-advanced-endpoint-types), your customers will see the option to use an SQS destination in the [App Portal](/app-portal). ![SQS Endpoint Create](/img/advanced-endpoints/sqs-create.png) They will be able to configure the connection right in the App Portal: - `queueUrl` — the full URL of the SQS queue to send messages to. - `region`, `accessKeyId`, `secretAccessKey` — the AWS region and credentials used to authenticate. Every webhook in the batch is sent to the queue as a separate message. # Transformations By default, all SQS Endpoints come bundled with the following transformation code. ```JavaScript /** * @param input - The input object * @param input.events - The array of webhooks in the batch. The number of webhooks in the batch is capped by the endpoint's batch size. * @param input.events[].payload - The message payload (string or JSON). * @param input.events[].eventType - The message event type (string). * * @returns Object containing the response. * @returns returns.messages - The array of SQS messages to send to the SQS queue. * @returns returns.messages[].payload - The payload of the message (string). */ function handler(input) { const messages = input.events.map((event) => ({ payload: event, })); return { messages, }; } ``` `input.events` is the list of webhooks received by the endpoint, processed in batches. Each entry in the returned `messages` array is sent as a separate SQS message, with its `payload` used as the message body. By default, each webhook is sent as its serialized JSON. For example, if the endpoint receives the following messages: ```json { "eventType": "user.created", "payload": "{\"email\": \"joe@enterprise.io\"}" } ``` ```json { "eventType": "user.login", "payload": "{\"id\": 12, \"timestamp\": \"2025-07-21T14:23:17.861Z\"}" } ``` The default transformation code would send two messages to your queue, with the following bodies. ```json {"payload":{"email":"joe@enterprise.io"},"eventType":"user.created"} ``` ```json {"payload":{"id":12,"timestamp":"2025-07-21T14:23:17.861Z"},"eventType":"user.login"} ``` To control the message bodies, return your own array of `messages`. Each message's `payload` becomes the body of one SQS message. SQS accepts at most 10 messages per batch request, so larger batches are automatically split across multiple `SendMessageBatch` calls. --- title: AI Quickstart --- # AI Quickstart Building your Svix integration with an AI coding agent? This is the quickstart for you (well, for it). It gets Claude Code, Cursor, Codex, or any other coding agent set up to build your webhook sending integration: skills, LLM-readable docs, and the CLI. If you're integrating by hand, the regular [Quickstart](/quickstart) is the place to start. **Sending or receiving?** Svix is webhook *sending* infrastructure, and this page is for platforms sending webhooks to their customers. If you're on the receiving end, consuming webhooks that a Svix-powered provider sends you, connect your agent to the [App Portal MCP](/ai/app-portal-mcp) to debug your deliveries, and use the `receiving-webhooks` skill below when writing your handler. ## Set up your agent ### Install the Svix agent skills [Agent Skills](https://agentskills.io/) are instructions that load into your agent's context when it touches Svix. Ours teach it to integrate Svix the way our own engineers would: tenancy design, idempotency, App Portal embedding, and the rest. ```bash npx skills add svix/ai ``` This installs two skills from [svix/ai](https://github.com/svix/ai) into your project: - **`svix-sending-webhooks`**: everything for building on Svix. First-time setup, sending webhooks to your customers, receiving third-party webhooks with [Ingest](https://www.svix.com/ingest/), and the [Svix CLI](/tutorials/cli). Ask it for a plan and it switches modes: it investigates your repo, asks the questions it can't answer on its own, and writes an integration plan before touching any code. - **`receiving-webhooks`**: guidelines for writing a webhook handler that verifies signatures correctly. Useful to you when testing your own webhooks end to end, and to your customers when they consume them. ### Create an API key This is the one step your agent can't do for you. Create a key on the [API Access page](https://dashboard.svix.com/api-access) and set it as an environment variable: ```bash export SVIX_AUTH_TOKEN="testsk_..." ``` The token encodes your region, so there's no base URL to configure. Keep it server-side; the skills know not to hardcode it. ### Tell it what you want That's it. Some prompts to start from: - "Add Svix webhooks to this app. Our customers should get an `invoice.paid` event when a payment settles." - "Write me a Svix integration plan before we build anything." - "Define a Svix event type catalog from the events this codebase already emits." - "Embed the Svix App Portal in our dashboard so customers can manage their own endpoints." ## What Svix gives your agent | Resource | What it is | | --- | --- | | [Agent skills](https://github.com/svix/ai) | Integration instructions that load on demand, installed with `npx skills add svix/ai` | | LLM-readable docs | Every docs page as markdown, plus [llms.txt](https://docs.svix.com/llms.txt) and [llms-full.txt](https://docs.svix.com/llms-full.txt) indexes | | [Svix CLI](/tutorials/cli) | The full API from the shell, plus `svix listen` to relay webhooks to localhost | ## Context to paste If your agent doesn't support skills, or you're working in a chat instead of a repo, use this condensed version of what the skills teach. Save it where your tool looks for instructions: | Tool | Where it goes | | --- | --- | | Claude Code | `CLAUDE.md` | | Cursor | `.cursor/rules/svix.mdc` | | GitHub Copilot | `.github/copilot-instructions.md` | | Gemini CLI | `GEMINI.md` | | Codex, Jules, Amp, and [many others](https://agents.md/) | `AGENTS.md` | | A chat conversation | paste it directly | ````markdown # Svix context for AI agents Svix is webhook-sending infrastructure: you make one API call and Svix handles delivery, retries, security, and observability. The docs are agent-readable: append `.md` to any https://docs.svix.com URL; the index is https://docs.svix.com/llms.txt and the API reference is https://api.svix.com/docs. ## Core model - An **Application** is one webhook-receiving tenant, almost always one of your customers. Create it with your own customer ID as the `uid` and use that `uid` everywhere; you never need to store Svix IDs. Creation is idempotent on `uid`. - An **Endpoint** is a URL an application's messages are delivered to. Customers usually manage their own via the App Portal. - A **Message** is one webhook event, sent to one application and fanned out to its endpoints. `eventType` uses a `group.event` convention (e.g. `invoice.paid`); include the type in the payload too. - **Event Types** form your catalog; consumers subscribe per type. ## The three calls that matter 1. `application.create({ name, uid })`: once per customer. 2. `message.create(uid, { eventType, payload })`: to send an event. 3. `authentication.appPortalAccess(uid, {})`: magic link to the App Portal, where customers add endpoints, view logs, and replay failures. ## Rules - `SVIX_AUTH_TOKEN` is server-side only; read it from the environment, never hardcode it or expose it to a browser. It encodes the region, so no base URL is needed. - Official SDKs: JavaScript, Python, Go, Rust, Java, Kotlin, Ruby, C#, PHP. Same call shapes, different argument conventions; check https://docs.svix.com/quickstart.md for the exact syntax per language. - Make sends idempotent: a deterministic `eventId` per source event, or the `Idempotency-Key` header. - Endpoints must be public HTTPS. For local development run `svix listen http://localhost:8000/webhook/` (Svix CLI) to relay deliveries to localhost. - Consumers verify the `svix-signature` header against the **raw** request body using the Svix SDK, then return a 2xx within seconds. - To receive third-party webhooks (Stripe, GitHub, ...) rather than send your own, use Svix Ingest: https://docs.svix.com/ingest/receiving-with-ingest.md ```` ## Agent tools for your webhook consumers Your customers, the ones receiving your webhooks, get agent tooling of their own. These are worth knowing about because you enable or recommend them: - The [App Portal MCP](/ai/app-portal-mcp) lets a customer point their coding agent at their deliveries: inspect failed attempts and the exact response their handler returned, fetch real payloads, and replay missed messages. Tokens are scoped to their single application, and you enable the feature per environment from your dashboard. - [Webhooks AutoConfig](/webhooks-autoconfig) lets a customer's agent configure its own endpoint (URL, event types) in code, with the signing secret bundled into one token, instead of clicking through a UI. - The [agent plugins](https://github.com/svix/ai#personal-agent-plugins) poll a Svix sink and hand messages to an agent runtime as if they were inbound POSTs, so a laptop behind NAT can receive webhooks without a tunnel. ## LLM-readable docs Everything on this site is available as plain markdown: - Append `.md` to any page URL, for example [docs.svix.com/quickstart.md](https://docs.svix.com/quickstart.md). - [docs.svix.com/llms.txt](https://docs.svix.com/llms.txt): an index of every page with a one-line description, following the [llms.txt convention](https://llmstxt.org/). - [docs.svix.com/llms-full.txt](https://docs.svix.com/llms-full.txt): the full documentation in one file. The API reference lives at [api.svix.com/docs](https://api.svix.com/docs), with request and response schemas for every endpoint. --- title: Consumer App Portal MCP --- # Consumer App Portal MCP The Consumer App Portal MCP server lets your users point their coding agent (Claude Code, Cursor, VSCode, Codex, and others) at their webhooks. Instead of clicking through the Consumer App Portal to figure out why a delivery failed, they can ask their agent. ## Enabling it for your customers The server is off by default for existing accounts. To turn it on, go to your Svix Dashboard **Settings** -> **General**, and toggle **Enable App Portal MCP**. This is an environment-level setting, so enable it in each environment (e.g. Development and Production) where you want it available. Once enabled, an **MCP** tab will appear under **Settings** in the App Portal for your customers. ## How your users connect From the App Portal, your users: ### Open **Settings** -> **MCP** ### Click **Connect to MCP** This generates an MCP access token and opens a dialog with ready-to-paste setup steps for their coding agent. ### Follow the steps for their agent The dialog covers a broad list of coding agents. If your agent is missing from this list, please [let us know](https://www.svix.com/contact/)! The [Connecting Your Coding Agent](/receiving/using-app-portal/connecting-your-coding-agent) guide walks your users through all of this, and you can point them to it directly. ### Connection details The generated configuration points the agent at the App Portal MCP server for the application: ``` https://mcp..svix.com/app/ ``` Where `` is the region your account is in (`us`, `eu`, `ca`, or `au`). The token is sent on every request as an `Authorization: Bearer ` header. The server is named `-webhooks` in the agent's configuration, where `` is derived from your App Portal display name (for example `your-company-name-webhooks`), so it's recognizable next to your users' other MCP servers. For reference, the underlying configuration looks like this in most agents: ```json { "mcpServers": { "your-company-name-webhooks": { "url": "https://mcp.us.svix.com/app/app_2ErlDgQ1QzKvSAqxdMQnjHNL", "headers": { "Authorization": "Bearer " } } } } ``` ## What the agent can do The server exposes the following tools: | Tool | Purpose | | --------------------------- | ------------------------------------------------------------------------------- | | `get_application` | The application this session is scoped to (name, UID, metadata). | | `list_endpoints` | List the application's endpoints (URL, enabled state, filtered event types). | | `get_endpoint` | Full configuration of one endpoint. | | `get_endpoint_stats` | Success / fail / pending / sending counts over a time window. | | `get_transformation` | An endpoint's transformation code, enabled state, and variables. | | `update_transformation` | Set an endpoint's transformation code and/or toggle it on or off. | | `list_messages` | List messages sent to the application (filter by event type, channel, time). | | `list_attempts_by_endpoint` | Delivery attempts for an endpoint (e.g. only failures), with response bodies. | | `list_attempts_by_message` | Every endpoint a single message was attempted against, and how each responded. | | `get_message` | A message's event type, channels, and JSON payload. | | `get_attempt` | One attempt in full, including the response status code and body. | | `resend_message` | Resend one message to an endpoint. | | `recover_endpoint` | Replay all failed messages for an endpoint since a given date. | Most of these are read-only. Three of them are not: `resend_message` and `recover_endpoint` perform real deliveries, and `update_transformation` changes live endpoint configuration. The server instructs agents to only use them when explicitly asked, and most agents will ask for confirmation before running a tool, but as with any agent, your users should review those calls before approving them. Some questions this makes easy to answer: - "Why is the `invoice.paid` event failing?": the agent searches the correct endpoint, checks the endpoint's delivery stats, then the failed attempts and the exact response your handler returned. - "What does the payload for this event actually look like?": the agent fetches a recent message and its JSON payload - "My handler was down for an hour, can you replay what it missed?": the agent recovers the endpoint from that point in time. ## Tokens and security The server is scoped to **a single application**: the token your user generates from the App Portal encodes the application it belongs to, so an agent can only ever see that one customer's data, which is the same data the App Portal itself shows them. MCP tokens are application-scoped and restricted: they can read the application, its endpoints, event types, integrations, messages, and attempts, and they can update endpoint transformations and create messages and attempts (what powers resend and recover). They cannot touch anything outside the application they were issued for, and they cannot be used as a Svix API key for your organization. A few things worth passing on to your users: - **Tokens expire.** By default they're valid for 7 days, after which the agent needs a new one generated from the App Portal. - **Tokens can be revoked at any time.** From the **MCP** tab, the token's menu -> **Expire** revokes it immediately, and any agent using it loses access. - **Payload data reaches the agent's model.** Message payloads the agent reads are sent to whichever model provider backs the coding agent, so treat this the same way you'd treat pasting a payload into that tool. Turning off **Enable App Portal MCP** in your Dashboard settings hides the MCP tab, so your users can no longer generate new tokens. Tokens that were already issued keep working until they expire, so revoke any you don't want to remain active. # API keys Manage your API keys to authenticate requests with Svix. Svix authenticates your API requests using your account’s API keys. If you don’t include your key when making an API request, or use an incorrect or outdated one, Svix returns an error. **API keys are per environment.** Every organization starts with a development environment with a corresponding API key. This key should only be used for development or internal testing and is not intended to be used in any production systems. ## Obtaining your API keys Your API keys can be found on the "API Access" page of the Svix Dashboard. ![API Access page](/img/api-access-page.png) ## Keeping your keys safe Your secret API key can be used to make any API call on behalf of your account. Treat your secret API key as you would any other password. Grant access only to those who need it. Ensure it is kept out of any version control system you may be using. Control access to your key using a password manager or secrets management service. ## Rotating keys Svix support creating multiple API keys per environment. This gives you a lot of flexibility when rotating keys. For example, you can create a new key, replace all instances of the old key, and then expire the old key for a zero downtime key rotation. When expiring keys, you can also define when you would like the key to expire. Either immediately, or at a later point in time. --- title: Consumer App Portal --- # Consumer App Portal Svix comes with a consumer application portal for your users (webhook consumers) that you can use out of the box. Your users can then use it to add endpoints, debug delivery, as well as inspect and replay past webhooks. This is the easiest way to get started, but you can alternatively use the API to build your own. If you're looking for a live instance of the app portal, you can find one here: https://example.svix.com/ Here is what it looks like standalone, or scroll down for the embedded version: ![App Portal screenshot](/img/app-portal/endpoint.png) ## Application portal usage guides For more information on how to use the app portal please refer to the [receiving webhooks](./receiving/introduction.mdx) section of the docs. ## Giving your users access App portal access is based on short-lived sessions using special magic links. You customers don't need a Svix account, and they don't even need to know that Svix exists. To give your users access to the App Portal, just use the [app portal access endpoint](https://api.svix.com/docs#tag/Authentication/operation/v1.authentication.app-portal-access). Calling this endpoint with an `app_id` returns a single-use URL you can just redirect your users to in order to log them into the App Portal. They will stay logged in for a few days or until they log out. The following API call to get the URL should be called from the backend as the Svix API key should not be shared with the frontend. The values returned from this API call (URL and token) are safe to pass to the frontend. ```js const svix = new Svix("AUTH_TOKEN"); const dashboard = await svix.authentication.appPortalAccess("app_Xzx8bQeOB1D1XEYmAJaRGoj0", {}); // A URL that automatically logs user into the dashboard console.log(dashboard.url); ``` ```python svix = Svix("AUTH_TOKEN") dashboard = svix.authentication.app_portal_access("app_Xzx8bQeOB1D1XEYmAJaRGoj0", AppPortalAccessIn()) # A URL that automatically logs user into the dashboard print(dashboard.url) ``` ```rust let svix = Svix::new("AUTH_TOKEN".to_string(), None); let dashboard = svix .authentication() .app_portal_access( "app_Xzx8bQeOB1D1XEYmAJaRGoj0".to_string(), AppPortalAccessIn::default(), None, ) .await?; // A URL that automatically logs user into the dashboard println!("{}", dashboard.url); ``` ```go svixClient := svix.New("AUTH_TOKEN", nil) dashboard, _ := svixClient.Authentication.AppPortalAccess(ctx, "app_Xzx8bQeOB1D1XEYmAJaRGoj0", &svix.AppPortalAccessIn{}) // A URL that automatically logs user into the dashboard fmt.Println(dashboard.Url) ``` ```java Svix svix = new Svix("AUTH_TOKEN"); AppPortalAccessOut dashboard = svix.getAuthentication().appPortalAccess("app_Xzx8bQeOB1D1XEYmAJaRGoj0", new AppPortalAccessIn()); // A URL that automatically logs user into the dashboard System.out.println(dashboard.getUrl()); ``` ```kotlin val svix = Svix("AUTH_TOKEN") val dashboard = svix.authentication.appPortalAccess("app_Xzx8bQeOB1D1XEYmAJaRGoj0", AppPortalAccessIn()) // A URL that automatically logs user into the dashboard println(dashboard.url) ``` ```ruby svix = Svix::Client.new("AUTH_TOKEN") dashboard = svix.authentication.app_portal_access("app_Xzx8bQeOB1D1XEYmAJaRGoj0", Svix::AppPortalAccessIn.new({})) # A URL that automatically logs user into the dashboard puts dashboard.url ``` ```csharp var svix = new SvixClient("AUTH_TOKEN", new SvixOptions("https://api.us.svix.com")); var dashboard = await svix.Authentication.AppPortalAccessAsync("app_Xzx8bQeOB1D1XEYmAJaRGoj0", new AppPortalAccessIn{}); // A URL that automatically logs user into the dashboard Console.WriteLine(dashboard.Url) ``` ```php $svix = new Svix('AUTH_TOKEN'); $dashboard = $svix->authentication->appPortalAccess( 'app_Xzx8bQeOB1D1XEYmAJaRGoj0', AppPortalAccessIn::create() ); echo $dashboard->url . PHP_EOL; ``` ```shell export SVIX_AUTH_TOKEN="AUTH_TOKEN" svix authentication app-portal app_Xzx8bQeOB1D1XEYmAJaRGoj0 ``` ```shell curl -X POST "https://api.us.svix.com/api/v1/auth/app-portal-access/app_Xzx8bQeOB1D1XEYmAJaRGoj0/" \ -H "Accept: application/json" \ -H "Authorization: Bearer AUTH_TOKEN" \ -H 'Content-Type: application/json' \ -d '{}' ``` In most cases you would want to create an API endpoint on your end that your frontend can call to get the URL. For example, your code may look something like this: ```javascript const svix = new Svix("AUTH_TOKEN"); // API path: /webhooks/app-portal function get_app_portal(authenticated_user) { const svix_app_id_or_uid = authenticated_user.id; return await svix.authentication.appPortalAccess(svix_app_id_or_uid, {}); } ``` Then your frontend will do something like this: ```javascript // React example function AppPortal() { const [appPortal, setAppPortal] = React.useState(null); React.useEffect(() => { fetch('https://api.your-backend.com/webhooks/app-portal', { method: "POST" }) .then((res) => res.json()) .then((result) => setAppPortal(result)); }, []); const url = appPortal?.url; if (url) { // NOTE: in practice you'd want to use the svix-react library return ( ); else { return
Loading...
; } }; ``` ### Showing a specific page If you want your users to be redirected to a specific page of the App Portal, you can add a `next` query parameter to the URL: ``` https://app.svix.com/login?next=/endpoints/abc#key=xyz ``` You should use a URL parsing library to add the query parameter (avoid editing the URL manually). ### Feature flags When [creating an App Portal URL](https://api.svix.com/docs#tag/Authentication/operation/v1.authentication.app-portal-access), you can optionally specify a list of feature flags (using the `featureFlags: [ ... ]` field) to restrict which event types your users will see in the Event Catalog and the API. To learn more refer to the [Event Types](./event-types.mdx#event-type-feature-flags) documentation. ### Session IDs App Portal URLs can also be generated with a specific session ID, using the `sessionId` field. App Portal URLs created with a session ID can be selectively invalidated by the [Expire All](https://api.svix.com/docs#tag/Authentication/operation/v1.authentication.expire-all) endpoint by including the session ID in the Expire All request. Multiple App Portal URLs can be created with the same session ID, and if [Expire All](https://api.svix.com/docs#tag/Authentication/operation/v1.authentication.expire-all) is called with a session ID, only App Portal URLs with that specific session ID will be invalidated. ## Embedding in your own dashboard ### Embedding as an iframe The returned URL from the previous section can also be embedded in your own dashboard using an iframe. ![Embedded App Portal screenshot](/img/app-portal/embedded.png) To add this to your application, just pass the URL you received in the previous example to the `src` property of the iframe: ```html ``` We have also included some basic styling to make the iframe to look nicer, though that can be omitted or modified depending on your needs. **Important**: the `allow` directive above is required for clipboard actions to work, though they may still fail if you've blocked them with the [Permissions Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy) header. ### Embedding in a React application We also provide the [`svix-react`](https://www.npmjs.com/package/svix-react) package that you can use to easily embed the App Portal. To use it, simply run: ```sh npm install svix-react # or yarn add svix-react ``` The npm package is lightweight and provides some basic styling and a loading indicator out of the box. To use it, simply provide the magic link from the [`app_portal_access` endpoint](https://api.svix.com/docs#tag/Authentication/operation/v1.authentication.app-portal-access). ```js import React from "react"; import ReactDOM from "react-dom"; import { AppPortal } from "svix-react"; import "svix-react/style.css"; const SvixEmbed = () => { const [appPortal, setAppPortal] = React.useState(null); React.useEffect(() => { // Prerequisite: You'll need an endpoint that returns the App Portal // magic URL (https://api.svix.com/docs#tag/Authentication/operation/v1.authentication.app-portal-access) fetch(`/your-backend-service/svix/app-portal`, { method: "POST" }) .then((res) => res.json()) .then((result) => setAppPortal(result)); }, []); return ; }; const App = () => ; ReactDOM.render(, document.body); ``` You can optionally use the `fullSize` prop (``) so that App Portal will automatically grow to fit its contents. ### Example integration The [`svix-example`](https://github.com/svix/svix-example) repo contains an example implementation of the App Portal in a React application, using NextJS. In the webhooks Dashboard [page](https://github.com/svix/svix-example/blob/main/src/pages/dashboard/webhooks.tsx), you can see how to get the App Portal login URL and use it to render the App Portal in an iframe. ### Design Considerations When embedding the App Portal into your application, keep in mind that the App Portal is an information dense master-detail view. We recommend keeping the view at 100% width to avoid abbreviating important information for your users. If you are hoping to avoid having an embedded scrollbar, we recommend putting the view inside a full screen modal from your dashboard, or using the `fullSize` prop so that the App Portal sizes itself based on its content. ![embedded iframe screen](/img/app-portal/embedded-view-screen.png) ## White labeling Keep the same look and feel of your application by white labeling the App Portal. From the [Svix dashboard](https://dashboard.svix.com/settings), you can configure the color palette, font and the logo that your users will see. To customize these settings, please head to your [Organization Settings](https://dashboard.svix.com/settings) on the Svix Dashboard. ### Custom settings per application In some scenarios, you may want to customize the application portal per application, rather than for your whole organization. For example, if you change the color of your own dashboard for each of your users. This is possible by changing the query parameters of the URL to the settings of your choosing. Please use proper URL parsing rather than string manipulation as the structure of the URL (e.g. the fragment part) may change. Another reason to use proper URL parsing is that you want to make sure to URL-encode the parameters. The supported parameters are: - `primaryColorLight` and `primaryColorDark` - the primary color of the UI in light and dark mode, respectively. Format: `RRGGBB`, e.g. `28bb93`. - `icon` - a URL to an image file. E.g. `https://www.example.com/logo.png` (remember to URL-encode it!). - `fontFamily` - one of the fonts listed in the dashboard (see previous section). E.g. `Roboto`. - `darkMode (false|true|auto)` - when set to `true`, dark mode will be turned on by default when the app portal is opened. `auto` will use the user's system preferred color mode. Defaults to `false`. - `hideNavigation` - when set to `true`, the navigation tabs will be hidden. This can be useful to embed only specific sections of the app portal, in combination with the `next` parameter (see [Showing a specific page](#showing-a-specific-page)). - `noGutters` - when set to `true`, the app portal will not have any left or right padding within the iframe. This can be useful to align the app portal content with other elements in your site. So for example: ``` http://app.svix.com/login?primaryColorLight=22cc91&primaryColorDark=28bb93&darkMode=true&fontFamily=Roboto#key=eyJhcHBJZCI6ICJhcHBfMXRSdFlMN3pwWWR2NHFuWTRRZFI1azE4eXQ0IiwgInRva2VuIjogImFwcHNrX2UxOUN0Rm5hbTFoOU1Gamh5azRSMTUzNUNSd05VSWI0In0= ``` ## App Portal capabilities App portal capabilities let you disable certain functionality when giving your customers access to the app portal. You can control the exact list of capabilities by using the `capabilities` property when calling [the app portal access API endpoint](https://api.svix.com/docs#tag/Authentication/operation/v1.authentication.app-portal-access). To give your customer read-only access to the app portal, you can use the `ViewBase` capabilities. For the full list of `capabilities`, please refer to [the app portal access API docs](https://api.svix.com/docs#tag/Authentication/operation/v1.authentication.app-portal-access). ## Implementing your own Consumer App Portal functionality You can implement Consumer App Portal functionality into your own dashboard using ergonomic React Hooks provided by the [`svix-react`] library or using the [Svix JavaScript library](https://api.svix.com/docs). This approach allows you to use your own UI structure and components and have full control over the behavior. It's also very common to mix-and-match your own app portal with the embedded consumer app portal that we provide as an iframe. For example, you can build the list, create, and edit endpoint views yourself, and then offer the full app portal in "read only" mode to give your customers access to the more advanced functionality available in the app portal. ### Authentication Whether implementing your own dashboard using the Svix React library or the JavaScript library, you can use the Svix frontend-friendly tokens. These are tokens that can be safely passed to the frontend as they expire automatically and are scoped to the respective application. These are the same kinds of tokens used by the iframe. To get a frontend-friendly token, call the [get consumer app portal access API](https://api.svix.com/docs#tag/Authentication/operation/v1.authentication.app-portal-access) from the backend and pass the returned token to the frontend. This token will be scoped to the specific application, will expire automatically based on the set expiry, and will only have the capabilities associated with it on creation. ### Svix React example The hooks are an abstraction over the `svix` JavaScript library and help to handle concerns like paginating over large lists of data, and reloading data. Using the hooks requires wrapping your application or components in ``: ```jsx import { SvixProvider } from 'svix-react' export default function App() { return ( {/** your app's components **/} ) } ``` Then, you can use hooks like `useEndpoints`: ```jsx import { useEndpoints } from 'svix-react' export default function ListEndpoints() { const endpoints = useEndpoints() return (
{endpoints.error &&
An error has occurred
} {endpoints.loading &&
Loading...
}
    {endpoints.data?.map((endpoint) => (
  • {endpoint.url}
  • ))}
) } ``` ## FAQ ### Can I inject my own CSS in the embedded app portal? We don't support injecting custom CSS into the pre-built embedded app portal as it would make the integration very fragile. CSS is very powerful, which means people injecting CSS would be able to make significant changes that we won't be able to test and will likely cause the UI to break. With that being said, there are two alternatives: 1. We support a set of white-labeling customizations, and are always happy to add more. Please reach out with your suggest customization. 2. You can refer to the [implementing your own consumer app portal](#implementing-your-own-consumer-app-portal-functionality) section above which will enable you to build a fully custom UI. [`svix-react`]: https://www.npmjs.com/package/svix-react --- title: Consumer App Portal (OSS) --- # Consumer App Portal (OSS) The [Consumer Application Portal](/app-portal) is currently only included in the [hosted version of Svix](https://www.svix.com) and not in the open source version. This is probably going to change in the future, but for now you can just build your own. Fortunately, building your own is fairly simple using [`svix-react`] if you use React, or directly with the [JavaScript / TypeScript SDK](https://www.npmjs.com/package/svix) otherwise. Using the `svix-react` hooks requires wrapping your application or components in ``: ```jsx import { SvixProvider } from 'svix-react' export default function App() { return ( {/** your app's components **/} ) } ``` Then, you can use hooks like `useEndpoints`: ```jsx import { useEndpoints } from 'svix-react' export default function ListEndpoints() { const endpoints = useEndpoints() return (
{endpoints.error &&
An error has occurred
} {endpoints.loading &&
Loading...
}
    {endpoints.data?.map((endpoint) => (
  • {endpoint.url}
  • ))}
) } ``` [`svix-react`]: https://www.npmjs.com/package/svix-react --- title: Channels --- # Channels Channels are an extra dimension of filtering messages that is orthogonal to [event types](./event-types.mdx). You can listen to multiple channels from each endpoint, and you can send each message to multiple channels. Event types imply a specific consistent schema, and mean a specific type of message. Channels are filters based on the expected recipient or group of recipients. **Note:** like event types, channels are meant to filter messages to a particular application, and not across applications. ## How to use it You first need to enable support to it for each of your environments from [the dashboard](https://dashboard.svix.com/settings/organization/general-settings). Once enabled, your customers can choose their wanted channels from the Application Portal, or alternatively you can set it per endpoint in the API. ![Add endpoint with channels](/img/channels.png) You then need to send messages with the corresponding channels in order for these to reach the endpoints that filter by them. See below for the channels filtering rules. ```typescript await svix.message.create('app_id', { eventType: "user.signup", channels: [ "project_123", "project_group_11" ], payload: { "username": "test_user", "email": "test@example.com" }, }); ``` ## When not to use it The channels are not meant as a way to filter messages across different customers, that's what applications are for. Applications provide proper isolation between messages of different customers, while channels do not. Additionally, while you can have an infinite number of applications, there's a limit to the number of endpoints per application (more on that [on the Endpoint API docs](https://api.svix.com/docs#tag/Endpoint)). Additionally, there are limits to the number of channels messages and endpoints can be associated with. And having applications with a large number of endpoints is very inefficient and can hurt performance. ## Example use-cases Channels are useful for when you have a variety of sub-categories or recipients that expect the same types of messages but just need additional filtering. For example, consider Github. You may want to define webhooks for the whole organization, but only send certain events to certain endpoints based on the repository. You could just create a Svix App per repository and then manually add the endpoints to each, but it makes for a much better experience to have the webhook handling defined in one place with the same endpoints listening to multiple projects. So for example, you can have `svix`, `svix/svix-webhooks` and `svix/svix-docs` as channels, and then have Github send messages for both `svix` and for each repo whenever an event occurs on a specific repository. Github's customers can then create endpoints that listen to events either for the whole group, or for each repository in particular. ## Channels filtering rules Channels are case-sensitive, and endpoints that are filtering for specific channels will only get messages sent to that specific channel. Svix will send (or not send) to endpoints based on the following conditions: 1. Endpoint has no channels set: this is a catch-all, all messages are sent to to it, regardless of whether the message had channels set. 2. Both endpoint and message have channels set: if there's a shared channel between them, the message will be sent to the endpoint. 3. Endpoint has channels set and message has *no* channels set: the message will not be sent to the endpoint. --- title: Common Usage Examples --- # Common Usage Examples This document includes examples and useful information for how to model your use-case with Svix. This list is not exhaustive and only covers some of the more common examples. Svix is used by a variety of different customers for a variety of different use-cases. If your use-case doesn't fit any of the below, please [contact us](/get-help/) and we would be happy to chat on how to best use Svix in your particular circumstances. ## Preamble While different use-cases often require different solutions, there are a few Svix features that are useful in most use-cases and we will highlight them in this section. ### Consumer Application and Endpoint UIDs, and message event IDs Svix enables you to use your internal `ID`s as ids for Svix entities by using the `UID` property of consumer applications and endpoints, and `eventId` of messages. This lets you use Svix in a completely stateless manner, without having to store the Svix identifiers (or anything) in your own database. For more information, please refer to the [section about `UID`s in the overview](./overview.mdx#ids-and-uids). ### Event types Each message sent through Svix has an associated [event type](./event-types.mdx). Event types are identifiers denoting the type of message being sent and are the primary way for webhook consumers to configure what events they are interested in receiving. You can even automatically generate public documentation for your webhooks [by making your event catalog public](./event-types.mdx#publishing-your-event-catalog). ### Consumer Application portal Svix comes with an application portal for your users that you can use out of the box. Your users can then use it to add endpoints, debug delivery, as well as inspect and replay past webhooks. The application portal can be used as a standalone page, or embedded in your own dashboard using an iframe. Most Svix customers use it in an iframe, or implement it themselves (either fully or partially) using the Svix API. For more information, please refer to the [app portal section of the docs](./app-portal.mdx). ### Idempotency Svix supports idempotency for safely retrying requests without accidentally performing the same operation twice. This is useful when an API call is disrupted in transit and you do not receive a response. For more information, please refer to the [idempotency section of the docs](./idempotency.mdx). ### Webhooks sent by Svix As you may expect from a webhooks service, Svix also uses webhooks to notify you of events. For example, Svix will send you a webhook when a message delivery has failed, or an endpoint has been failing for too long of a period. For more information, please refer to the [operational webhooks section of the docs](./incoming-webhooks.mdx). ### Environments (sub-accounts) Svix supports having multiple environments (sub-accounts) within the same Svix account. One common use-case is to separate production, staging and development environments. Another one, is ensuring data stays in a specific geography (e.g. the EU) for compliance reasons. For more information, please refer to the [managing environments section of the docs](./account/environments.mdx). ## Example use-cases ### The common use-case (probably you) #### Who is it for This use-case is the right one if you have different customers on your service, and each of them gets sent webhooks independently based on activities on their own separate accounts. This is the most common use-case and probably matches most people. One company that matches this description is Stripe. Every Stripe customer has their own account, and webhooks are sent by Stripe to a specific account. The webhooks may cover the activities of many of a particular Stripe customer's users (e.g. whether they paid), but webhooks sent by Stripe only ever affect one customer. Another example is [Clerk](https://clerk.dev). Each of Clerk's customers may subscribe to webhooks related to the activity of its own account only. #### Usage example A service matching this description should create one application for each one of its customers. The service will then send events related to this specific customer to the application, and offer the [application portal](./app-portal.mdx) to each of its customers so that customers can manage their own webhooks. As mentioned above, the service can utilize event types to let its customers filter which event they would like to listen to, and it can also utilize application `UID`s to [use Svix in a fully stateless manner](./overview.mdx#ids-and-uids). ### Multiple channels use-case #### Who is it for This use-case is similar to the common one, but with one small difference: your customers may take part in completely separate activities or projects. One common example for this is project management software such as Github, Jira, and Linear. Github users may be watching multiple repositories and be a member of multiple organizations. Their customers want to be able to choose which webhooks should get to which endpoints based on the project or organization. #### Usage example One easy solution is to use a different application for each project. While it's possible to do it this way, it does pose a few limitations. First of all, the app portal only shows one application at a time, so managing endpoints across different applications is difficult. But also, oftentimes you'd like to listen for events affecting multiple projects from the same endpoint, which is very cumbersome if you've created multiple applications. Enter [channels](./channels.mdx). Channels are an extra dimension of filtering messages that is similar, but orthogonal to event types. Channels let you include in the message which channels have been affected by a particular change. For example, you can send a message and mark it as affecting `proj_12` and `proj_35`. Endpoints can then be marked as listening to only changes affecting particular projects (channels) and the messages will be filtered accordingly. Services matching this use-case often implement their own UI for adding and editing endpoints instead of using the application portal, in order to build a nice UI for auto completing, and choosing relevant channels. They would still use the application portal for offering their customers a way to gain visibility into the webhooks being sent, just in read-only mode. ### Partners use-case #### Who is it for This use-case is for services where the webhooks are not being ingested by the service's customers, but rather by partners that, for example, build extensions for the service. Let's take Zoom for example. They have an application marketplace, and you can install different applications on your Zoom account. As a Zoom customer, you are not actually listening to webhooks yourself, but Zoom needs to send them to the applications you've installed. #### Usage example There are multiple ways to achieve this, but the recommended method is to have one application per partner. When an event is triggered for a customer that a partner is interested in, a message is sent to this partner's app (as well as those for any other partners listening). Let's take a look at a concrete example: let's assume we have a customer that has enabled multiple partners on their account (e.g. installed those apps). Your system knows which partners are enabled for that specific customer (as they enabled them on your system in the first place), for simplicity, let's assume you can get that list by calling `list_enabled_partners()`. Then, whenever an event happens on your end for a specific customer, you can get the list of enabled partners and send all of them messages one-by-one, similar to the following pseudo-code: ```python # Some event was triggered on your system for a customer # Send a message to the relevant customer svix.message.create(customer.id, {...}) # Get the list of all of the enabled partners for this customer enabled_partners = customer.list_enabled_partners() # Send a message for each enabled partner for partner in enabled_partners: svix.message.create(partner.id, {...}) ``` ### The mix and match use-case #### Who is it for Some services have more complex use-cases that match more than one of the above examples. For example, your service may have both partners and end-users, and both may need to be able to listen to webhooks. Or maybe even two types of customers listening to the same events (so essentially "using" the common scenario twice). One such example is Gmail, where the user may want to connect third party add-ons (e.g. a CRM) and listen to a webhook themselves. #### Usage example One easy solution is to just have the equivalent of multiple use-cases layered on top of one another, i.e., send messages to multiple applications that are exposed differently in your service's dashboard to different partners and customers based on their roles. ### Other use-cases As mentioned above, this list is not exhaustive and only covers some of the more common examples. Svix is used by a variety of customers for a variety of use-cases. If your use-case doesn't fit any of the above, please [contact us](/get-help/) and we would be happy to chat on how to best use Svix in your particular circumstances. --- title: Custom Connector --- # Custom Connector If you don't find a built-in integration for the service you want to connect to, you can create a Custom Connector. ![Custom Connector in the App Portal](/img/connectors/custom-connector-dropdown.png) ## Configuration You can provide your own integration name and icon, and write instructions for your users on how to use it (in addition to the [default transformation code](/connectors/#how-to-use-connectors) like in any other connector). ![Custom Connector](/img/connectors/custom-connector-form.png) You can also (optionally) configure a default endpoint URL and a default authentication method for custom connectors. This will prefill certain fields in the app portal for your customers, to make it easier for them to use the connector. ![Custom Connector Authentication Configuration](/img/connectors/custom-connector-form-auth.png) The supported authentication methods are `HttpBasic` and `HttpBearer`. When using `HttpBasic`, the App Portal will display a `username` and `password` input field, and add the `Authorization: Basic base64(:)` header to the endpoint (see [RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617)) . When using `HttpBearer`, the App Portal will display a `token` input field, and add the `Authorization: Bearer ` header to the endpoint (see [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750)). ![Custom Connector Authentication](/img/connectors/custom-connector-auth.png) --- title: Connectors Endpoints asIndexPage: true --- # Connectors Endpoints Connectors is a Svix feature that lets you provide your customers with pre-made integrations to connect your webhooks to other services. ## How to use Connectors Connectors are enabled when [enabling transformations](/transformations#enabling-transformations). After enabling Transformations, you will see a new dashboard section where you will be able to create new connectors. ![Connectors list](/img/connectors-list.png) To configure a connector, you need to provide the following details: - `Type`: Choose from a set of services that provide easy webhook integrations, or choose `Custom` to build your own integration. We currently support Slack, Discord, and many more services, listed below. - `Description`: Describe the integration to your customers. - `Event Types`: Specify which events are going to be supported out-of-the-box by the integration. - `Transformation Code`: This is the glue code that will turn incoming webhook events into useful payloads for your integration. For example, if you are building a connector that sends Slack messages every time a new invoice is created, the transformation will read `invoice.created` events and return a payload that works for Slack, like: ```js webhook.payload = { "text": `An invoice of $${webhook.payload.amount} has been created.` } ``` The transformation code needs to account for all event types you choose to support. You can define different behaviors for each event type by switching on the `webhook.eventType` field. For example, this transformation will create a different message depending on if the event type is `invoice.created` or `invoice.deleted`: ```js /** * @param webhook the webhook object * @param webhook.method destination method. Allowed values: "POST", "PUT", "PATCH" * @param webhook.url current destination address * @param webhook.eventType current webhook Event Type * @param webhook.payload JSON payload * @param webhook.cancel whether to cancel dispatch of the given webhook */ function handler(webhook) { switch (webhook.eventType) { case "invoice.created": webhook.payload = { text: `${webhook.payload.name} created an invoice for $${webhook.payload.amount}` }; break; case "invoice.deleted": webhook.payload = { text: `${webhook.payload.name} deleted an invoice` }; break; } return webhook } ``` Your customers will be able to customize the transformation code and the event types they listen to. Your definition should act as a working starting point they can use. ## Connectors in the App Portal After creating at least one connector, your customers will be able to choose it when creating an endpoint. ![Adding an Endpoint using a Connector](/img/connectors-endpoint.png) ## Connection types ### Slack [#slack] The Slack built-in integration lets you send messages to Slack channels via webhooks. It shows a 'Connect to Slack' button that lets your users get a Slack incoming webhook URL using OAuth in a few clicks, without leaving your site. ![Connect to Slack](/img/connectors/connect-to-slack.png) The transformation code should format the webhook payload according to [Slack's Incoming Webhook API](https://api.slack.com/messaging/webhooks#posting_with_webhooks). Here's an example transformation: ```js function handler(webhook) { /** * Example payload is: * { * amount: 99.99, * customer_name: "John Doe", * invoice_id: "inv_123" * } * * See https://api.slack.com/messaging/webhooks#posting_with_webhooks */ webhook.payload = { text: `💰 New invoice for $${webhook.payload.amount} from ${webhook.payload.customer_name}` }; return webhook; } ``` ### Discord [#discord] The Discord built-in integration lets you send messages to Discord channels via webhooks. It shows a 'Connect to Discord' button that lets your users get a Discord incoming webhook URL using OAuth in a few clicks, without leaving your site. ![Connect to Discord](/img/connectors/connect-to-discord.png) The transformation code should format the webhook payload according to [Discord's Execute Webhook API](https://discord.com/developers/docs/resources/webhook#execute-webhook). Here's an example transformation: ```js function handler(webhook) { /** * Example payload is: * { * user: { * email: "john@example.com", * name: "John Doe" * }, * cancellation_reason: "Too expensive" * } * * See https://discord.com/developers/docs/resources/webhook#execute-webhook */ webhook.payload = { embeds: [{ title: "❌ Subscription Cancelled", description: `User ${webhook.payload.user.email} cancelled their subscription`, color: 0xff0000 }] }; return webhook; } ``` ### Microsoft Teams [#teams] The Microsoft Teams integration will show your users instructions on how to get a Teams webhook URL. The transformation code should format the webhook payload according to [Microsoft Teams' Incoming Webhook API](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?#send-adaptive-cards-using-an-incoming-webhook). ![Connect to Microsoft Teams](/img/connectors/connect-to-teams.png) Here's an example transformation for a support ticket created event: ```js function handler(webhook) { /** * Example payload is: * { * ticket_id: "TICKET-123", * customer_name: "John Doe", * priority: "high", * description: "Can't access my account" * } * * See https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?#send-adaptive-cards-using-an-incoming-webhook */ webhook.payload = { type: "message", attachments: [{ contentType: "application/vnd.microsoft.card.adaptive", content: { type: "AdaptiveCard", body: [ { type: "TextBlock", text: "🆘 New Support Ticket", weight: "Bolder" }, { type: "TextBlock", text: `Ticket #${webhook.payload.ticket_id} from ${webhook.payload.customer_name}` } ] } }] }; return webhook; } ``` ### Hubspot [#hubspot] The Hubspot integration lets you connect webhooks directly to the [Hubspot API](https://developers.hubspot.com/docs/api/overview). It shows a 'Connect to Hubspot' button that lets your users get a Hubspot access token that can be used to connect to the Hubspot API. The integration will request access to the following [OAuth scopes](https://developers.hubspot.com/docs/api/working-with-oauth) in the [Hubspot API](https://developers.hubspot.com/docs/api/overview): - `crm.objects.contacts.write` - `crm.objects.companies.write` - `crm.objects.deals.write` - `crm.objects.quotes.write` - `crm.objects.marketing_events.write` ![Connect to Hubspot](/img/connectors/connect-to-hubspot.png) The transformation code should set the webhook URL to the Hubspot API endpoint (depending on the object) and format the payload appropriately to create the object in Hubspot. For example, if you send a `user.created` webhook, and want to write a connector that creates a contact in Hubspot, the transformation code could look like this: ```js function handler(webhook) { /** * Example payload is: * { * user: { * firstname: 'John', * lastname: 'Doe', * email: 'john.doe@example.com' * } * } */ webhook.url = "https://api.hubapi.com/crm/v3/objects/contacts"; // The method is already POST webhook.payload = { properties: { email: webhook.payload.email, firstname: webhook.payload.user.firstname, lastname: webhook.payload.user.lastname, } }; return webhook } ``` ### Close CRM [#closecrm] The Close CRM connector lets your users connect their webhooks to the Close CRM API. Users enter their Close CRM API key in the [App Portal](/app-portal). This example uses [Close CRM's Contact API](https://developer.close.com/resources/contacts/), formatting the payload to create a contact in Close CRM: ```js function handler(webhook) { /** * Example payload is: * { * user: { * email: "john@example.com", * name: "John Doe", * phone: "+1234567890" * } * } * * See https://developer.close.com/resources/contacts/ */ webhook.url = "https://api.close.com/api/v1/contact/"; webhook.payload = { lead_id:"lead_QyNaWw4fdSwxl5Mc5daMFf3Y27PpIcH0awPbC9l7uyo", name: webhook.payload.user.name, emails: [webhook.payload.user.email], phones: [webhook.payload.user.phone] }; return webhook; } ``` ### Segment [#segment] The Segment connector lets your users send events to Segment's API. Users enter their Segment Write Key in the [App Portal](/app-portal). This example uses [Segment's Track API](https://segment.com/docs/connections/sources/catalog/libraries/server/http-api/#track), formatting the payload appropriately: ```js function handler(webhook) { /** * Example payload is: * { * user: { * id: "user123", * email: "john@example.com", * name: "John Doe" * }, * event: "subscription_created", * properties: { * plan: "premium", * price: 99.99 * } * } * * See https://segment.com/docs/connections/sources/catalog/libraries/server/http-api/#track */ webhook.url = "https://api.segment.io/v1/track"; webhook.payload = { userId: webhook.payload.user.id, event: webhook.payload.event, properties: { ...webhook.payload.properties, email: webhook.payload.user.email, name: webhook.payload.user.name }, timestamp: new Date().toISOString() }; return webhook; } ``` ### Customer.io [#customerio] The Customer.io connector lets your users send events to Customer.io's Track API. Users enter their Customer.io Site ID and API Key in the [App Portal](/app-portal). This example uses [Customer.io's Track API](https://docs.customer.io/integrations/api/track/), formatting the payload appropriately: ```js function handler(webhook) { /** * Example payload is: * { * user: { * id: "user123", * email: "john@example.com", * name: "John Doe" * }, * event: "subscription_created", * properties: { * plan: "premium", * price: 99.99 * } * } * * See https://docs.customer.io/integrations/api/track/ */ webhook.url = "https://track.customer.io/api/v2/entity"; webhook.payload = { type: "person", identifiers: { id: webhook.payload.user.id }, action: webhook.payload.event, name: webhook.payload.event, attributes: { ...webhook.payload.properties, email: webhook.payload.user.email, name: webhook.payload.user.name } }; return webhook; } ``` ### Sendgrid [#sendgrid] The SendGrid connector lets your users send events as emails using SendGrid. Users enter their SendGrid API key in the [App Portal](/app-portal). The transformation code should format the webhook payload according to [SendGrid's Mail Send API](https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send). Here's an example transformation for a user signup event: ```js function handler(webhook) { /** * Example payload is: * { * user: { * email: "john@example.com", * name: "John Doe" * } * } * * See https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send */ webhook.payload = { personalizations: [ { to: [{ email: webhook.payload.user.email }] } ], from: { email: "notifications@yourdomain.com", name: "Your App" }, subject: "Welcome to Your App!", content: [ { type: "text/html", value: `

Welcome ${webhook.payload.user.name}!

Thank you for signing up to our service.

` } ] }; return webhook; } ``` ### Resend [#resend] The Resend connector lets your users send events as emails using Resend. Users enter their Resend API key in the [App Portal](/app-portal). The transformation code should format the webhook payload according to [Resend's Email API](https://resend.com/docs/api-reference/emails/send-email). Here's an example transformation for a user signup event: ```js function handler(webhook) { /** * Example payload is: * { * user: { * email: "john@example.com", * name: "John Doe" * } * } * * See https://resend.com/docs/api-reference/emails/send-email */ webhook.payload = { from: "Your App ", to: [webhook.payload.user.email], subject: "Welcome to Your App!", html: `

Welcome ${webhook.payload.user.name}!

Thank you for signing up to our service.

` }; return webhook; } ``` ### Loops [#loops] The Loops connector lets your users send events to Loops to trigger email automations. Users enter their Loops API key in the [App Portal](/app-portal). The transformation code should format the webhook payload according to [Loops' Send Event API](https://loops.so/docs/api-reference/send-event). Here's an example transformation for a user signup event: ```js function handler(webhook) { /** * Example payload is: * { * user: { * email: "john@example.com", * id: "user123" * } * } * * See https://loops.so/docs/api-reference/send-event */ webhook.payload = { email: webhook.payload.user.email, userId: webhook.payload.user.id, eventName: "user_signup", eventProperties: { source: "webhook" } }; return webhook; } ``` ### Windmill [#windmill] The Windmill connector lets your users get a [Windmill](https://windmill.dev) webhook URL in a few clicks from the App Portal. It supports [Windmill Cloud](https://app.windmill.dev) as well as self-hosted Windmill instances. ![Connect to Windmill](/img/connectors/connect-to-windmill.png) Since Windmill webhooks support arbitrary payloads, it does not require any custom transformation code. ### Inngest [#inngest] The Inngest connector lets your users send events to Inngest to trigger serverless functions. Users enter their Inngest API key in the [App Portal](/app-portal). Since Inngest webhooks support arbitrary payloads, it does not require any custom transformation code. ### Zapier [#zapier] The Zapier connector lets your users send events to Zapier to trigger automations. It shows instructions on how to get a Zapier webhook URL. Since Zapier Webhooks support arbitrary payloads, it does not require any custom transformation code. ### Custom Integration In case you don't find a built-in integration for the service you want to connect to, you can create a custom integration. You can provide your own integration name and icon, and write instructions for your users on how to use it. [Learn more about Custom Connectors](/connectors/custom-connector) ![Custom Integration](/img/connectors/custom-integration.png) If you have a request for a built-in integration not listed above, [contact us](https://www.svix.com/contact/). --- title: Consuming Webhooks --- # Consuming Webhooks In addition to helping you send webhooks to your users, we also help your users to easily verify the authenticity and security of the webhooks they receive from you. For information on how to verify webhooks, please head to the [Consuming Webhooks documentation](/receiving/introduction.mdx). ## Documentation for your users We offer easy to use docs for how to safely consume webhooks which you can share with your users directly: [Consuming Webhooks documentation](/receiving/introduction.mdx). ## Building your own libraries Depending on your product, you may want to offer additional processing before passing the verified payload for your customers. For example, you may want to create API objects from the payload that your users can use to interact with your API. In this scenario you would want to create your own `Webhook` class equivalent that uses the `Svix` class internally. This way you can get all of the verification that Svix offer, while still being able to post-process the payload before passing it to your users. --- title: Documenting Your Webhooks --- # Documenting Your Webhooks Once you're ready to launch your webhook feature, you'll need to add documentation. At Svix we put a lot of effort into making it as easy as possible for our customers to send webhooks reliably at scale. By extension, this includes helping our customers write great docs. We recommend having 7 sections in your webhook docs: an intro, an explanation of available events and event types, instructions on how to add an endpoint, how to test endpoints, why and how to verify signatures, an explanation of the retry mechanism, and troubleshooting/failure recovery tips. Here are some sample text and examples of each section to help you get started: **The Intro** The introduction to your webhook docs should give a brief explanation of what webhooks are and how to set them up. Here's an example you can just copy and put in your docs: ```plaintext filename="The Intro" Webhooks are how services notify each other of events. At their core they are just a POST request to a pre-determined endpoint. The endpoint can be whatever you want, and you can just add them from the UI. You normally use one endpoint per service, and that endpoint listens to all of the event types. For example, if you receive webhooks from Acme Inc., you can structure your URL like: https://www.example.com/acme/webhooks/. The way to indicate that a webhook has been processed is by returning a 2xx (status code 200-299) response to the webhook message within a reasonable time-frame (15s). It's also important to disable CSRF protection for this endpoint if the framework you use enables them by default. Another important aspect of handling webhooks is to verify the signature and timestamp when processing them. You can learn more about it in the signature verification section. ``` Here is an example from one of our customers, Brex: [https://developer.brex.com/guides/webhooks](https://developer.brex.com/guides/webhooks) **Events and Event Types** The core value of webhooks is to notify users when events happen, so its extremely important to document what events are available and provide schemas and payload examples. We make this very simple with our [Event Catalog](https://docs.svix.com/event-types#publishing-your-event-catalog) feature. You can take a look at [Brex's docs](https://developer.brex.com/guides/webhooks) again for an example of the event catalog. You can also preview your own event catalog in your Svix dashboard once you're added Event Types. **How to Add an Endpoint** After understanding what events they want to listen for, your users will need to specify an endpoint where they can receive the webhooks. Sample text: ```plaintext filename="Adding an Endpoint" In order to start listening to messages, you will need to configure your endpoints. Adding an endpoint is as simple as providing a URL that you control and selecting the event types that you want to listen to. If you don't specify any event types, by default, your endpoint will receive all events, regardless of type. This can be helpful for getting started and for testing, but we recommend changing this to a subset later on to avoid receiving extraneous messages. If your endpoint isn't quite ready to start receiving events, you can press the "with Svix Play" button to have a unique URL generated for you. You'll be able to view and inspect webhooks sent to your Svix Play URL, making it effortless to get started. ``` **How to Test Endpoints** Once a user specifies an endpoint, they'll want to test it to make sure its working correctly. The simplest way to do this is to send test messages to the endpoint under the "Testing" tab. Sample text: ```plaintext filename="Testing Endpoints" Once you've added an endpoint, you'll want to make sure its working. The "Testing" tab lets you send test events to your endpoint. After sending an example event, you can click into the message to view the message payload, all of the message attempts, and whether it succeeded or failed. ``` **Signature Verification** One of the most common ways that webhooks fail is a faulty signature verification mechanism. The endpoint rejects a webhook thinking its fraudulent when in reality, they simply did the verification incorrectly. We'll discuss common reasons for failed signature verification in the troubleshooting section. Here our goal is to clearly explain how and why to verify signatures and provide code samples that users can simply copy/paste to get a working endpoint. Sample text: ```plaintext filename="Verifying Signatures" Webhook signatures let you verify that webhook messages are actually sent by us and not a malicious actor. For a more detailed explanation, check out this article on [why you should verify webhooks](https://docs.svix.com/receiving/verifying-payloads/why). Our webhook partner Svix offers a set of useful libraries that make verifying webhooks very simple. Here is a an example using Javascript: ``` Javascript sample: ```javascript filename="Javascript Code Sample" import { Webhook } from "svix"; const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"; // These were all sent from the server const headers = { "svix-id": "msg_p5jXN8AQM9LWM0D4loKWxJek", "svix-timestamp": "1614265330", "svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=", }; const rawBody = '{"test": 2432232314}'; const wh = new Webhook(secret); // Throws on error, returns the verified content on success const verified = wh.verify(rawBody, headers); ``` For more instructions and examples of how to verify signatures, check out their [webhook verification documentation](https://docs.svix.com/receiving/verifying-payloads/how). We also have some customers who simply link to our [webhook verification docs](https://docs.svix.com/receiving/verifying-payloads/how) directly (e.g. [Clerk](https://clerk.com/docs/integrations/webhooks/overview) and [incident.io](https://help.incident.io/en/articles/6984344-webhooks)) as they have code samples in 10 different languages. **Retries** Retries are one of the core features of Svix that make webhooks more reliable. You want to let your users know under what conditions failed messages will be retried and when. Sample text: ```plaintext filename="Retry Schedule" Retries We attempt to deliver each webhook message based on a retry schedule with exponential backoff. The schedule Each message is attempted based on the following schedule, where each period is started following the failure of the preceding attempt: - Immediately - 5 seconds - 5 minutes - 30 minutes - 2 hours - 5 hours - 10 hours - 10 hours (in addition to the previous) If an endpoint is removed or disabled delivery attempts to the endpoint will be disabled as well. For example, an attempt that fails three times before eventually succeeding will be delivered roughly 35 minutes and 5 seconds following the first attempt. Manual retries You can also use the application portal to manually retry each message at any time, or automatically retry ("Recover") all failed messages starting from a given date. ``` Lob does a great job of explaining and showing the retry schedule in their [webhook docs](https://help.lob.com/print-and-mail/getting-data-and-results/using-webhooks#retry-policy-7). **Troubleshooting & Failure Recovery** Adding tips on troubleshooting failing endpoints and how to recover from endpoint failures helps users get unstuck to minimize frustrations when setting up webhooks. Sample text: ```plaintext filename="Troubleshooting Tips" There are some common reasons why your webhook endpoint is failing: Not using the raw payload body This is the most common issue. When generating the signed content, we use the raw string body of the message payload. If you convert JSON payloads into strings using methods like stringify, different implementations may produce different string representations of the JSON object, which can lead to discrepancies when verifying the signature. It's crucial to verify the payload exactly as it was sent, byte-for-byte or string-for-string, to ensure accurate verification. Missing the secret key From time to time we see people simple using the wrong secret key. Remember that keys are unique to endpoints. Sending the wrong response codes When we receive a response with a 2xx status code, we interpret that as a successful delivery even if you indicate a failure in the response payload. Make sure to use the right response status codes so we know when message are supposed to succeed vs fail. Responses timing out We will consider any message that fails to send a response within {timeout duration} a failed message. If your endpoint is also processing complicated workflows, it may timeout and result in failed messages. We suggest having your endpoint simply receive the message and add it to a queue to be processed asynchronously so you can respond promptly and avoiding getting timed out. ``` Sample text for Failure Recovery: ```plaintext filename="Failure Recovery" Re-enable a disabled endpoint If all attempts to a specific endpoint fail for a period of 5 days, the endpoint will be disabled. To re-enable a disabled endpoint, go to the webhook dashboard, find the endpoint from the list and select "Enable Endpoint". Recovering/Resending failed messages If your service has downtime or if your endpoint was misconfigured, you probably want to recover any messages that failed during the downtime. If you want to replay a single event, you can find the message from the UI and click the options menu next to any of the attempts. From there, click "resend" to have the same message send to your endpoint again. If you need to recover from a service outage and want to replay all the events since a given time, you can do so from the Endpoint page. On an endpoint's details page, click "Options > Recover Failed Messages". From there, you can choose a time window to recover from. For a more granular recovery - for example, if you know the exact timestamp that you want to recover from - you can click the options menu on any message from the endpoint page. From there, you can click "Replay..." and choose to "Replay all failed messages since this time." ``` If you'd like us to take a look at your docs before or after your launch, just reach out and review them. --- title: Email Notifications --- # Email Notifications Svix supports sending personalized email notifications to your customers when certain events occur with their webhooks setup. This is useful for keeping customers aware of webhook delivery issues that might affect their integrations. {/* ## How email notifications work */} Email notifications are sent per [Consumer Application](/overview#consumer-applications). For example, when an endpoint for a particular Application is disabled due to repeated failures, Svix will send an email to the configured email address for that application. ## Setting up email notifications ### Enable email notifications in your organization To get started, you'll need to enable email notifications for your organization. Head to the [Email Notifications](https://dashboard.svix.com/settings/organization/email-notifications) section of the Svix dashboard. ### Configure your branding Personalize the email notifications by adding: - Company name - The name of your company or service - Logo URL - A link to your company logo for branding - Webhooks management page URL - Where your customers can manage their webhook settings ### Set the email address for each application For each application that should receive email notifications, you'll need to specify an email address. This is done by setting the `svix.email` field in the Application's `metadata`. You can set the email address when creating an application, or by updating an existing application's metadata. #### When creating an application ```js import { Svix } from "svix"; const svix = new Svix("AUTH_TOKEN"); const app = await svix.application.create({ name: "Example customer 123", uid: "example-customer-123", metadata: { "svix.email": "example-customer-123@example.com" } }); ``` ```python from svix.api import Svix, ApplicationIn svix = Svix("AUTH_TOKEN") app = svix.application.create(ApplicationIn( name="Example customer 123", uid="example-customer-123", metadata={ "svix.email": "example-customer-123@example.com" } )) ``` ```rust use svix::api::{ApplicationIn, Svix, SvixOptions}; let svix = Svix::new("AUTH_TOKEN".to_string(), None); let mut metadata = std::collections::HashMap::new(); metadata.insert("svix.email".to_string(), serde_json::Value::String("example-customer-123@example.com".to_string())); let app = svix .application() .create( ApplicationIn { name: "Example customer 123".to_string(), uid: Some("example-customer-123".to_string()), metadata: Some(metadata), ..ApplicationIn::default() }, None, ) .await?; ``` ```go import ( svix "github.com/svix/svix-webhooks/go" ) svixClient := svix.New("AUTH_TOKEN", nil) metadata := map[string]interface{}{ "svix.email": "example-customer-123@example.com", } app, err := svixClient.Application.Create(ctx, &svix.ApplicationIn{ Name: "Example customer 123", Uid: "example-customer-123", Metadata: &metadata, }) ``` ```java import com.svix.models.ApplicationIn; import com.svix.models.ApplicationOut; import com.svix.Svix; import java.util.HashMap; import java.util.Map; Svix svix = new Svix("AUTH_TOKEN"); Map metadata = new HashMap<>(); metadata.put("svix.email", "example-customer-123@example.com"); ApplicationOut app = svix.getApplication().create( new ApplicationIn().name("Example customer 123").uid("example-customer-123").metadata(metadata) ); ``` ```kotlin import com.svix.kotlin.models.ApplicationIn import com.svix.kotlin.models.ApplicationOut import com.svix.kotlin.Svix val svix = Svix("AUTH_TOKEN") val metadata = mapOf("svix.email" to "example-customer-123@example.com") val applicationOut = svix.application.create( ApplicationIn( name = "Example customer 123", uid = "example-customer-123", metadata = metadata ) ) ``` ```ruby require "svix" svix = Svix::Client.new("AUTH_TOKEN") application_out = svix.application.create(Svix::ApplicationIn.new({ "name" => "Example customer 123", "uid" => "example-customer-123", "metadata" => { "svix.email" => "example-customer-123@example.com" } })) ``` ```csharp var svix = new SvixClient("AUTH_TOKEN", new SvixOptions("https://api.us.svix.com")); var metadata = new Dictionary { { "svix.email", "example-customer-123@example.com" } }; var applicationOut = await svix.Application.CreateAsync( new ApplicationIn(name: "Example customer 123", uid: "example-customer-123", metadata: metadata) ); ``` ```php $svix = new Svix('AUTH_TOKEN'); $metadata = [ 'svix.email' => 'example-customer-123@example.com' ]; $applicationOut = $svix->application->create( ApplicationIn::create( name: 'Example customer 123' ) ->withUid('example-customer-123') ->withMetadata($metadata) ); ``` ```shell export SVIX_AUTH_TOKEN='AUTH_TOKEN' svix application create '{ "name": "Example customer 123", "uid": "example-customer-123", "metadata": { "svix.email": "example-customer-123@example.com" } }' ``` ```shell export SVIX_AUTH_TOKEN='AUTH_TOKEN' curl -X POST "https://api.us.svix.com/api/v1/app/" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${SVIX_AUTH_TOKEN}" \ -d '{ "name": "Example customer 123", "uid": "example-customer-123", "metadata": { "svix.email": "example-customer-123@example.com" } }' ``` #### Updating an existing application ```js import { Svix } from "svix"; const svix = new Svix("AUTH_TOKEN"); await svix.application.patch("example-customer-123", { metadata: { "svix.email": "example-customer-123@example.com" } }); ``` ```python from svix.api import Svix svix = Svix("AUTH_TOKEN") svix.application.patch("example-customer-123", { "metadata": { "svix.email": "example-customer-123@example.com" } }) ``` ```rust let svix = Svix::new("AUTH_TOKEN".to_string(), None); let mut metadata = std::collections::HashMap::new(); metadata.insert("svix.email".to_string(), serde_json::Value::String("example-customer-123@example.com".to_string())); let app = svix .application() .patch( "example-customer-123".to_string(), ApplicationPatch { metadata: Some(metadata), ..ApplicationPatch::default() }, ) .await?; ``` ```go import ( svix "github.com/svix/svix-webhooks/go" ) svixClient := svix.New("AUTH_TOKEN", nil) metadata := map[string]interface{}{ "svix.email": "example-customer-123@example.com", } app, err := svixClient.Application.Patch(ctx, "example-customer-123", &svix.ApplicationPatch{ Metadata: &metadata, }) ``` ```java import com.svix.Svix; import com.svix.models.ApplicationPatch; import java.util.HashMap; import java.util.Map; Svix svix = new Svix("AUTH_TOKEN"); Map metadata = new HashMap<>(); metadata.put("svix.email", "example-customer-123@example.com"); svix.getApplication() .patch("example-customer-123", new ApplicationPatch() .metadata(metadata) ); ``` ```kotlin import com.svix.kotlin.Svix import com.svix.kotlin.models.ApplicationPatch val svix = Svix("AUTH_TOKEN") val metadata = mapOf("svix.email" to "example-customer-123@example.com") svix.application.patch("example-customer-123", ApplicationPatch( metadata = metadata )) ``` ```ruby svix = Svix::Client.new("AUTH_TOKEN") svix.application.patch("example-customer-123", Svix::ApplicationPatch.new({ "metadata" => { "svix.email" => "example-customer-123@example.com" } })) ``` ```csharp var svix = new SvixClient("AUTH_TOKEN", new SvixOptions("https://api.us.svix.com")); var metadata = new Dictionary { { "svix.email", "example-customer-123@example.com" } }; await svix.Application.PatchAsync("example-customer-123", new ApplicationPatch( metadata: metadata )); ``` ```php $svix = new Svix('AUTH_TOKEN'); $metadata = [ 'svix.email' => 'example-customer-123@example.com' ]; $svix->application->patch( 'example-customer-123', ApplicationPatch::create() ->withMetadata($metadata) ); ``` ```shell export SVIX_AUTH_TOKEN='AUTH_TOKEN' svix application patch 'example-customer-123' '{ "metadata": { "svix.email": "example-customer-123@example.com" } }' ``` ```shell export SVIX_AUTH_TOKEN='AUTH_TOKEN' curl -X PATCH "https://api.us.svix.com/api/v1/app/example-customer-123/" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${SVIX_AUTH_TOKEN}" \ -d '{ "metadata": { "svix.email": "example-customer-123@example.com" } }' ``` ### Redirecting to your webhooks management page When the webhooks management page URL is included in emails, Svix adds `svix_app_id` as a query parameter, with the application ID as the value (or the UID if the application has one). This can be used to redirect the user to the right page in your application, in cases where the URL varies per customer. For example, if the webhooks management page URL in your site is `app.example.com/webhooks/customer-123`, set the webhooks management URL to `app.example.com/webhooks`. Then, in your application, add a redirect from `/webhooks/?svix_app_id={APP_UID}` to `/webhooks/{APP_UID}`. ### An example email notification ![Email notification example](/img/email-notification-example.png) --- title: Advanced Endpoint Authentication --- # Advanced Endpoint Authentication Svix supports advanced endpoint authentication methods that can be used by your customers on top of the standard signature verification. These are advanced methods that are not required to keep your webhooks secure, but your customers might need them for their use case. Read the [security docs](/security) for more information. ## Enabling OAuth and mTLS OAuth and mTLS can be enabled at the environment level in the [Svix Dashboard](https://dashboard.svix.com/settings/organization/general-settings). ![Enable Endpoint Authentication](/img/endpoint-authentication/endpoint-authentication-enable.png) When enabled, your users will see an option in the [App Portal](/app-portal) to configure the respective authentication method on their endpoints. ![Endpoint Authentication Configure](/img/endpoint-authentication/endpoint-authentication-configure.png) ## OAuth To configure OAuth, your users will need to enter a `Client ID` and the `Authorization Server URL`, as well as the rest of the OAuth parameters, depending on the desired `Grant type` and `Authentication method`. ![OAuth Authentication](/img/endpoint-authentication/endpoint-configure-oauth.png) ## Mutual TLS (mTLS) With mTLS, your users can upload a private PEM encoded private key and certificate, which will be used to sign the requests sent to the endpoint, and to verify the identity of the receiving server. ![Mutual TLS Authentication](/img/endpoint-authentication/endpoint-configure-mtls.png) For self-signed certificates, a custom Certificate Authority certificate can also be specified. --- title: Event Types --- # Event Types Each message sent through Svix has an associated event type. Event types are identifiers denoting the type of message being sent. Because they are the primary way for webhook consumers to configure what events they are interested in receiving, we highly recommend including an event catalog that describes each event type and provides sample payloads in your documentation. To make it easy for webhook consumers to find and subscribe to the right events, Svix automatically generates an event catalog. See more about [publishing your event catalog here](#publishing-your-event-catalog). Event types are just a string, for example: `user.signup`, `invoice.paid` and `workflow.completed`. Webhook consumers can choose which events are sent to which endpoint. By default, all messages are sent to all endpoints. Though when adding or editing endpoints, users can choose to only subscribe to some of the event types for this particular endpoint. **Bulk Create** It's recommended to add the event types you are going to use ahead of time. You can do it by [importing them from an OpenAPI spec](#import-event-types-from-an-openapi-specification) or [bulk creating them from code](#bulk-creating-event-types-from-code). ## What your users will see This is how choosing event types look like in the [pre-built application portal](app-portal.mdx): ![Management UI screenshot](/img/event-type-selection.png) ## Event Type Format Event types have a pattern defined a `^[a-zA-Z0-9\\-_.]+$`, meaning it can contain any of the following characters: - A through Z (uppercase or lowercase) - 0 through 9 - Special characters: `-`, `_`, `.` **Style guide** We recommend you use period-delimited event type names (e.g. `.`). If you do, the App Portal UI will logically group them for your users and make it easier for them to subscribe an endpoint to all events in a particular group of event types. ## Using event types You can add, edit, and delete event types in [the dashboard](https://dashboard.svix.com) or through the API below. ```js import { Svix } from "svix"; const svix = new Svix("AUTH_TOKEN"); const eventType = await svix.eventType.create({ name: "user.signup", description: "A user has signed up", }); ``` ```python from svix.api import Svix, EventTypeIn svix = Svix("AUTH_TOKEN") app = svix.event_type.create(EventTypeIn( name="user.signup", description="A user has signed up" )) ``` ```rust let svix = Svix::new("AUTH_TOKEN".to_string(), None); let event_type = svix .event_type() .create( EventTypeIn { name: "user.signup".to_string(), description: "A user has signed up".to_string(), ..EventTypeIn::default() }, None, ) .await?; ``` ```go import ( svix "github.com/svix/svix-webhooks/go" ) svixClient := svix.New("AUTH_TOKEN", nil) app, err := svixClient.EventType.Create(ctx, &svix.EventTypeIn{ Name: "user.signup", Description: "A user has signed up", })} ``` ```java import com.svix.Svix; import com.svix.models.EventTypeIn; Svix svix = new Svix("AUTH_TOKEN"); svix.getEventType() .create(new EventTypeIn() .name("user.signup") .description("A user has signed up") ); ``` ```kotlin import com.svix.kotlin.Svix; import com.svix.kotlin.models.EventTypeIn; val svix = Svix("AUTH_TOKEN"); svix.eventType.create( EventTypeIn( name = "user.signup", description = "A user has signed up", )); ``` ```ruby svix = Svix::Client.new("AUTH_TOKEN") svix.event_type.create(Svix::EventTypeIn.new({ "name" => "user.signup", "description" => "A user has signed up"})) ``` ```csharp var svix = new SvixClient("AUTH_TOKEN"); await svix.EventType.CreateAsync(new EventTypeIn( name: "user.signup", description: "A user has signed up" )) ``` ```php $svix = new Svix('AUTH_TOKEN'); $eventType = $svix->eventType->create(EventTypeIn::create( name: 'user.signup', description: 'A user has signed up' )); ``` ```shell export SVIX_AUTH_TOKEN='AUTH_TOKEN' svix event-type create '{ "name": "user.signup", "description": "A user has signed up" }' ``` ```shell export SVIX_AUTH_TOKEN='AUTH_TOKEN' curl -X POST "https://api.us.svix.com/api/v1/event-type/" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${SVIX_AUTH_TOKEN}" \ -d '{ "name": "user.signup", "description": "A user has signed up" }' ``` ## Event Type Schema One of the best ways to help your users integrate with you is by defining schemas for your event types. Event type schemas allow your users to anticipate the shape of the message body they will receive as well as introduce guardrails for each data type. Schemas can be created using our visual schema editor, or by providing your own JSONSchema (Draft 7) spec either in the UI or [the API](https://api.svix.com/docs#tag/Event-Type/operation/v1.event-type.create). ![schema-editor-basic](/img/schema-editor-basic.png) Once you have a schema defined, your users will be able to view the schema definition as well as an example event from the Event Catalog in the App Portal. ![schema-preview](/img/schema-preview.png) To learn more about creating schemas, check out our guide on [adding your first event type schema](./tutorials/event-type-schema). ### Schema validation Svix doesn't enforce the event type schema when creating a message. There are a few reasons to why we do it, though the main one is that we would much rather send a message with a bad schema, than block messages because the schema was slightly wrong. This is especially true for more strict schemas like ones that enforce some fields follow a specific regex (a potentially minor violation). Additionally, since the payload is fully controlled by our customers (you), it's very easy to verify the schema on the sender side, before sending to Svix, which ensures people get exactly the level of validation they expect. We do however love schemas and think they are super important, so we plan on adding an automatic SDK generator that will have type checked and validated schemas for you client side, so that bad schemas will fail at compile time, not run time! ## Import event types from an OpenAPI specification The [OpenAPI specification](https://www.openapis.org/) is a formal standard for describing HTTP APIs. Some people write these by hand, and some automatically generate these from the web framework of their choice. If you already have an OpenAPI specification, you can upload it to Svix which will automatically create event types for you based on the [`webhooks`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#fixed-fields) section (or `x-webhooks` for OpenAPI 3.0). In addition to the standard features supported by the OpenAPI spec, Svix also supports a couple of extensions: `x-svix-feature-flags` and `x-svix-group-name` which let you set the [feature flags](#event-type-feature-flags) and group name on the event type respectively as follows: ```json "webhooks": { "pet.new": { "post": { "operationId": "pet.new", "description": "A new pet has been created", "x-svix-feature-flags": ["beta-feature-a"], "x-svix-group-name": "group-name-1", "requestBody": { ... } } } } ``` If your API spec has a `webhooks` section (or `x-webhooks`) you can continue to the next sections to learn how to upload it. If you need help with adding the `webhooks` section to your OpenAPI spec, please refer to the [Generating an OpenAPI spec section](#generating-an-openapi-spec). ### From the dashboard Your first option for uploading your OpenAPI spec is by uploading it from the Svix dashboard. Once uploaded you'll be faced with this preview which will give you the opportunity to review the added event types before saving them. ![event-type-openapi](/img/event-type-openapi.png) The event description, schema and examples will be used to create the Svix event type, using the `path id` as the event type name. If you use choose the name of an already existing event type, it will be updated with the new values. ### Using the API The API includes an endpoint which helps keep your [event-types in sync with a JSON OpenAPI Spec](https://api.svix.com/docs#tag/Event-Type/operation/v1.event-type.import-openapi). Event types and their schemas are generated from `webhooks` or `x-webhooks` top-level key of the spec, just like with the UI importer. When the event type already exists, it'll be updated with the latest description and schema, otherwise a new event type will be created. ```js const spec = loadOpenapiFromFile("openapi.json"); await svix.eventType.importOpenApi({ spec }); ``` ```python spec = load_openapi_from_file("openapi.json") svix.event_type.import_openapi(EventTypeImportOpenApiIn(spec=spec)) ``` ```rust let spec = load_openapi_from_file("openapi.json")?; let _ = svix .event_type() .import_openapi(EventTypeImportOpenApiIn { spec }, None) .await?; ``` ```go spec := loadOpenapiFromFile("openapi.json") svixClient.EventType.ImportOpenApi(context.Background(), svix.EventTypeImportOpenApiIn{ Spec: spec, }) ``` ```java Map spec = MySpecReader.loadOpenapiFromFile("openapi.json"); svix.getEventType().importOpenApi(new EventTypeImportOpenApiIn().spec(spec)); ``` ```kotlin var spec = load_openapi_from_file("openapi.json") var eventTypeImportOpenApiOut = await svix.EventType.ImportOpenapiAsync( new EventTypeImportOpenApiIn{spec: spec} ) ``` ```ruby spec = load_openapi_from_file("openapi.json") event_type_import_open_api_out = svix.event_type.import_openapi(Svix::EventTypeImportOpenApiIn.new(spec)) ``` ```csharp var spec = loadOpenapiFromFile("openapi.json") var eventTypeImportOpenApiOut = await svix.EventType.ImportOpenapiAsync( new EventTypeImportOpenApiIn{ spec: spec } ) ``` ```php $spec = loadOpenapiFromFile('openapi.json'); $svix = new Svix('AUTH_TOKEN'); $eventTypeImportOpenApiOut = $svix->eventType->importOpenapi( EventTypeImportOpenApiIn::create() -> withSpec($spec) ); ``` ```shell # TBD ``` ```shell export SVIX_AUTH_TOKEN='AUTH_TOKEN' curl -X POST "https://api.us.svix.com/api/v1/event-type/import/openapi/" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${SVIX_AUTH_TOKEN}" \ -d '{"spec":' $(cat path/to/openapi.json) '}' ``` ### Using the Github Action Svix also provides a [Github Action](https://github.com/svix/svix-event-type-import-action) to upload your OpenAPI spec and automatically create or update your event types as part of your CI/CD pipeline. ```yaml - name: Upload Event Types to Svix uses: svix/svix-event-type-import-action@v1.0.0 with: openapi-file: 'path/to/your/openapi-spec.yml' # can be a .json too svix-api-key: ${{ secrets.SVIX_API_KEY }} ``` ## Bulk creating event types from code The easiest way to bulk-create event types is by just writing a simple script to load a [pipe delimited `CSV`](https://en.wikipedia.org/wiki/Comma-separated_values) file or a `JSON` file with the event types and make the API requests. Here is an example `CSV` file (without headers) of events: ```csv user.created|A user has been created user.removed|A user has been removed user.changed|A user has changed ``` Here are some example scripts for processing the above file: ```js import { Svix } from "svix"; import fs from "fs"; import readline from "readline"; const svix = new Svix("AUTH_TOKEN"); async function execute() { const fileStream = fs.createReadStream("./data.csv"); const data = readline.createInterface({ input: fileStream, crlfDelay: Infinity, }); for await (const lineItr of data) { const line = lineItr.split("|"); const eventType = await svix.eventType.create({ name: line[0], description: line[1], }); } } execute(); ``` ```python import json from svix.api import Svix, EventTypeIn svix = Svix("AUTH_TOKEN") with open("./data.csv", "r") as f: for line in f: name, description = line.split("|") app = svix.event_type.create(EventTypeIn( name=name, description=description, )) ``` ```rust use std::{error::Error, fs::File}; use svix::api::{EventTypeIn, Svix}; async fn execute() -> Result<(), Box> { let svix = Svix::new("AUTH_TOKEN".to_string(), None); let mut reader = csv::ReaderBuilder::new() .delimiter(b'|') .from_reader(File::open("src/data.csv")?); for result in reader.records() { let record = result?; svix.event_type() .create( EventTypeIn { name: record[0].to_string(), description: record[1].to_string(), ..EventTypeIn::default() }, None, ) .await?; } Ok(()) } #[tokio::main] async fn main() { execute().await.unwrap(); } ``` ```go package main import ( "bufio" "log" "os" "strings" svix "github.com/svix/svix-webhooks/go" ) func main() { svixClient := svix.New("AUTH_TOKEN", nil) f, err := os.Open("./data.csv") if err != nil { log.Fatal(err) } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.Split(scanner.Text(), "|") _, err = svixClient.EventType.Create(ctx, &svix.EventTypeIn{ Name: line[0], Description: line[1], }) if err != nil { log.Println(err) } } if err := scanner.Err(); err != nil { log.Fatal(err) } } ``` ```java package sviximport; import java.io.FileReader; import java.io.IOException; import java.util.Scanner; import com.svix.Svix; import com.svix.models.EventTypeIn; import com.svix.exceptions.ApiException; public class App { public static void main(String[] args) { Svix svix = new Svix("AUTH_TOKEN"); try (Scanner scanner = new Scanner(new FileReader("/path/to/data.csv"));) { while (scanner.hasNextLine()) { try { String[] line = scanner.nextLine().split("|"); svix.getEventType().create(new EventTypeIn().name(line[0]).description(line[1])); } catch(ApiException e) { if (e.getCode() != 409) { System.out.println(e.getResponseBody()); System.exit(1); } } } } catch(IOException e) { System.out.println(e.toString()); System.exit(1); } } } ``` ```kotlin package sviximport import com.svix.kotlin.Svix import com.svix.kotlin.models.EventTypeIn import com.svix.kotlin.exceptions.ApiException import java.io.File import java.io.IOException import kotlinx.coroutines.runBlocking import kotlin.system.exitProcess fun main() = runBlocking { val svix = Svix("AUTH_TOKEN") try { File("/path/to/data.csv").useLines { lines -> lines.forEach { try { val line = it.split("|") svix.eventType.create(EventTypeIn( name = line[0], description = line[1] )) } catch (e: ApiException) { println(e.message) exitProcess(1) } } } } catch (e: IOException) { println(e.toString()) exitProcess(1) } } ``` ```ruby require "svix" require "csv" svix = Svix::Client.new("AUTH_TOKEN") CSV.foreach('data.csv', {:col_sep => "|"}) do |line| event_type = svix.event_type.create(Svix::EventTypeIn.new({ "name" => line[0], "description" => line[1]})) end ``` ```csharp // Example TBD // Read the CSV and create event types ``` ```php $svix = new Svix('AUTH_TOKEN'); $handle = fopen('./data.csv', 'r'); if ($handle) { while (($line = fgets($handle)) !== false) { $parts = explode('|', trim($line)); $name = $parts[0]; $description = $parts[1]; $eventType = $svix->eventType->create(EventTypeIn::create( name: $name, description: $description )); } fclose($handle); } ``` ```shell export SVIX_AUTH_TOKEN='AUTH_TOKEN' while IFS=$'|', read -r name description do svix event-type create "{ \"name\": \"${name}\", \"description\": \"${description}\" }" done < data.csv ``` ```shell export SVIX_AUTH_TOKEN='AUTH_TOKEN' while IFS=$'|', read -r name description do curl -X POST "https://api.us.svix.com/api/v1/event-type/" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${SVIX_AUTH_TOKEN}" \ -d "{ \"name\": \"${name}\", \"description\": \"${description}\" }" done < data.csv ``` ## Publishing your Event Catalog By default, your event types are only accessible to users from within an authenticated session on the Application Portal. If you would like to have them publicly accessible, you can enable the "Make Public" setting from the Event Catalog configuration screen in the Dashboard settings. Enabling this setting will cause your event types to be statically served on **svix.com**. You can link or embed that site within your own documentation. ![event-catalog-config](/img/event-catalog-config.png) ### Configuration Options - **Make Public:** Whether or not the Event Catalog will be publicly accessible from **svix.com**. - **Display Name:** _Required to make your Event Catalog public._ The display name will be shown in the heading of the published page. It should be the name of your company or product. ### Using a custom domain It is also possible to have the Event Catalog served from your own domain (e.g. `webhooks.example.com`) instead of the default `www.svix.com`. To set this up, please set the following record on your DNS provider: * Type: `CNAME` * Name: `webhooks` (or whatever subdomain you would like to use) * Value: `event-types.svix.com` And [let us know](https://www.svix.com/contact/) once you do so that we can activate it on our end. ### What it looks like ![event-catalog-published](/img/event-catalog-published.png) ## Event type feature flags When introducing new features in your application it can be useful to only expose them to a select set of users. Event types can be hidden from users by setting feature flags on them. If a feature flag is set on an event type, users won't see the event type in the Event Catalog and the API, unless they have an authorization token that explicitly allows them. A feature flag is an arbitrary string that you can set at event type creation time. You can also remove or add back the feature flag by updating an existing event type. Feature flags follow the same naming rules as the event type name itself: `^[a-zA-Z0-9\\-_.]+$`. Here's how you can create an event type with a feature flag: ```js import { Svix } from "svix"; const svix = new Svix("AUTH_TOKEN"); const eventType = await svix.eventType.create({ name: "user.signup", description: "A user has signed up", featureFlags: ["beta-feature-a"], }); ``` ```python from svix.api import Svix, EventTypeIn svix = Svix("AUTH_TOKEN") app = svix.event_type.create(EventTypeIn( name="user.signup", description="A user has signed up", feature_flags=["beta-feature-a"] )) ``` ```rust use svix::api::{ApplicationIn, Svix, SvixOptions}; let svix = Svix::new("AUTH_TOKEN".to_string(), None); let event_type = svix .event_type() .create( EventTypeIn { name: "user.signup".to_string(), description: "A user has signed up".to_string(), feature_flags: Some(vec!["beta-feature-a".to_string()]), ..EventTypeIn::default() }, None, ) .await?; ``` ```go import ( svix "github.com/svix/svix-webhooks/go" ) svixClient := svix.New("AUTH_TOKEN", nil) app, err := svixClient.EventType.Create(ctx, &svix.EventTypeIn{ Name: "user.signup", Description: "A user has signed up", FeatureFlag: []string{"beta-feature-a"}, })} ``` ```java import com.svix.Svix; import com.svix.models.EventTypeIn; Svix svix = new Svix("AUTH_TOKEN"); svix.getEventType() .create(new EventTypeIn() .name("user.signup") .description("A user has signed up") .featureFlags(new String[]{"beta-feature-a"}) ); ``` ```kotlin import com.svix.kotlin.Svix; import com.svix.kotlin.models.EventTypeIn; val eventTypeOut = svix.eventType.create(EventTypeIn() .name("user.signup") .description("A user has signed up") .featureFlags(arrayOf("beta-feature-a")) ) ``` ```ruby svix = Svix::Client.new("AUTH_TOKEN") svix.event_type.create(Svix::EventTypeIn.new({ "name": "user.signup", "description": "A user has signed up", "feature_flags": ["beta-feature-a"] })) ``` ```csharp var svix = new SvixClient("AUTH_TOKEN"); await svix.EventType.CreateAsync(new EventTypeIn{ name: "user.signup", description: "A user has signed up", featureFlags: new string[] {"beta-feature-a"} }); ``` ```php $svix = new Svix('AUTH_TOKEN'); $eventType = $svix->eventType->create( EventTypeIn::create( name: 'user.signup', description: 'A user has signed up' ) ->withFeatureFlags(['beta-feature-a']) ); ``` ```shell export SVIX_AUTH_TOKEN='AUTH_TOKEN' svix event-type create '{ "name": "user.signup", "description": "A user has signed up", "featureFlags": ["beta-feature-a"] }' ``` ```shell export SVIX_AUTH_TOKEN='AUTH_TOKEN' curl -X POST "https://api.us.svix.com/api/v1/event-type/" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${SVIX_AUTH_TOKEN}" \ -d '{ "name": "user.signup", "description": "A user has signed up", "featureFlags": ["beta-feature-a"] }' ``` If a user tries to retrieve this newly created event type they will get a not-found error. To give them access you need to give them an access token that explicitly allows them to see event types with at least one of the feature flags set during creation. ```js const svix = new Svix("AUTH_TOKEN"); const dashboard = await svix.authentication.appPortalAccess("app_Xzx8bQeOB1D1XEYmAJaRGoj0", { featureFlags: ["beta-feature-a"] }); // A URL that automatically logs user into the dashboard console.log(dashboard.url); ``` ```python svix = Svix("AUTH_TOKEN") dashboard = svix.authentication.app_portal_access("app_Xzx8bQeOB1D1XEYmAJaRGoj0", AppPortalAccessIn( feature_flags=["beta-feature-a"] )) # A URL that automatically logs user into the dashboard print(dashboard.url) ``` ```rust let svix = Svix::new("AUTH_TOKEN".to_string(), None); let dashboard = svix .authentication() .app_portal_access( "app_Xzx8bQeOB1D1XEYmAJaRGoj0".to_string(), AppPortalAccessIn { feature_flags: Some(vec!["beta-feature-a".to_string()]), ..Default::default() }, None, ) .await?; // A URL that automatically logs user into the dashboard println!("{}", dashboard.url); ``` ```go svixClient := svix.New("AUTH_TOKEN", nil) dashboard, _ := svixClient.Authentication.AppPortalAccess(ctx, "app_Xzx8bQeOB1D1XEYmAJaRGoj0", &svix.AppPortalAccessIn{ FeatureFlags: []string{"beta-feature-a"} }) // A URL that automatically logs user into the dashboard fmt.Println(dashboard.Url) ``` ```java Svix svix = new Svix("AUTH_TOKEN"); AppPortalAccessOut dashboard = svix.getAuthentication().appPortalAccess("app_Xzx8bQeOB1D1XEYmAJaRGoj0", new AppPortalAccessIn() .featureFlags(new String[]{"beta-feature-a"}) ); // A URL that automatically logs user into the dashboard System.out.println(dashboard.getUrl()); ``` ```kotlin val svix = Svix("AUTH_TOKEN") val dashboard = svix.authentication.appPortalAccess("app_Xzx8bQeOB1D1XEYmAJaRGoj0", AppPortalAccessIn() .featureFlags(arrayOf("beta-feature-a")) ) // A URL that automatically logs user into the dashboard println(dashboard.url) ``` ```ruby svix = Svix::Client.new("AUTH_TOKEN") dashboard = svix.authentication.app_portal_access("app_Xzx8bQeOB1D1XEYmAJaRGoj0", Svix::AppPortalAccessIn.new({ "feature_flags": ["beta-feature-a"] })) # A URL that automatically logs user into the dashboard puts dashboard.url ``` ```csharp var svix = new SvixClient("AUTH_TOKEN"); var dashboard = await svix.Authentication.AppPortalAccessAsync("app_Xzx8bQeOB1D1XEYmAJaRGoj0", new AppPortalAccessIn{ featureFlags: new string[] {"beta-feature-a"} }); // A URL that automatically logs user into the dashboard Console.WriteLine(dashboard.Url) ``` ```php $svix = new Svix('AUTH_TOKEN'); $dashboard = $svix->authentication->appPortalAccess( 'app_Xzx8bQeOB1D1XEYmAJaRGoj0', AppPortalAccessIn::create() -> withFeatureFlags(['beta-feature-a']) ); echo $dashboard->url . PHP_EOL; ``` ```shell export SVIX_AUTH_TOKEN="AUTH_TOKEN" svix authentication app-portal app_Xzx8bQeOB1D1XEYmAJaRGoj0 '{ "featureFlags": ["beta-feature-a"] }' ``` ```shell curl -X POST "https://api.us.svix.com/api/v1/auth/app-portal-access/app_Xzx8bQeOB1D1XEYmAJaRGoj0/" \ -H "Accept: application/json" \ -H "Authorization: Bearer AUTH_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"featureFlags": ["beta-feature-a"]}' ``` A user with this newly minted dashboard access token will be able to see this new event type. Once you're ready to release this new event type to all of your users simply remove the feature flag from it. ```js const svix = new Svix("AUTH_TOKEN"); await svix.eventType.update("user.signup", { featureFlag: null }); ``` ```python svix = Svix("AUTH_TOKEN") svix.event_type.update("user.signup", EventTypeUpdate(feature_flag=None)) ``` ```rust let svix = Svix::new("AUTH_TOKEN".to_string(), None); let event_type_out = svix .event_type() .update( "user.signup".to_string(), EventTypeUpdate { feature_flag: None, ..Default::default() }, None, ) .await?; ``` ```go svixClient := svix.New("AUTH_TOKEN", nil) eventTypeOut, _ := svixClient.EventType.Update(ctx, "user.signup", &svix.EventTypeUpdate{ FeatureFlag: nil }) ``` ```java Svix svix = new Svix("AUTH_TOKEN"); EventTypeOut eventTypeOut = svix.getEventType().update("user.signup", new EventTypeUpdate() .featureFlag(null) ); ``` ```kotlin val svix = Svix("AUTH_TOKEN") val eventTypeOut = svix.eventType.update("user.signup", EventTypeUpdate().featureFlag(null)) ``` ```ruby svix = Svix::Client.new("AUTH_TOKEN") event_type_out = svix.event_type.update("user.signup", Svix::EventTypeUpdate.new({ "feature_flag": nil })) ``` ```csharp var svix = new SvixClient("AUTH_TOKEN"); var eventTypeOut = await svix.EventType.Update("user.signup", new EventTypeUpdate{ featureFlag: null }); ``` ```php $svix = new Svix('AUTH_TOKEN'); $eventTypeOut = $svix->eventType->update( 'user.signup', EventTypeUpdate::create(description: 'updated description') ->withFeatureFlag(null) ); ``` ```shell export SVIX_AUTH_TOKEN="AUTH_TOKEN" svix event-type update user.signup '{"featureFlag": null}' ``` ```shell curl -X POST "https://api.us.svix.com/api/v1/event-type/user.signup/" \ -H "Accept: application/json" \ -H "Authorization: Bearer AUTH_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"featureFlag": null}' ``` **Important** Keep in mind that endpoints with no event type filtering will receive all messages regardless of feature flags. Feature flags only impact event types' visibility in the app portal, catalog, and the API; not their deliverability. This means that endpoints with no event type filtering will receive all messages regardless of feature flags. Additionally, if a user is subscribed to a feature flagged event, they will continue to be able to see it in the app portal regardless of whether the feature flag is enabled for them or not. ## Appendix ### Generating an OpenAPI spec While most web frameworks support generating OpenAPI specs, not all of them support generating the new `webhooks` section (added in OpenAPI 3.1). If your framework of choice already support webhooks you are probably good to go, just make sure that it generates it correctly. Here's an example of how a `pet.new` event type should look like in your OpenAPI file: ```json "webhooks": { "pet.new": { "post": { "operationId": "pet.new", "description": "A new pet has been added", "x-svix-feature-flags": ["beta-feature-a"], // Optional field "x-svix-group-name": "group-name-1", // Optional field "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "tag": { "type": "integer" } } }, "example": { "name": "Buddy", "tag": 1234 } } } } } } } ``` If your framework doesn't support generating a `webhooks` section, or if your webhooks section is not generated correctly, you can resort to generating the section yourself using a simple post-processing script that adds the `webhooks` section to your spec. One alternative is to write the section manually and then have code to inject it into the OpenAPI spec. A better alternative is to use your web framework's auto-generation to do some of the heavy lifting for you. You can make your web framework generate the types for you by creating a dummy model that includes all of your submodels. Here is a Python example: ```python class PetNewEvent(pydantic.BaseModel): """A new pet has been added""" event_type = "pet.new" foo: str bar: str ... class WebhookTypes(BaseModel): """All of the webhook types that we support""" a1: PetNewEvent a2: PetChangedEvent a3: PetDeletedEvent ``` This will generate the schemas for all of your event types. Once you have those, you can write a script to generate the `webhooks` for you, or you can generate it manually. The result should look something like this: ```json "webhooks": { "pet.new": { "post": { "operationId": "pet.new", "description": "A new pet has been added", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PetNewEvent" }, "example": { "event_type": "pet.new", "foo": "Example 1", "bar": "Example 2", } } } } } } // ... } ``` --- title: Frequently Asked Questions (FAQ) --- # Frequently Asked Questions (FAQ) Here are some frequently asked questions about Svix. ### How long does it take to integrate with Svix? Most teams wire up our SDK and start sending webhooks in under a day. Many small companies ship in a few hours; while larger enterprise roll-outs that include design reviews and QA typically budget ~two weeks from first commit to production. ### I already have an event stream, how do I connect to Svix? Having an existing event stream makes integrating Svix even easier. All you need to do is either create a consumer that reads from the event stream and uses the Svix [`message.create`](https://api.svix.com/docs#tag/Message/operation/v1.message.create) API directly or connect [Svix Bridge](/sending-messages-with-bridge) to read event directly from the stream. ### Build vs. buy a webhooks service? We created Svix because we believe that when it comes to webhooks, buying is much preferable to building. Buying means that you don't have to spend your precious engineering time and effort onto maintaining the webhooks system. For a thorough comparison, please refer to the build vs. buy matrix. ### How do you ensure that customers only receive events pertaining to them and aren't affected by other customers? In Svix, each one of your customers will have its own [Consumer Application](/overview#consumer-applications) which is where you will be sending messages to. Each Consumer Application will only get messages sent to it, and Consumer Applications are completely isolated from other Consumer Applications on the database level. ### How do my customers subscribe to receive webhooks? Svix offers an [embeddable Consumer Application Portal](https://www.svix.com/application-portal/) that you can fully white-label to match your UI (or build your own with the API). From there your customers will be able to add endpoints, pick event types, view logs, and replay deliveries without ever needing a Svix account. ### We have time-sensitive and business critical events, how does your platform ensure reliability and message delivery guarantees? Svix queues every message durably, delivers with low latency, and retries on an exponential back-off schedule until it succeeds or exhausts the policy, giving you at-least-once delivery even under network hiccups or spikes. ### What is your outage recovery process? If your service or a customer endpoint is down, you (or the customer) can replay all failed messages for a chosen time window straight from the Portal, or resend individual webhooks, ensuring nothing is lost once both systems are back online. ### What support options are available and what are your SLAs? All tiers include email & community support. The Enterprise tier also offers 24/7 access to on-call engineers and custom support SLAs. For full information about the support and uptime SLAs options, please refer to the [Svix Pricing page](https://www.svix.com/pricing/). ### Can Svix handle our scale? We process billions of webhooks a year for our customers, and we power webhooks for top companies like Brex and Lob. We can most likely handle your scale, but please reach out if you have any specific questions or requirements. ### Will my customers know that we are using Svix? No. Your customers don't need to know you are using Svix. Though we always appreciate our customers spreading the word about Svix. :) ### What happens when a customer endpoint is down? Failed attempts trigger the automatic retry schedule; all tries (and responses) are logged for transparency. After the final attempt the message is marked failed but retained, so the customer (or you) can later replay it once the endpoint is healthy. ### What are webhooks? Webhooks are user-defined HTTP callbacks for server-to-server communication. You can think of them as a reverse-API or asynchronous API notifications. An API call is how a server can make requests from another service, and webhooks are how that service would asynchronously notify the server of events. ### What is webhooks as a service? Webhooks as a service refers to Svix's software as a service (SaaS) webhook platform. We've built a user interface for managing your users' webhook subscriptions as well as an API that simplifies the process of implementing webhook best practices at scale. ### Are Svix webhooks secure? Yes! Our webhook service was designed for security from the ground up. We follow industry best practices, encrypt all data both in transit and at rest, and are compliant with SOC 2 Type II, GDPR, CCPA and more. ### How to test and debug webhooks? There most common way for testing and debugging webhooks is by pushing test events from the service you are testing to either a receiver that can show you the events, or to a proxy tool that can forward requests to your development machine. Svix Play supports both! ### How can I test webhooks locally? Svix Play integrates seamlessly with the Svix CLI, allowing you to relay webhooks to your local development environment. This means you can test webhook integrations locally without exposing your development server to the internet. ### Can I test webhooks for free? Yes! You can use Svix Play to test your webhooks completely for free. You don't even need to signup to use any of it, including the web UI, and the forwarding CLI. --- title: Get Help & Chat With Us --- # Get Help & Chat With Us We love chatting with developers! If anything is unclear, you have some feedback, or you would just like to chat please get in touch! You can join the community on Slack or reach out via email at contact@svix.com. We are also happy to jump on a call, so just drop us a line. --- title: Idempotency --- # Idempotency Svix supports [idempotency](https://en.wikipedia.org/wiki/Idempotence) for safely retrying requests without accidentally performing the same operation twice. This is useful when an API call is disrupted in transit and you do not receive a response. This section is about making API calls to Svix. For information on helping your customers ensure they only get a message once, please refer to [receiving idempotency section (deduplication)](#receiving-idempotency-deduplication) below. To perform an idempotent request, pass the idempotency key in the `Idempotency-Key` header to the request. The idempotency key should be a unique value generated by the client. You can create the key in however way you like, though we suggest using UUID v4, or any other string with enough entropy to avoid collisions. ```js const svix = new Svix("AUTH_TOKEN"); const message = { eventType: "invoice.paid", eventId: "evt_Wqb1k73rXprtTm7Qdlr38G", payload: { type: "invoice.paid", id: "invoice_WF7WtCLFFtd8ubcTgboSFNql", status: "paid", attempt: 2, }, }; await svix.message.create("app_Xzx8bQeOB1D1XEYmAJaRGoj0", message, { idempotencyKey: "fd56a56b-838d-4456-8b83-390802672895", }); ``` ```python svix = Svix("AUTH_TOKEN") message = MessageIn( event_type="invoice.paid", event_id="evt_Wqb1k73rXprtTm7Qdlr38G", payload={ "type": "invoice.paid", "id": "invoice_WF7WtCLFFtd8ubcTgboSFNql", "status": "paid", "attempt": 2 } ) svix.message.create( "app_Xzx8bQeOB1D1XEYmAJaRGoj0", message, PostOptions(idempotency_key="fd56a56b-838d-4456-8b83-390802672895") ) ``` ```rust let svix = Svix::new("AUTH_TOKEN".to_owned(), None); svix.message() .create( "app_Xzx8bQeOB1D1XEYmAJaRGoj0".to_owned(), MessageIn { event_type: "invoice.paid".to_owned(), event_id: Some("evt_Wqb1k73rXprtTm7Qdlr38G".to_owned()), payload: json!({ "type": "invoice.paid", "id": "invoice_WF7WtCLFFtd8ubcTgboSFNql", "status": "paid", "attempt": 2 }), ..MessageIn::default() }, Some(PostOptions { idempotency_key: Some("fd56a56b-838d-4456-8b83-390802672895".to_owned()), }), ) .await?; ``` ```go svixClient := svix.New("AUTH_TOKEN", nil) eventId := "evt_Wqb1k73rXprtTm7Qdlr38G" message := svix.MessageIn{ EventType: "invoice.paid", EventId: *svix.NullableString(&eventId), Payload: map[string]interface{}{ "type": "invoice.paid", "id": "invoice_WF7WtCLFFtd8ubcTgboSFNql", "status": "paid", "attempt": 2, }, } idempotencyKey := "fd56a56b-838d-4456-8b83-390802672895" svixClient.Message.CreateWithOptions(ctx, "app_Xzx8bQeOB1D1XEYmAJaRGoj0", &message, &svix.PostOptions{ IdempotencyKey: &idempotencyKey, }) ``` ```java Svix svix = new Svix("AUTH_TOKEN"); MessageIn message = new MessageIn() .eventType("invoice.paid") .eventId("evt_Wqb1k73rXprtTm7Qdlr38G") .payload("{" + "\"type\": \"invoice.paid\"," + "\"id\": \"invoice_WF7WtCLFFtd8ubcTgboSFNql\"," + "\"status\": \"paid\"," + "\"attempt\": 2" + "}"); PostOptions opts = new PostOptions() .idempotencyKey("fd56a56b-838d-4456-8b83-390802672895"); svix.getMessage() .create("app_Xzx8bQeOB1D1XEYmAJaRGoj0", message, opts); ``` ```kotlin val svix = Svix("AUTH_TOKEN") svix.message.create("app_Xzx8bQeOB1D1XEYmAJaRGoj0", MessageIn( eventType = "invoice.paid", payload = mapOf( "type" to "invoice.paid", "id" to "invoice_WF7WtCLFFtd8ubcTgboSFNql", "status" to "paid", "attempt" to 2 ), eventId = "evt_Wqb1k73rXprtTm7Qdlr38G"), PostOptions( idempotencyKey = "fd56a56b-838d-4456-8b83-390802672895")) ``` ```ruby svix = Svix::Client.new("AUTH_TOKEN") svix.message.create( "app_Xzx8bQeOB1D1XEYmAJaRGoj0", Svix::MessageIn.new({ "event_type" => "invoice.paid", "payload" => { "type": "invoice.paid", "id" => "invoice_WF7WtCLFFtd8ubcTgboSFNql", "status" => "paid", "attempt" => 2 }, "event_id" => "evt_Wqb1k73rXprtTm7Qdlr38G"}), { "idempotency_key" => "fd56a56b-838d-4456-8b83-390802672895" }) ``` ```csharp var svix = new SvixClient("AUTH_TOKEN", new SvixOptions("https://api.us.svix.com")); var message = new MessageIn( eventType: "invoice.paid", payload: new { type = "invoice.paid", id = "invoice_WF7WtCLFFtd8ubcTgboSFNql", status = "paid", attempt = 2 }, eventId: "evt_Wqb1k73rXprtTm7Qdlr38G" ); await svix.Message.CreateAsync( "app_Xzx8bQeOB1D1XEYmAJaRGoj0", message, null, "fd56a56b-838d-4456-8b83-390802672895" ); ``` ```php $svix = new Svix('AUTH_TOKEN'); $message = MessageIn::create( eventType: 'invoice.paid', payload: [ 'type' => 'invoice.paid', 'id' => 'invoice_WF7WtCLFFtd8ubcTgboSFNql', 'status' => 'paid', 'attempt' => 2 ] )->withEventId('evt_Wqb1k73rXprtTm7Qdlr38G'); $svix->message->create( 'app_Xzx8bQeOB1D1XEYmAJaRGoj0', $message, new MessageCreateOptions(idempotencyKey: 'fd56a56b-838d-4456-8b83-390802672895') ); ``` ``` Idempotency is not yet supported in the Svix CLI. ``` ```shell curl "https://api.us.svix.com/api/v1/app/app_Xzx8bQeOB1D1XEYmAJaRGoj0/msg/" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer AUTH_TOKEN" \ -H "Idempotency-Key: fd56a56b-838d-4456-8b83-390802672895" -d '{ "eventType": "invoice.paid", "eventId": "evt_Wqb1k73rXprtTm7Qdlr38G", "payload": { "type": "event.type", "id": "invoice_WF7WtCLFFtd8ubcTgboSFNql", "status": "paid", "attempt": 2 } }' ``` Svix's idempotency works by saving the resulting status code and body of the first request made for any given idempotency key for any successful request. Subsequent requests using the same auth token and the same idempotency key will return the same result for a period of up to 12 hours. Please note that idempotency is only supported for `POST` requests. ## Receiving idempotency (deduplication) Svix offers "at least once" delivery semantics. This means that if there are issues during delivery (e.g. networking issues) a message can sometimes be processed twice by the webhook receiver. In many cases this is not a concern, and recipients can just process the duplicated requests. Though when "exactly once" semantics are required, webhook consumers can use Svix's webhook deduplication support to ensure that. Svix includes a `webhook-id` header with every webhook request. That ID is unique per message but is reused across retries of the same message. Consumers can then use this identifier to ensure that they only process each event once, by for example storing the ID in redis with a 24hr expiry, and checking whether they already processed the message's `webhook-id` before processing it. --- title: Operational Webhooks --- # Operational Webhooks In addition to enabling you to send webhooks to your customers, Svix also sends you webhooks about the events in your Svix environment. We call these: "Operational webhooks". These webhooks let you build powerful workflows and automations over events in your account. For example, a common one is to monitor for [`endpoint.disabled`](https://api.svix.com/docs#tag/Webhook/operation/endpoint.disabled), which triggers when an endpoint has been automatically disabled after multiple days of failing, and notify your customer automatically via email when that happens. To subscribe to operational webhooks, please head to the [Operational Webhooks](https://dashboard.svix.com/webhooks) section of the dashboard. For more information on the exact events, their schemas, and other related documentation please refer to the [the webhooks section](https://api.svix.com/docs#tag/Webhook) of the API reference documentation. --- title: Receiving Webhooks with Ingest --- # Receiving Webhooks with Ingest Receiving webhooks with [Svix Ingest] starts by creating a `Source`. A `Source` generates an endpoint you can share with a webhook provider as a destination for their webhooks. Ingest supports signature verification schemes and flows used by a variety of webhook providers, including `adobeSign`, `airwallex`, `clerk`, `docusign`, `github`, `hubspot`, `open-ai`, `resend`, `shopify`, `slack`, `stripe`, `zoom`, and many others. Check out [the product][Svix Ingest] to see the full list. Additionally there's the option to configure a `Source` as `genericWebhook` to skip performing signature verification. This is useful for providers that have no verification scheme and for providers whose verification scheme is not yet supported by Ingest. Don't see your provider? Let us know and we'll add it! ## Create a Source Creating a `Source` from the [Ingest Dashboard]: ![screenshot of the Ingest Dashboard showing the overview tab for a Source named "demo"](/img/ingest/source-edit.png) ```javascript const svix = new Svix("AUTH_TOKEN"); const ingestSourceOut = await svix.ingest.source.create({ name: "myGithubWebhook", uid: "unique-identifier", type: "github", config: { secret: "SECRET" }, }); ``` ```python svix = Svix("AUTH_TOKEN") ingest_source_out = svix.ingest.source.create(IngestSourceIn( name="myGithubWebhook", uid="unique-identifier", type="github", config=GithubConfig( secret="SECRET" ), )) ``` ```rust let svix = Svix::new("AUTH_TOKEN".to_string(), None); let ingest_source_out = svix.ingest().source().create( IngestSourceIn { name: "myGithubWebhook".to_owned(), uid: Some("unique-identifier".to_owned()), config: IngestSourceInConfig::Github(GithubConfig { secret: "SECRET".to_owned(), ..Default::default() }), ..Default::default() }, None, ).await?; ``` ```go svixClient := svix.New("AUTH_TOKEN", nil) ingestSourceOut, err := svixClient.Ingest.Source.Create( ctx, &IngestSourceIn{ Name: "myGithubWebhook", Uid: "unique-identifier", Type: IngestSourceInTypeGithub, Config: GithubConfig{ Secret: "SECRET", }, }, ) ``` ```java Svix svix = new Svix("AUTH_TOKEN"); IngestSourceOut ingestSourceOut = svix .getIngest() .getSource() .create(new IngestSourceIn() .name("myGithubWebhook") .uid("unique-identifier") .config(new IngestSourceInConfig.Github(new GithubConfig() .secret("SECRET") )) ); ``` ```kotlin val svix = Svix("AUTH_TOKEN") val ingestSourceOut = svix.ingest.source.create(IngestSourceIn( name = "myGithubWebhook", uid = "unique-identifier", config = IngestSourceInConfig.Github(GithubConfig( secret = "SECRET" )), )) ``` ```ruby svix = Svix::Client.new("AUTH_TOKEN") ingest_source_out = svix.ingest.source.create(Svix::IngestSourceIn.new({ "name": "myGithubWebhook", "uid": "unique-identifier", "config": Svix::IngestSourceInConfig::Github.new({ "secret": "SECRET" }) })) ``` ```csharp var svix = new SvixClient("AUTH_TOKEN", new SvixOptions("https://api.us.svix.com")); var ingestSourceOut = await svix.Ingest.Source.CreateAsync( new IngestSourceIn{ Name = "myGithubWebhook", Uid = "unique-identifier", Config = IngestSourceInConfig.Github(new GithubConfig { Secret = "SECRET", }), } ); ``` ```shell svix ingest source create '{ "name": "myGithubWebhook", "uid": "unique-identifier", "type": "github", "config": { "secret": "SECRET" } }' ``` ```shell curl -X 'POST' \ 'https://api.eu.svix.com/ingest/api/v1/source' \ -H 'Authorization: Bearer AUTH_TOKEN' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "name": "myGithubWebhook", "uid": "unique-identifier", "type": "github", "config": { "secret": "SECRET" } }' ``` ## Tell your provider where to send webhooks The Ingest URL is also listed on the [Ingest Dashboard] for each `Source`. The `IngestSourceOut` response from the API will include an `ingestUrl` which is the endpoint you give to your provider, telling them where to send their webhooks. Given an ingest URL of the format `https://api.svix.com/ingest/api/v1/source/src_abcdefghijklmnop/in/token1234567`, providers can actually send requests to any trailing path, and with any query parameter; e.g., `https://api.svix.com/ingest/api/v1/source/src_abcdefghijklmnop/in/token1234567/foo/bar/baz?qux=duck`. Trailing path segments will be forwarded along to the eventual destination under the `svix-ingest-trailing-path-segments` header; query parameters will be forwarded under the `svix-ingest-query` header. Both are and are also available as [transformations](./transformations.mdx) parameters. For GitHub, as is used in this example, this is referred to as the _"Payload URL"_. ## Managing incoming messages In the [Ingest Dashboard], the Destinations tab for your `Source` is where you can configure endpoints, view logs, inspect message payloads, etc. ![screenshot of the Ingest Dashboard showing the Destination/Endpoints tab for a Source named "demo"](/img/ingest/destination-endpoints.png) Configuring endpoints allows you to forward messages received by Ingest over to endpoints of your choosing. ![screenshot of the Ingest Dashboard showing the Destination/Logs tab for a Source named "demo"](/img/ingest/destination-logs.png) Logs and statistics are available to help monitor for problems, replay or recover messages. ## Managing Source Tokens The last portion of the Ingest URL is a `Token` which can be invalidated and rotated: ![screenshot of the Ingest Dashboard showing the overview tab for a Source named "demo"](/img/ingest/url-token-rotate.png) `Token`s that are rotated stay usable for 24 hours. During this time both the old and new `Token`s are honored by Ingest. During this period it's important that you reconfigure your provider with the new Ingest URL in order to have a seamless transition. ## Verifying Webhooks Verifying webhooks forwarded by Svix Ingest works the same way as verifying webhooks sent by Svix Dispatch. Check [How to Verify Webhooks with the Svix Libraries][verify-libs] and the following pages for ways to ensure only legitimate requests get processed at your destination endpoint(s). [Svix Ingest]: https://svix.com/ingest [Ingest Dashboard]: https://dashboard.svix.com/ingest [verify-libs]: https://docs.svix.com/receiving/verifying-payloads/how --- title: Source Errors --- # Source Errors In the Source Errors tab, you can see the log of all the incoming webhooks that were rejected by the Source. The most likely reason for a source error is an invalid signature, or an invalid payload format in the incoming message. Every time your Ingest URL returns a non-200 status code, the reason for the failure will be logged here, and the incoming message will not be delivered to your endpoints. ![Source Errors tab](/img/ingest/source-errors.png) In the error details, you can inspect the message payload, the HTTP method and headers, and the reason for the failure. ![Source Error details](/img/ingest/source-errors-details.png) --- title: Transformations --- # Transformations Ingest Transformations are a powerful Svix feature that allows the modification of certain webhook properties in-flight. With transformations, you can write JavaScript code to change a webhook's HTTP method, target URL, and body payload before it's sent to your endpoint. ## Using Transformations Ingest Transformations are available in all plans. To use them, after creating an endpoint in a Source, go to the 'Advanced' tab and scroll down to the 'Transformations' card: ![Ingest Transformations card](/img/ingest/ingest-transformation.png) An endpoint's Transformation can be enabled or disabled at any time by toggling the switch on this card. You can write Javascript code to edit an endpoint's Transformation, and test your code against a sample incoming payload to see the result. ![Editing an endpoint's Transformation](/img/ingest/edit-transformation.png) ### How to write a Transformation Svix expects a Transformation to declare a function named `handler`. Svix will pass an object with the following properties to the function: - `method`, a string representing the HTTP method the webhook will be sent with. It is always `"POST"` by default, and its only valid values are `"POST"`, `"PUT"`, or `"PATCH"`. - `url`, a string representing the destination endpoint's URL. It can be changed to any valid URL. - `payload`, which contains the webhook's payload as a JSON object. It can be changed as needed. - `eventType`, a string representing the event type. Changes to it are ignored. This will always have the value `svix.in` for ingest events - `transformationsParams`, additional parameters made available in ingest; see below for details The Transformation must return the same object, but may modify its properties as described above. In addition to the ones listed above, it can also set the following properties on the returned object: - `cancel`, a boolean which controls whether or not to cancel the dispatch of a webhook. This value defaults to `false`. - `headers`, an object with keys being HTTP header names and values being the associated header values. Headers set here take precedence over endpoint headers. ### An example Transformation Suppose that sometimes, you want to redirect webhooks to a custom URL instead of the endpoint's defined URL. And you only want to do this redirect if a custom URL is present in the webhook payload. You can write a transformation like this: ```js function handler(webhook) { if (webhook.payload.customUrl) { webhook.url = webhook.payload.customUrl; } return webhook; } ``` Great, the webhook is redirected to the custom URL if the `customUrl` property exists on the payload. Otherwise, it is sent to the endpoint's defined URL. ### Ingest-Specific Transformations Params Transformations called from an Ingest request will receive additional parameters under the `transformationsParams` property. Specifically: - `transformationsParams.headers` will be a string/string key-value map containing the headers of the input request. If a header is duplicated on the input request, the values will be concatenated with ", ", as per [RFC 9110 § 5.2](https://www.rfc-editor.org/info/rfc9110/#section-5.2). - `transformationsParams.headers.svix-ingest-query` contains the un-parsed query string (the part after the `?` on the request URL) - `transformationsParams.headers.svix-ingest-trailing-path-segments` contains any parts of the URL after the token, not including the first `/`. If your source URL is `https://api.svix.com/ingest/api/v1/source/src_abcdefghijklmnop/in/token1234567` and your provider makes a request to `https://api.svix.com/ingest/api/v1/source/src_abcdefghijklmnop/in/token1234567/foo/bar/baz?qux=duck`, `transformationsParams.headers["svix-ingest-query"]` will contain the string `qux=duck` and `transformationsParams.headers["svix-ingest-trailing-path-segments"]` will contain the string `foo/bar/baz`. --- title: Advanced Zapier Integrations --- # Advanced Zapier Integrations [Zapier](https://zapier.com/) is an online automation tool that connects apps and services. You can build a Zapier integration that allows your customers to connect your service to other services with Svix. The Svix auto-generated Zapier integration is a Node/JavaScript project meaning it's defined as code and very easy to customize. This doc gives guidance on ways to enhance your Zapier webhook integration powered by Svix. **Prerequisites** Before reading this doc, we strongly recommend reading the [Build a Zapier Integration](./zapier) docs. This doc assumes you're familiar with the basics of Svix Integrations and have a functional API key-based Zapier webhook integration. ## Upgrade an existing integration If you add or change your event types, you may want to upgrade your Zapier integration to a new auto-generated integration. ### Upgrading to an updated auto-generated package Follow the [Download the package](./zapier#download-the-package) section of the [Build a Zapier Integration](./zapier) guide to download an updated auto-generated package. Then follow the steps below to link the new package to the existing integration and deploy. #### Build and deploy After downloading and extracting the auto-generated integration package, download the package dependencies: ``` npm install ``` Then, link the new integration package to your existing Zapier integration using the Zapier CLI [link command](https://platform.zapier.com/cli_docs/cli#link): ``` zapier link ``` Finally, build & deploy the integration to Zapier through the [push command](https://platform.zapier.com/cli_docs/cli#push): ``` zapier push ``` ### Managing integration versions If you wish to version bump your integration, be sure to update your integration's `package.json` (shown below) to reflect the new version number. It defaults to `1.0.0`. ```json { "name": "zapier-webhook-integration-with-svix", "version": "1.0.0", // <------ CHANGE ME "main": "index.js", // << truncated >> } ``` Zapier supports complex versioning use cases including migrating users during a version bump. Review the [Zapier Platform](https://platform.zapier.com/cli_docs/docs#deploying-an-app-version) docs for more information. ### Upgrading a customized integration If you have customized your integration package, you will need to manually reconcile the diff. We strongly recommend using version control like git to track the changes and aid in merging. ## Alternative authentication schemes By default, the auto-generated Zapier integration uses [custom (API key)](https://platform.zapier.com/cli_docs/docs#custom) authentication type and requires users to explicitly provide an application ID and integration key. The default scheme might be undesirable because it requires the user to copy-paste credentials and doesn't connect to your existing auth flows. To provide a better user experience that hides the Svix constructs, we recommend using [OAuth2](https://platform.zapier.com/cli_docs/docs#oauth2) or [Session](https://platform.zapier.com/cli_docs/docs#session) authentication. See the [Zapier Platform authentication](https://platform.zapier.com/cli_docs/docs#authentication) docs for more information. - With OAuth2, Zapier will redirect your user to your site where you can authenticate them and send Zapier back an access token. An demonstration of this flow's user experience is shown in the video below. - With Session Auth, Zapier will show a login form for your user to provide their username and password. Those are securely sent to your service where you'll send Zapier back a session token. With either authentication flow, you can make another request to your APIs to exchange the access/session token for the Svix application ID and integration key. An example of that API endpoint for a Flask-based service might look like the following: ```py svix = Svix("AUTH_TOKEN") @app.route('/webhook/integration/') @auth_required def get_svix_integration_key(): app_id = ... # get your user's app id integration_out = svix.integration.create(app_id, IntegrationIn( name="Zapier Integration" )) ... # store your user's zapier integration ID key_out = svix.integration.get_key(app_id, integration_out.id) return jsonify({ "application_id": app_id, "integration_key": key_out.key }) ``` ### OAuth2 Flow User Experience
## Custom triggers ### Multiple event types per trigger If you wish to configure triggers with multiple event types, you can modify the `filterTypes` field on the create endpoint operation during the trigger's subscribe hook. If you wish your users to specify a set of event types when creating the trigger, you can use Zapier's [input fields](https://platform.zapier.com/cli_docs/docs#input-fields) feature. ### Additional trigger fields By default, the auto-generated integration contains the example payload that you provided with the event type schema. Zapier supports explicitly defining the list of output fields for the trigger. If you have optional fields or multiple event types with different schemas, we recommend explicitly defining the [output fields](https://platform.zapier.com/cli_docs/docs#output-fields) on the trigger. ### Adding custom actions or triggers The auto-generated Zapier integration can be extended with additional actions, triggers, creates, or other Zapier resources. More information on this is available on the [Zapier CLI platform docs](https://platform.zapier.com/cli_docs/docs). The Zapier CLI can modify your package (e.g. creating a new action from a template). More information on that is available on the [Zapier CLI Reference](https://platform.zapier.com/cli_docs/cli). --- title: Using the ngrok Integration --- # Using the ngrok Integration [ngrok](https://ngrok.com) is a staple tool for many developers that creates tunnels between networks. It is often used to expose a port on localhost to the public Internet, but with ngrok Cloud Edge it can be also be used to secure traffic from the Internet to production cloud environments. In this tutorial, we will learn how to verify Svix webhook requests using ngrok both for local development and on ngrok Cloud Edge. This tutorial assumes you are already familiar with the [Svix webhooks service](https://www.svix.com) and [ngrok](https://ngrok.com). If this is your first time using Svix, we recommend you first check out our [quickstart documentation](https://docs.svix.com/overview). We also recommend checking out ngrok's documentation on [Webhook Verification](https://ngrok.com/docs/http/webhook-verification/). ## Verify Svix webhooks locally with ngrok CLI ### Install ngrok If you haven't already, [install the ngrok CLI](https://ngrok.com/download). ### Create a tunnel First, create an endpoint in the Consumer App Portal, and copy the endpoint's Signing Signature. Then, run the following ngrok command on your computer's terminal, replacing SIGNATURE with the Signing Signature you just copied: ```sh ngrok http 3000 --verify-webhook=svix --verify-webhook-secret=SIGNATURE ``` When you run the command, ngrok should generate URL that looks like `https://d7f4c8296c55.ngrok.io`. Copy that URL, and set it as the URL for the Svix endpoint you previously created in the Consumer App Portal. Assuming you are running a service on port 3000, all Svix webhooks to your endpoint will now be forwarded to that local service by ngrok. And because you configured the `--verify-webhook` and `--verify-webhook-secret` options, ngrok will only forward verified Svix webhooks. ## Verify Svix Webhooks on ngrok Cloud Edge ### Create a Svix endpoint Create an endpoint in the consumer application portal. You'll need the newly-created endpoint's Signing Signature later on. ### Sign up for ngrok Create an account on [ngrok.com](https://ngrok.com). ### Create an ngrok Edge Login to the [ngrok dashboard](https://dashboard.ngrok.com). Using the menu on the left, expand "Cloud Edge" and choose "Edges." Create an edge by clicking the "New edge" button and choose "HTTPS Edge". ### Configure ngrok Edge On the Edge configuration page, find and click the "Webhooks Verification" menu item, and click "Begin Setup." Choose Svix as your webhook provider: ![Selecting Svix as the Webhook Provider](/img/ngrok-webhook-provider.png) And paste your endpoint's Signing Signature from Svix as the webhook signing key: ![Entering your Svix endpoint's Signing Key](/img/ngrok-signing-key.png) --- title: Building a Zapier Integration --- # Building a Zapier Integration [Zapier](https://zapier.com/) is an online automation tool that connects apps and services. Svix can automatically build a Zapier integration for you directly from your Svix account. This lets your customers easily build Zapier workflows on top of your service. It generates a trigger for each event type and a special trigger that subscribes to all event types. The integration package also comes with secure webhook verification enabled by default. This guide will walk you through the steps to set up a Zapier integration for your users. You can see the end user experience (using the default API key auth) in the video below.
## Prerequisites ### Set up event type schemas Before setting up a Zapier integration, we strongly recommend configuring event types with schemas and examples as those will be embedded in the auto-generated integration package. Follow the [Your first event type schema](/tutorials/event-type-schema) tutorial and refer to the [Event Types](/tutorials/event-type-schema) docs to set this up. ### Set up Zapier CLI **Information** Zapier integrations can be defined on the Zapier UI or via the Zapier CLI (as a Node/JavaScript package). This guide is based on the CLI which allows for more powerful customizations. We recommend reading through the [How to Use REST Hooks in Zapier CLI](https://platform.zapier.com/cli_tutorials/resthooks) tutorial provided by Zapier to get an understanding of the structure. The Zapier integration package requires [Node.js v14](https://nodejs.org/) as that is the version used by the Zapier platform at runtime. Install the Zapier CLI globally: ``` npm install -g zapier-platform-cli ``` Make a [Zapier Platform](https://developer.zapier.com/) account and login: ``` # Login with username and password zapier login # Login with SSO zapier login --sso ``` ## Create an integration ### Download the package You can download an auto-generated integration package from the [integrations page](https://dashboard.svix.com/integrations) on the dashboard shown below. ![zapier-integration-generator](/img/zapier-integration-generator.png) ### Generate integration keys To integrate Svix with Zapier REST Hooks, the Zapier integration needs access to your user's application to add a Zapier endpoint. Svix provides an Integrations API to generate long-lived rotatable credentials called integration keys for this purpose. On your service, return the Svix application ID and integration secret to your user. See the [Integration section](http://api.svix.com/docs#tag/Integration) of the API docs for more information. Code example including application creation: ```js import { Svix } from "svix"; const svix = new Svix("AUTH_TOKEN"); const app = await svix.application.create({ name: "Test Application" }); const integ = await svix.integration.create(app.id, { name: "Zapier Integration" }); const integKey = await svix.integration.getKey(app.id, integ.id); ``` ```python from svix.api import Svix, ApplicationIn, IntegrationIn svix = Svix("AUTH_TOKEN") app_id = svix.application.create(ApplicationIn(name="Test Application")).id integ_id = svix.integration.create(app_id, IntegrationIn(name="Zapier Integration")).id integ_key = svix.integration.get_key(app_id, integ_id).key ``` ```rust // TBD ``` ```go import ( svix "github.com/svix/svix-webhooks/go" ) svixClient := svix.New("AUTH_TOKEN", nil) appOut, err := svixClient.Application.Create(ctx, &svix.ApplicationIn{ Name: "Test Application", })} integOut, err := svixClient.Integration.Create(ctx, appOut.Id, &svix.IntegrationIn{ Name: "Zapier Integration", }) integKeyOut, err := svixClient.Integration.GetKey(ctx, appOut.Id, integOut.Id) ``` ```java import com.svix.Svix; import com.svix.models.ApplicationIn; import com.svix.models.IntegrationIn; Svix svix = new Svix("AUTH_TOKEN"); String appId = svix.getApplication().create(new ApplicationIn().name("Test Application")).id; String integId = svix.getIntegration().create(appId, new IntegrationIn().name("Zapier Integration")).id; String integKey = svix.getIntegration().getKey(appId, integId).key; ``` ```kotlin import com.svix.kotlin.Svix; import com.svix.kotlin.models.ApplicationIn; import com.svix.kotlin.models.IntegrationIn; val svix = Svix("AUTH_TOKEN"); val appId = svix.application.create(ApplicationIn().name("Test Application")).id val integId = svix.integration.create(appId, IntegrationIn().name("Zapier Integration")).id val integKey = svix.integration.getKey(appId, integId).key ``` ```csharp // TBD ``` ```php $svix = new Svix('AUTH_TOKEN'); $app = $svix->application->create(ApplicationIn::create( name: 'Test Application' )); $integ = $svix->integration->create($app->id, IntegrationIn::create( name: 'Zapier Integration' )); $integKey = $svix->integration->getKey($app->id, $integ->id); ``` ```shell svix application create '{"name": "Test Application"}' svix integration create 'app_24D5XFE8W4VVYQ2XPdJlFKlYKrf' '{"name": "Zapier Integration"}' svix integration get-key 'app_24D5XFE8W4VVYQ2XPdJlFKlYKrf' 'integ_24D5mQaRudh54XvMNMmgUroNfRA' ``` ```shell curl -X 'POST' \ 'https://api.us.svix.com/api/v1/app/' \ -H 'Authorization: Bearer AUTH_TOKEN' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "name": "Test Application", }' curl -X 'POST' \ 'https://api.us.svix.com/api/v1/app/APP_ID/integration/' \ -H 'Authorization: Bearer AUTH_TOKEN' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "name": "Zapier Integration" }' curl -X 'GET' \ 'https://api.us.svix.com/api/v1/app/APP_ID/integration/INTEG_ID/key/' \ -H 'Authorization: Bearer AUTH_TOKEN' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' ``` ### Build and deploy After downloading and extracting the auto-generated integration package, download the package dependencies: ``` npm install ``` Then, create a new Zapier integration using the Zapier CLI [register command](https://platform.zapier.com/cli_docs/cli#register): ``` zapier register ``` Finally, build & deploy the integration to Zapier through the [push command](https://platform.zapier.com/cli_docs/cli#push): ``` zapier push ``` ### That's it! You're done! You have a working Zapier integration for your Svix-powered webhooks. :) Try it out! The next section walks through the experience of creating a Zap with a webhook trigger. Then read on to see how you can customize your integration and release it publicly on the Zapier website. ![congratulations](/img/congratulations.png) ## Try it out You can create a Zap with a Svix-powered webhook trigger from the account where you deployed the integration. You try it out from another account by obtaining the share link on the [Zapier Platform console](https://developer.zapier.com/). ### Set up the trigger 1. Start by going to the [Zap editor](https://zapier.com/app/editor/) and give your Zap a name. 2. Select the integration (aka "app"). You can search for it by name (the same name you set when calling `zapier register`). 3. Select a trigger event. This corresponds to an event type (or the all trigger for all event types) which the Zap will be triggered on. ![zap-creation-step-1](/img/zap-creation-step-1.png) ### Authenticate #### Retrieve the integration key for your application Your users should use the mechanism you provide to generate integration keys, but for testing you can use the Svix CLI to obtain an application ID and integration key. ```bash $ svix application create '{"name": "Test Application"}' { "name": "Test Application", "id": "app_24D5XFE8W4VVYQ2XPdJlFKlYKrf", # <------------------- Application ID "createdAt": "2022-01-25T23:12:34.055654Z", "updatedAt": "2022-01-25T23:12:34.055694Z" } $ svix integration create 'app_24D5XFE8W4VVYQ2XPdJlFKlYKrf' '{"name": "Zapier Integration"}' { "name": "Zapier Integration", "id": "integ_24D5mQaRudh54XvMNMmgUroNfRA", "createdAt": "2022-01-25T23:14:34.876543+00:00", "updatedAt": "2022-01-25T23:14:35.019512+00:00" } $ svix integration get-key 'app_24D5XFE8W4VVYQ2XPdJlFKlYKrf' 'integ_24D5mQaRudh54XvMNMmgUroNfRA' { "key": "testintegsk_zrHyRLxf7qMed4sv2onkoearKUgHWkK9" # <--- Integration Key } ``` #### Create Account Zapier will prompt you to sign in to an account for the integration. When you create a new account, enter the application ID and integration key from the previous step. ![zap-creation-step-2](/img/zap-creation-step-2.png) **User Authentication Experience** By default, the auto-generated Zapier integration uses token-based auth where your users input the application ID and an integration key. You can set up OAuth or Session-based authentication schemes to mask these Svix constructs by following the guide in the [Advanced Zapier Integrations](./advanced-zapier#alternative-authentication-schemes) doc. ### Test the trigger Finally, test your trigger by sending a test message. Zapier will prompt you to test your integration. Upon attempting it, Zapier will register a new endpoint on the application (which will also show in the App Portal). ![zap-creation-step-3](/img/zap-creation-step-3.png) In the App Portal, send a test event for the corresponding event type to the trigger's endpoint. ![zap-creation-step-3.1](/img/zap-creation-step-3.1.png) In Zapier, re-run the trigger test. Once successful, Zapier will automatically populate the fields available based on the example's message. If you skipped sending the test message, the fields provided in the event schema example will be shown. ![zap-creation-step-4](/img/zap-creation-step-4.png) ### Set up an action You can now set up an action that uses any of these available fields. ![zap-creation-step-5](/img/zap-creation-step-5.png) ## Next Steps ### Release it live You can see your integration on the [Zapier Platform](https://developer.zapier.com/). From there you can edit the icon and title, get the sharable link for your users, and view analytics. If you wish to make the integration generally available on the Zapier website, you'll need to submit it for review to Zapier. More information on this process can be found on the Zapier [lifecycle planning docs](https://platform.zapier.com/partners/lifecycle-planning). Zapier also provides [integration review guidelines](https://platform.zapier.com/partners/integration-review-guidelines) that describe the requirements for release. ### Advanced Customizations You can configure additional triggers, set up authentication schemes like OAuth2 or Session-based, and much more within the Zapier platform. We recommend reading our [Advanced Zapier Integrations](./advanced-zapier) docs page and going through the extensive [Zapier CLI Platform](https://platform.zapier.com/cli_docs/docs) docs. --- title: Introduction slug: / --- # Introduction Svix makes sending webhooks easy and reliable by offering [webhook sending as a service](https://www.svix.com). With Svix you can start sending webhooks in minutes, while ensuring robust deliverability, and a great developer experience for your users. ## Webhooks are harder than they seem. Webhooks require a lot more engineering time, resources, and ongoing maintenance than you would first expect. When building your own webhooks you have to deal with a lot of challenges, such as: unreliable user endpoints, which fail or hang more often than you think; monitoring and reliability of your webhook system; security implications which are unique to webhooks and much more. This is where we come in. With Svix you can start sending webhooks in under five minutes, and we take care of all of the above and more. --- title: Message Tags --- # Message Tags Message tags are free-form strings that can be added to messages to use them for filtering. In some cases, while debugging webhooks, you may want to get a specific message with a certain field or attribute in the payload, beyond it's event type or channel. ## How to use it To use message tags, simply add the `tags` field to the create message call. For example, if you want to give your users the option to filter messages related to a specific user id, you can add the user id as a tag. ```typescript await svix.message.create('app_id', { eventType: "user.signup", tags: ["user_1"], payload: { "user_id": "user_1", "email": "test@example.com" }, }); ``` Usually, tags contain values that are already present in the payload, such as user ids, order ids, product categories, etc. Tags will be visible in the Application Portal, and your users will be able to use them to filter messages. ![Message Tags in the Application Portal](/img/message-tags.png) They can also be used via the API when [listing messages](https://api.svix.com/docs#tag/Message/operation/v1.message.list) or [message attempts](https://api.svix.com/docs#tag/Message-Attempt/operation/v1.message-attempt.list-attempted-messages). ## When not to use it Tags are not meant as a way to route messages to specific endpoints or specific customers, that's what [event types](./event-types) and [channels](./channels) are for. Message tags have no impact on message delivery. They are only meant to be used for message filtering. --- title: Onboarding --- # Onboarding The onboarding document has now been merged with the rest of the documentation. In order to get started just navigate to the [main docs section](./introduction.mdx). --- title: OpenTelemetry Streaming --- import OpenTelemetryProviderNotes from './_common/otel-provider-notes.mdx' # OpenTelemetry Streaming **Important** OpenTelemetry streaming is only available as part of the Enterprise tier. Please refer to [the pricing page](https://www.svix.com/pricing/) for more information. Some of this functionality can also be achieved by using [Operational Webhooks](/incoming-webhooks). Svix offers OpenTelemetry streaming as a way to stream webhook delivery traces to observability platforms that support OpenTelemetry such as Datadog, Grafana, Coralogix, and most other observability platforms. ## What does Svix send Svix sends OpenTelemetry traces as two spans: the outer span `message_attempt` and the inner span `http_attempt`. Both spans include the following attributes: ```rust start: DateTime // Included as the span start time (not a field) end: DateTime // Included as the span end time (not a field) org_id: OrganizationId app_id: ApplicationId app_uid: Option endpoint_id: EndpointId msg_id: MessageId msg_event_id: Option event_type: EventTypeName, attempt_count: u16 status: MessageStatus ``` And the inner `http_attempt` also has: ```rust http.response.status_code: i16 ``` Here is an example of how it looks like in an observability dashboard: ![OpenTelemetry Spans](/img/opentelemetry-spans.png) ### Additional attributes You have the flexibility to add your own custom attributes to spans at either the app level or per event. #### Application attributes You can add additional otel attributes per app by adding key-value pairs to an application's metadata, prefixing the key with `otel.`. Example: ```json { "metadata": { "otel.custom-app-key-1": "custom-app-value-1" } } ``` The param `custom-app-key-1=custom-app-value-1` will be added to your spans. #### Event attributes You can add additional per-event attributes to your spans by passing custom key-value pairs in `transformationsParams.otel` when calling the Create Message endpoint. Example: ```json { "payload": { "abc": "123" }, "eventType": "my-event", "transformationsParams": { "otel": { "custom-key-1": "custom-value-1", "custom-key-2": "custom-value-2", } } } ``` ## Configuring OpenTelemetry in the Dashboard Configure OpenTelemetry settings in the Observability section of Dashboard settings. Once you've input the URL and optional headers, you can test functionality directly in the browser. ![Configure OpenTelemetry](/img/opentelemetry-config.png) ## How to use it The raw spans sent by Svix can be used in a variety of ways: * Deriving graphs and metrics for your observability dashboards. * Alerting for when specific customers fail over a certain threshold or their latency increases. * Storing raw delivery logs for compliance reasons. * Much more... ### Video: Integrating with Grafana Cloud This video shows how you can quickly integrate Svix OpenTelemetry exports with Grafana Cloud.