Skip to content
Esc
  • OverviewGuidesWhat exists today, and where to start.
  • QuickstartGuidesKey, domain, first send — in that order.
  • AuthenticationGuidesBearer keys, the mandatory User-Agent, and what each refusal means.
  • ErrorsGuidesThe whole vocabulary, with the status each name carries.
  • IdempotencyGuidesRetry a send without sending it twice.
  • PaginationGuidesCursors are item IDs, not page numbers.
  • Rate limitsGuidesTen a second per team, and the headers that tell you where you are.
  • Sending newslettersGuidesUse a campaign, or include an unsubscribe link.
  • EventsGuidesEvery event a webhook can carry, with one real payload each.
  • DomainsGuidesThe records, where they go at each registrar, and what the page does while you wait.
  • TrackingGuidesOpens and clicks: one record, two toggles, and what an open really means.
  • Sign-up formsGuidesPop-ups and embedded forms on your site, and the rules that pick one.
  • Website trackingGuidesrasket.js on your own site: the snippet, consent, three events and the check.
  • TemplatesGuidesFifty-seven starters sorted by goal, and how to keep a campaign as a template.
  • ReceivingGuidesInbound mail, and the Inbox: a webhook fires, you read it, you answer it.
  • InboxGuidesChannels, personal mailboxes and seats: who sees what, and where a reply goes.
  • Mail appsGuidesComing soon: your Rasket address in Apple Mail, Outlook and the Gmail app.
  • Inbox APIGuidesOpen, read and answer support conversations from your app, with the same key.
  • Support inbox in your appGuidesA Contact us form that opens a conversation your team answers, in five minutes.
  • Replies to your emailsGuidesOne field on a send, and the customer's reply opens a conversation.
  • Let an agent triageGuidesAn AI agent reads, notes and drafts over MCP; a person sends.
  • Node SDKGuidesThe rasket package: typed from the API's own document, retries only what is safe.
  • Python SDKGuidesThe rasket package on PyPI: the Node client's methods, in snake_case, over httpx.
  • MCP serverGuidesConnect Claude, ChatGPT or any MCP client: your scopes, no key.
  • AI assistGuidesSubject lines, drafts and diagnosis — in the dashboard and over the API, off until you allow it.
  • AgentsGuidesLet an AI agent set Rasket up: the skill, the rules file, MCP, and the recipe they share.
  • OAuthGuidesLet another app act for a team: register, authorize with PKCE, exchange, refresh.
  • Single sign-onGuidesOIDC login for your team, a domain proved by DNS, enforcement and break-glass.
  • IntegrationsGuidesEvery integration and its status, and Rasket on Vercel, Netlify and Cloudflare.
  • SMTPGuidesSend from anything that speaks SMTP: settings, setup guides, limits and replies.
  • ZapierGuidesSend email, add contacts and react to email events from a Zap, with no code.
  • n8nGuidesThe Rasket node and trigger for n8n workflows: install, connect, every operation.
  • VercelGuidesAdd Rasket on Vercel: a Sending key in each project as RASKET_API_KEY, no copying.
  • WordPressGuidesThe Rasket plugin: every email your site sends, through Rasket, with a safe fallback.
  • NotionGuidesTurn a Notion database of people into contacts in a segment, once or every hour.
  • Migrating to RasketGuidesBring contacts, unsubscribes, lists and templates from Mailchimp, Klaviyo and more.
  • PipedreamGuidesRasket actions and instant sources for Pipedream workflows and Connect apps.
  • FramerGuidesThe Rasket plugin: sign-up forms on a Framer site, and Framer forms into contacts.
  • MakeGuidesComing soon: send email and react to email events from a Make scenario.
  • monday.comGuidesTurn board items into contacts and send a template when a board changes.
  • ClickUpGuidesReady-made Zapier and Make automations between ClickUp tasks and Rasket.
  • SlackGuidesEmail alerts in the Slack channels you choose, and /rasket for numbers and look-ups.
  • Google SheetsGuidesComing soon: turn a sheet tab of people into contacts in a segment.
  • AirtableGuidesComing soon: turn an Airtable table of people into contacts in a segment.
  • ShopifyGuidesRasket for Shopify: customers with their consent, store events and ready-made flows.
  • SMS quickstartGuidesSend your first text to a US or Canadian phone.
  • Get a numberGuidesToll-free or local numbers, and the registration carriers require.
  • Consent and STOPGuidesWho you may text, the proof you keep, and replies like STOP.
  • SMS campaignsGuidesText a segment, at a local time, priced before you send.
  • Texts in automationsGuidesThe Send SMS step, and what happens to a contact without a phone.
  • SMS webhooksGuidesThe six sms events and what each one carries.
  • SMS pricingGuidesTexts included in each plan, the price per message, and number fees.
  • EmailsAPI referenceSend, batch, retrieve, list, reschedule, cancel, attachments.
  • DomainsAPI referenceAdd a domain, publish its records, verify it.
  • API keysAPI referenceCreate, list, rename and revoke credentials.
  • WebhooksAPI referencePayloads, signature verification, retries and replay.
  • SuppressionsAPI referenceAddresses we will not send to, and why.
  • LogsAPI referenceEvery request made with this team's credentials.
  • MetricsAPI referenceDelivery, bounce, complaint and engagement counts.
  • TemplatesAPI referenceVersioned email content with typed variables, addressed by ID or alias.
  • ContactsAPI referenceYour audience: contacts, their typed properties, segments and topic choices.
  • SegmentsAPI referenceAudiences defined by a filter, by hand, or both.
  • TopicsAPI referenceWhat contacts subscribe to, and the preference page's list.
  • CampaignsAPI referenceCampaigns, at /broadcasts: one message to a segment, from draft to results.
  • AutopilotAPI referenceStore campaigns Autopilot drafts for you, each waiting for a yes or a skip.
  • ImportsAPI referenceCSV uploads: column mapping, conflicts and counts.
  • AutomationsAPI referenceWorkflows that run per contact: the graph, its versions, and every run.
  • Custom eventsAPI referenceThe names your product fires, and what starts a workflow.
  • ReceivingAPI referenceMail sent to you: the message, its attachments, its raw source.
  • OAuthAPI referenceClient registration, the token endpoint, and the grants a team has given.
  • TeamAPI referenceThe team a credential belongs to: its plan, sender identity, AI flag and members.
  • BillingAPI referencePlan, usage, invoices and add-ons, and the hosted pages where a customer pays.
  • AI helpersAPI referenceSubject lines, a first draft, and why an email did what it did.
  • InboxAPI referenceChannels, conversations, messages, notes and search in your support inbox.
  • SMSAPI referenceSend, schedule and cancel texts, numbers, test phones and SMS consent.

