Sources
What a source is, how its ingest URL works, and how to create, configure, disable and delete one.
A source is an inbound webhook endpoint. Each source has its own ingest URL, and that URL is all a webhook provider needs to send you events. Sources can also verify that a request really came from the provider before anything is stored or delivered.
Sources live inside a project. Everything a source receives becomes an event, and its connections decide where those events go.
How it works
Creating a source gives you a maskId and an ingestUrl:
https://sdhk.events/<maskId>The provider's payload is accepted raw, as it sends it, with any content type and no wrapper object. The maskId is the credential: there is no other auth on the ingest URL.
The ingest URL is served from its own origin, separate from the management API. Posting a webhook to the API host returns 404.
Source ids start with src_. The provider you choose decides how the source verifies requests and which event type it reads by default.
Providers
| Provider | What it is |
|---|---|
github, linear, shopify, nuvion, paystack | HMAC presets. |
stripe | Stripe signature preset. |
openai, anthropic, resend | Standard Webhooks presets. |
generic | Any other sender. You choose the verifier, or none. |
email | Receives inbound email instead of HTTP webhooks. See Email ingestion. |
For a preset provider, the provider fixes the whole signing scheme. You send only the secret.
Verification at a glance
Name a preset provider and send only its secret:
curl -s "$API/v1/sources" \
-H "authorization: Bearer $SDHK_KEY" \
-H 'content-type: application/json' \
-d '{
"name": "GitHub events",
"provider": "github",
"verifierConfig": { "secret": "<the-webhook-secret-you-set-in-github>" }
}'verifierType and every scheme detail are filled in for you. A scheme value that differs from the preset is rejected, not ignored. Use provider: "generic" when you need full control.
Leave out the secret and verifierType (with an empty verifierConfig) and the source accepts everything unverified. A config that carries a secret SolidHooks cannot match to a verifier is rejected rather than stored unverified.
A request that fails verification gets an opaque 401, and is stored as a rejected event so you can see why. For every verifier type, replay windows and the generic HMAC options, see Verification.
Per-source settings
| Field | What it does |
|---|---|
name | Human-readable name, up to 100 characters. |
description | Optional free text, up to 500 characters. |
maxPayloadSizeKb | Largest payload accepted, from 1 to 4096 KB. Defaults to 256. |
idempotencyKeys | Dot-paths such as headers.x-webhook-id or body.data.id used to deduplicate. Rejected on email sources. |
eventTypeField | Dot-path to the event type, headers.<name> or body.<path>. Connection filters match on it. |
customResponseBody, customResponseContentType | A static response returned to the sender. Intended for generic sources. |
rateLimit | Read-only. Requests per second, 0 means unlimited. |
eventTypeField can be set on any provider. Known providers default it to their standard path, for example headers.x-github-event for GitHub and body.type for the three Standard Webhooks presets. Omitting it on create, or sending an empty string on update, resets it to the provider's default (null for generic).
rateLimit is an operational clamp that SolidHooks support sets. It is not writable through the API.
Some presets default idempotencyKeys when you send none. openai and anthropic use ["headers.webhook-id"], and resend uses ["headers.svix-id"]. Anything you send, including an empty array, wins.
Create a source
curl -s "$API/v1/sources" \
-H "authorization: Bearer $SDHK_KEY" \
-H 'content-type: application/json' \
-d '{
"name": "Scratch",
"provider": "generic",
"verifierConfig": {}
}'The response includes id, maskId and ingestUrl. Configure ingestUrl in the webhook provider.
Update a source
curl -s -X PATCH "$API/v1/sources/$SOURCE_ID" \
-H "authorization: Bearer $SDHK_KEY" \
-H 'content-type: application/json' \
-d '{ "data": { "name": "renamed" } }'The fields go inside data. Send only the ones you are changing; an empty data object is a valid no-op.
verifierType and verifierConfig travel together. To touch verification, including rotating a secret, send both. A verifierConfig on its own is rejected.
Changing provider or verifierType needs a new credential, because a credential belongs to one provider under one scheme. Send the field the new verifier reads (secret, password or header_value), or set verifierType: "noop" to turn verification off.
To change a secret without an outage, use secret rotation instead.
Rotate the mask
POST /v1/sources/{id}/rotate-mask gives the source a new maskId and so a new ingest URL. The old URL stops working. For an email source the email address changes too, because it is derived from the maskId.
The dashboard has no control for this yet, so it is an API call. It needs a key with source set to read and write.
Disable or enable
POST /v1/sources/{id}/toggle-disabled takes { "isDisabled": true } or false. A disabled source rejects all incoming webhooks. Nothing is deleted, and enabling it again resumes processing.
In the dashboard, use the Disable and Enable buttons in the source's header.
Delete a source
DELETE /v1/sources/{id} removes the source. In the dashboard, the delete confirmation warns that the source and all its connections are removed, and that this cannot be undone.
Deleting needs the delete permission: an organisation admin or owner, or an API key with source set to read and write. See API keys.
The URL is the credential
Anyone who has the ingest URL can post to it. On a source with no verification, that means anyone can create events. Turn verification on where the provider supports it.
Next steps
- Verification — every verifier and the replay window
- Connections — route a source to destinations
- Events — what a source stores
- Secret rotation — change a verifier secret without dropping traffic