API referenceWebhooks

Webhooks

We POST a signed JSON payload to your URL when something happens on your team: an email is sent, delivered, bounces or arrives, a text is sent or answered, a contact, domain or suppression changes, an automation runs, or Autopilot proposes a campaign. This page is the payload, the signature, and what we do when your endpoint is down.

The payload

Every event has the same envelope: a type, the created_at of the event itself, and a data object describing the email. Types that carry more detail add one extra key inside data.

{
  "type": "email.bounced",
  "created_at": "2026-09-09T10:16:44.902Z",
  "data": {
    "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
    "from": "Acme <orders@send.acme.example>",
    "to": ["ronald.williams@example.com"],
    "subject": "Your order has shipped",
    "message_id": "<01000199a3c4d5e6-7f8a9b0c@send.acme.example>",
    "created_at": "2026-09-09T10:14:02.118Z",
    "tags": { "order": "1042" },
    "bounce": {
      "type": "Permanent",
      "subType": "General",
      "message": "smtp; 550 5.1.1 The email account that you tried to reach does not exist.",
      "diagnosticCode": ["smtp; 550 5.1.1 user unknown"]
    }
  }
}

data always carries email_id, from, to, subject, message_id and the email's own created_at. tags and headers appear when the email had them. For per-recipient events — bounce, complaint, delivery — to lists the recipients that event is about, not everyone the message went to.

Event-specific objects
TypeExtra keyFields
email.bouncedbouncetype, subType, message, diagnosticCode[]
email.openedopenipAddress, timestamp, userAgent
email.clickedclickipAddress, link, timestamp, userAgent
email.failedfailedreason
email.suppressedsuppressedreason, type, message, diagnosticCode[]
email.delivery_delayeddelaytype, expirationTime, delayedRecipients[]
email.sent, email.scheduled, email.delivered, email.complained—The envelope, with no extra object.
sms.failedfailedtype (undelivered | skipped), reason, description, carrier_code

Event types

An endpoint subscribes to one or more of these. Any other type is refused when you create or update the endpoint.

  • email.sent
  • email.scheduled
  • email.delivered
  • email.delivery_delayed
  • email.bounced
  • email.complained
  • email.opened
  • email.clicked
  • email.failed
  • email.suppressed
  • email.canceled
  • domain.created
  • domain.updated
  • domain.deleted
  • suppression.added
  • suppression.removed
  • contact.created
  • contact.updated
  • contact.deleted
  • email.received
  • automation.run.started
  • automation.run.completed
  • automation.run.failed
  • inbox.conversation.created
  • inbox.message.received
  • inbox.reply.sent
  • inbox.note.created
  • inbox.conversation.assigned
  • inbox.conversation.status_changed
  • sms.queued
  • sms.sent
  • sms.delivered
  • sms.failed
  • sms.received
  • contact.sms_unsubscribed
  • autopilot.proposal.created
  • autopilot.proposal.approved
  • autopilot.proposal.skipped
  • autopilot.proposal.expired
  • autopilot.proposal.sent

Verifying a signature

Every delivery carries three headers. Reject anything that does not verify — the URL is public, and the signature is the only thing that says the request came from us.

Headers we send
HeaderMeaning
svix-idThe event's ID. Stable across replays — dedupe on this.
svix-timestampUnix seconds. Reject anything more than five minutes old.
svix-signatureOne or more `v1,<base64>` signatures, space separated. Accept the payload if any of them verifies.
user-agentRasket-Webhooks/1.0

The signature covers the raw request body. Any framework that parses JSON before your handler runs has already destroyed the bytes we signed — key order and whitespace both change when a parsed object is serialized again, and the signature will never match. Read the body as text or bytes first, verify, then parse.

The contract

Five steps, identical in every language:

verify(rawBody, headers, secret) -> payload

  1. require svix-id, svix-timestamp, svix-signature
  2. reject if |now - timestamp| > 300 s
  3. expected = base64(hmac_sha256(
       base64decode(secret without the whsec_ prefix),
       `${id}.${timestamp}.${rawBody}`
     ))
  4. for each "v1,<sig>" in svix-signature (space separated):
       constant-time compare against expected
  5. any match -> JSON.parse(rawBody); none -> throw

All seven recipes use the official Svix libraries today: the scheme is Svix-compatible, so they verify our signatures as they are. @rasket/webhook-verify, our own zero-dependency verifier, follows when it is published. Every recipe reads the raw body first; that is the line that matters.

Node

// @rasket/webhook-verify, our own zero-dependency verifier, is not on npm yet. The
// scheme is Svix-compatible, so the official svix package verifies it today.
// npm install svix
import { createServer } from "node:http";
import { Webhook } from "svix";

const wh = new Webhook(process.env.RASKET_WEBHOOK_SECRET);

createServer((req, res) => {
  // Collect the raw bytes. Nothing parses them: req.body does not exist here, which is
  // the one thing a bare Node server has going for it.
  const chunks = [];
  req.on("data", (chunk) => chunks.push(chunk));
  req.on("end", () => {
    const rawBody = Buffer.concat(chunks);

    try {
      const event = wh.verify(rawBody, req.headers);
      handle(event);
      res.writeHead(200).end();
    } catch {
      // A missing header, a timestamp more than five minutes off, or no matching signature.
      res.writeHead(400).end();
    }
  });
}).listen(3000);

Next.js

// app/api/hooks/rasket/route.ts
// @rasket/webhook-verify, our own zero-dependency verifier, is not on npm yet. The
// scheme is Svix-compatible, so the official svix package verifies it today.
// npm install svix
import { Webhook } from "svix";

const wh = new Webhook(process.env.RASKET_WEBHOOK_SECRET);

export async function POST(request: Request) {
  // await request.text(), NOT request.json(). Parsing to an object and serializing it
  // again is not the identity function — key order and whitespace both change, and the
  // signature is over the bytes we sent.
  const rawBody = await request.text();

  try {
    // request.headers is a Headers; the verifier wants a plain object.
    const event = wh.verify(rawBody, Object.fromEntries(request.headers));
    await handle(event);
  } catch {
    return new Response("invalid signature", { status: 400 });
  }

  return new Response("ok", { status: 200 });
}

Express

// @rasket/webhook-verify, our own zero-dependency verifier, is not on npm yet. The
// scheme is Svix-compatible, so the official svix package verifies it today.
// npm install svix
import express from "express";
import { Webhook } from "svix";

const app = express();
const wh = new Webhook(process.env.RASKET_WEBHOOK_SECRET);

// express.raw(), NOT express.json(). Mount it on this route only and BEFORE any global
// body parser, so the rest of your application still gets parsed bodies.
app.post("/hooks/rasket", express.raw({ type: "application/json" }), (req, res) => {
  // req.body is a Buffer here — the exact bytes we signed.
  try {
    const event = wh.verify(req.body, req.headers);
    handle(event);
    res.sendStatus(200);
  } catch {
    res.status(400).send("invalid signature");
  }
});

app.use(express.json()); // everything else, after the webhook route

Python

# pip install svix
from flask import Flask, request
from svix.webhooks import Webhook, WebhookVerificationError

app = Flask(__name__)


@app.post("/hooks/rasket")
def rasket_webhook():
    # request.get_data() is the raw body. Never request.get_json() first:
    # it parses, and the signature is over what arrived.
    raw_body = request.get_data()

    try:
        event = Webhook(RASKET_WEBHOOK_SECRET).verify(raw_body, dict(request.headers))
    except WebhookVerificationError:
        return "invalid signature", 400

    handle(event)
    return "", 200


# FastAPI is the same shape: raw_body = await request.body()

Go

// go get github.com/svix/svix-webhooks/go
package main

import (
	"io"
	"net/http"
	"os"

	svix "github.com/svix/svix-webhooks/go"
)

func rasketWebhook(w http.ResponseWriter, r *http.Request) {
	// io.ReadAll on r.Body, before anything decodes it. json.NewDecoder(r.Body)
	// consumes the reader, and the bytes are gone.
	rawBody, err := io.ReadAll(r.Body)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	wh, err := svix.NewWebhook(os.Getenv("RASKET_WEBHOOK_SECRET"))
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	if err := wh.Verify(rawBody, r.Header); err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	handle(rawBody) // decode it now, after the signature checked out
	w.WriteHeader(http.StatusOK)
}

Ruby

# gem install svix
require "sinatra"
require "svix"

post "/hooks/rasket" do
  # request.body.read is the raw body. Do not use params or a JSON middleware:
  # both parse, and the signature is over what arrived.
  raw_body = request.body.read
  request.body.rewind

  begin
    wh = Svix::Webhook.new(ENV["RASKET_WEBHOOK_SECRET"])
    event = wh.verify(raw_body, request.env)
  rescue Svix::WebhookVerificationError
    halt 400, "invalid signature"
  end

  handle(event)
  status 200
end

PHP

<?php
// composer require svix/svix
use Svix\Webhook;
use Svix\Exception\WebhookVerificationException;

// file_get_contents("php://input") is the raw body. $_POST is a parsed form and
// json_decode of it is a different string from the one we signed.
$rawBody = file_get_contents("php://input");

$headers = [
    "svix-id" => $_SERVER["HTTP_SVIX_ID"] ?? "",
    "svix-timestamp" => $_SERVER["HTTP_SVIX_TIMESTAMP"] ?? "",
    "svix-signature" => $_SERVER["HTTP_SVIX_SIGNATURE"] ?? "",
];

try {
    $wh = new Webhook(getenv("RASKET_WEBHOOK_SECRET"));
    $event = $wh->verify($rawBody, $headers);
} catch (WebhookVerificationException $e) {
    http_response_code(400);
    exit("invalid signature");
}

handle($event);
http_response_code(200);

Rotation

POST /webhooks/{id}/rotate-secret issues a new secret and keeps the previous one signing for 24 hours. During the overlap svix-signature carries both signatures, space separated. A verifier that accepts any matching signature — as the code above does — needs no downtime and no coordinated deploy. Both secrets start with whsec_ and are shown once.

Ordering and duplicates

  • Events are not ordered. A delivery can arrive before the sent event that logically precedes it. Key your handler on email_id plus type plus created_at rather than on arrival order.
  • Events can arrive more than once. Dedupe on svix-id, which is stable for the life of an event and does not change when it is replayed.
  • Answer with any 2xx as soon as you have stored the event. Do your work afterwards; a handler that finishes its work before replying will eventually time out and be retried.

Retries

Anything that is not a 2xx within ten seconds is a failure: a timeout, a DNS or TLS error, a refused connection, or a 3xx — we never follow redirects, so a redirect is a failed delivery, not a hop. We try up to ten times in all — the first delivery and nine retries — with ±10% jitter on every delay so a recovering outage does not get a thundering herd.

The retry schedule
AttemptDelay before it
1immediately
25 s
330 s
42 m
510 m
630 m
71 h
82 h
94 h
108 h

After the tenth attempt the event is marked failed and we stop. Nothing is lost — it stays readable, and you can replay it. An endpoint with no successful delivery for five consecutive days is disabled automatically and the team's admins are emailed; events keep being recorded for it while it is off.

Endpoints

Create an endpoint

POST /webhooks

Subscribe a URL to the events you care about.

Body

  • endpointstringRequired

    An absolute https URL. Redirects are never followed, so give the final one.

  • eventsstring[]Required

    At least one event type from the Events list. An unknown type is refused.

Request

curl -X POST "https://api.rasket.com/webhooks" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "endpoint": "https://acme.example.com/hooks/rasket",
  "events": ["email.delivered", "email.bounced", "email.complained"]
}'

Response 201

{
  "object": "webhook",
  "id": "3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38",
  "signing_secret": "whsec_5+7nYV7zXzDkm0guFcrnUAcgHhJhew/t"
}
  • signing_secret is returned by this response and never again in full. Every later read shows it masked.
  • The URL is checked against blocked (private and internal) address ranges when you create it, and again on every delivery.
  • How many endpoints a team may have depends on its plan.

List endpoints

GET /webhooks

Every endpoint on the team.

Query parameters

  • limitinteger

    How many items to return, 1–100. Defaults to 20.

  • afterstring

    Return the page that follows this item ID. Mutually exclusive with before.

  • beforestring

    Return the page that precedes this item ID. Mutually exclusive with after.

Request

curl -X GET "https://api.rasket.com/webhooks" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38",
      "endpoint": "https://acme.example.com/hooks/rasket",
      "events": ["email.delivered", "email.bounced", "email.complained"],
      "status": "enabled",
      "created_at": "2026-09-09T09:20:31.004Z"
    }
  ]
}

Retrieve an endpoint

GET /webhooks/{webhook_id}

One endpoint and its subscriptions.

Path parameters

  • webhook_idstringRequired

    The endpoint's ID.

Request

curl -X GET "https://api.rasket.com/webhooks/3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "webhook",
  "id": "3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38",
  "endpoint": "https://acme.example.com/hooks/rasket",
  "events": ["email.delivered", "email.bounced", "email.complained"],
  "status": "enabled",
  "signing_secret": "whsec_••••••••Ab3d",
  "created_at": "2026-09-09T09:20:31.004Z"
}
  • signing_secret comes back masked. Revealing it in full is a dashboard action and is audited.

Update an endpoint

PATCH /webhooks/{webhook_id}

Change the URL, the subscriptions, or turn it off.

Path parameters

  • webhook_idstringRequired

    The endpoint's ID.

Body

  • endpointstring

    A new absolute https URL.

  • eventsstring[]

    Replaces the subscription list.

  • statusstring

    enabled or disabled.

Request

curl -X PATCH "https://api.rasket.com/webhooks/3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "events": ["email.delivered", "email.bounced", "email.complained", "email.failed"]
}'

Response 200

{
  "object": "webhook",
  "id": "3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38"
}
  • Re-enabling an endpoint resumes new events only. Older ones are replayed explicitly.

Delete an endpoint

DELETE /webhooks/{webhook_id}

Stop delivering to this URL.

Path parameters

  • webhook_idstringRequired

    The endpoint's ID.

Request

curl -X DELETE "https://api.rasket.com/webhooks/3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "webhook",
  "id": "3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38",
  "deleted": true
}

List events

GET /webhooks/{webhook_id}/events

What we tried to deliver to this endpoint.

Path parameters

  • webhook_idstringRequired

    The endpoint's ID.

Query parameters

  • limitinteger

    How many items to return, 1–100. Defaults to 20.

  • afterstring

    Return the page that follows this item ID. Mutually exclusive with before.

  • beforestring

    Return the page that precedes this item ID. Mutually exclusive with after.

4 more fields (status, type, start_date, end_date)
  • statusstring

    Only events in this state: pending, attempting, success or failed.

  • typestring

    Only events of this type, such as email.bounced.

  • start_datestring

    ISO 8601 instant, inclusive. A calendar date alone is 422 invalid_parameter — send 2026-09-09T00:00:00.000Z.

  • end_datestring

    ISO 8601 instant, inclusive. Must be at or after start_date, or the request is 422 invalid_parameter.

Request

curl -X GET "https://api.rasket.com/webhooks/3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38/events" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "list",
  "has_more": true,
  "data": [
    {
      "id": "msg_0198f4c2a1b27c3e9d4f5a6b7c8d9e0f",
      "type": "email.delivered",
      "created_at": "2026-09-09T10:14:09.331Z",
      "status": "success"
    }
  ]
}
  • Event IDs start with msg_ and are the value of the svix-id header we sent.

Retrieve an event

GET /webhooks/{webhook_id}/events/{event_id}

One event, with the exact payload we signed.

Path parameters

  • webhook_idstringRequired

    The endpoint's ID.

  • event_idstringRequired

    The event's msg_ ID.

Request

curl -X GET "https://api.rasket.com/webhooks/3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38/events/msg_0198f4c2a1b27c3e9d4f5a6b7c8d9e0f" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "webhook_event",
  "id": "msg_0198f4c2a1b27c3e9d4f5a6b7c8d9e0f",
  "type": "email.delivered",
  "status": "success",
  "attempt_count": 1,
  "last_http_status": 200,
  "next_attempt_at": null,
  "created_at": "2026-09-09T10:14:09.331Z",
  "payload": {
    "type": "email.delivered",
    "created_at": "2026-09-09T10:14:09.331Z",
    "data": {
      "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
    }
  }
}

List attempts

GET /webhooks/{webhook_id}/events/{event_id}/attempts

Every delivery we made for one event, and what came back.

Path parameters

  • webhook_idstringRequired

    The endpoint's ID.

  • event_idstringRequired

    The event's msg_ ID.

Request

curl -X GET "https://api.rasket.com/webhooks/3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38/events/msg_0198f4c2a1b27c3e9d4f5a6b7c8d9e0f/attempts" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "atmpt_0198f4c2a1c07d4e8f9a0b1c2d3e4f50",
      "attempt_number": 1,
      "http_status_code": 200,
      "response": "ok",
      "duration_ms": 142,
      "error": null,
      "sent_at": "2026-09-09T10:14:09.480Z"
    }
  ]
}
  • Every attempt, oldest first, in one response: the list is not paginated.
  • Your response body is stored, truncated to 8 KB. Do not answer with anything secret.
  • error names a failure other than your status code — timeout (we wait 10 seconds), redirect_not_followed, a blocked address — and is null when your endpoint simply answered.

Replay an event

POST /webhooks/{webhook_id}/events/{event_id}/replay

Send the same bytes again, after you have fixed your handler.

Path parameters

  • webhook_idstringRequired

    The endpoint's ID.

  • event_idstringRequired

    The event's msg_ ID.

Request

curl -X POST "https://api.rasket.com/webhooks/3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38/events/msg_0198f4c2a1b27c3e9d4f5a6b7c8d9e0f/replay" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "webhook_event",
  "id": "msg_0198f4c2a1b27c3e9d4f5a6b7c8d9e0f"
}
  • A replay re-sends the identical payload with the same svix-id and a fresh timestamp and signature — so a handler that dedupes on svix-id will correctly ignore it.
  • Ten replays per minute per team. Replaying to a disabled endpoint is refused.

Rotate the signing secret

POST /webhooks/{webhook_id}/rotate-secret

Issue a new secret with a 24-hour overlap.

Path parameters

  • webhook_idstringRequired

    The endpoint's ID.

Request

curl -X POST "https://api.rasket.com/webhooks/3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38/rotate-secret" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "webhook",
  "id": "3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38",
  "signing_secret": "whsec_p+Fkd5b/AivqjtAqgqF1kw8jN803lMUi"
}
  • For 24 hours both secrets sign, and svix-signature carries both signatures space-separated. Accept any one that verifies and the rotation needs no downtime.
  • The new secret is shown once, exactly like the first one.

List parked events

GET /webhooks/{webhook_id}/parked

The events created while the endpoint was disabled, oldest first.

Path parameters

  • webhook_idstringRequired

    The endpoint's ID.

Query parameters

  • limitinteger

    How many to return, 1–100. has_more says whether more are parked.

Request

curl -X GET "https://api.rasket.com/webhooks/3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38/parked?limit=20" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "msg_0198f4c2a1b27c3e9d4f5a6b7c8d9e0f",
      "type": "email.delivered",
      "created_at": "2026-09-09T09:20:33.412Z",
      "status": "failed",
      "attempt_count": 6,
      "last_http_status": 503
    }
  ]
}
  • A disabled endpoint keeps receiving events without delivering them. They are listed oldest first, the order they are delivered in.

Deliver parked events

POST /webhooks/{webhook_id}/parked/deliver

Replay the oldest parked events, in order, within the replay budget.

Path parameters

  • webhook_idstringRequired

    The endpoint's ID.

Request

curl -X POST "https://api.rasket.com/webhooks/3b7f21c0-d9a4-4e8b-b6c2-7a1f5d0e9c38/parked/deliver" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"

Response 200

{
  "object": "parked_delivery",
  "delivered": 10,
  "remaining": 32,
  "stopped_reason": "rate_limit_exceeded"
}
  • At most 10 events per call, inside the same budget as a single replay: 10 a minute per team. When it runs out, delivery stops with stopped_reason: "rate_limit_exceeded".
  • Call again while remaining is above zero. A disabled endpoint is 422 validation_error: enable it first.