Webhooks
Do you need this? Most stores do not. Webhooks are for connecting Champ to another system, usually with a developer's help. If you only run a forum, you can skip this guide.
A webhook sends a message to a web address you choose whenever someone creates a topic or a post. Use it to send forum activity to Slack, a CRM, a support desk, a spreadsheet, or any other tool that can receive a web request.
What is a webhook?#
A webhook is an HTTP request (a message sent over the web) that Champ sends to your system. Your system does not have to keep asking Champ "has anything happened?". You give Champ a URL, and Champ calls it when something happens.
How Champ webhooks work#
Each webhook has:
- A URL to call
- A method —
POSTorPUT - An on/off switch — only enabled webhooks are sent
- A topic —
new_topicornew_post, the event that triggers it - A payload — the body of the request: a Liquid template (Shopify's template language) that renders to JSON
- Optional headers — your own name/value pairs, for API tokens and the like
- Optional basic auth — a username and password, stored encrypted, used only when both are filled in
- An expected response code — what your endpoint returns on success; blank means
200
Champ sends the webhook in the background, about 30 seconds after the topic or post is created. A slow endpoint never slows down a member's post. If your endpoint does not return the expected code, Champ retries up to 10 times, waiting longer before each retry. Posts that Champ's spam filter catches do not trigger new_post.
Creating a webhook#
- In the Champ dashboard, open Webhooks.
- Click Create webhook.
- In URL to send the webhook to, enter your endpoint. It must start with
http://orhttps://and be reachable from the public internet —localhostwill not work. Each URL can be used by only one webhook. - Choose the Method and whether it is Enabled.
- In Send when, choose A new topic is created (
new_topic) or A new reply is created (new_post). - Optionally, set the Expected response code.
- Edit the Payload, or keep the default.
- Under Custom headers, click Add header for each header your endpoint needs.
- Click Save.
The payload template#
The payload is Liquid, rendered to JSON. The default looks like this:
{
{% if user %}
"user_id": "{{ user.id }}",
"user_email": "{{ user.email }}",
{% endif %}
{% if webhook.topic == 'new_topic' %}
"topic": {
"id": "{{ topic.id }}",
"title": "{{ topic.title }}",
"url": "{{ topic.url }}",
"forum": "{{ topic.forum_title }}"
}
{% endif %}
{% if webhook.topic == 'new_post' %}
"post": {
"id": "{{ post.id }}",
"content": "{{ post.content }}",
"topic": "{{ post.topic.title }}",
"forum": "{{ post.topic.forum_title }}"
}
{% endif %}
}
Available objects depend on the event:
| Object | Available on | Useful fields |
|---|---|---|
webhook | both | topic (which event this is) |
user | both | id, email |
topic | new_topic | id, title, url, forum_title |
post | new_post | id, content, topic.title, topic.forum_title |
Warning The rendered template must be valid JSON. Champ parses it before sending, and a trailing comma or an unescaped quote in post content will break delivery. Champ validates the template's syntax when you save, but it cannot know what a member will type — test with real content before relying on it.
Reshape the template freely to match whatever your endpoint expects. For Slack, for example, render Slack's own message format directly rather than posting Champ's default and translating it on the other side.
Verifying webhooks#
Every request carries these headers:
Content-Type: application/json
X-Powered-By: Champ Forums
X-Champ-Hmac-SHA256: <base64 signature>
Use the X-Champ-Hmac-SHA256 header to check that a request really came from Champ. HMAC is a signature: Champ computes it from the request body and a secret only you and Champ know. Anyone who does not have the secret cannot produce a valid signature.
The signature is a base64-encoded HMAC-SHA256 of the raw request body, keyed with your store's shared secret — shown under Shared secret at the top of the Webhooks page in the dashboard. It is the same scheme Shopify uses for its own webhooks.
To verify, compute the HMAC of the raw body you received and compare it to the header using a constant-time comparison:
expected = Base64.strict_encode64(
OpenSSL::HMAC.digest('sha256', SHARED_SECRET, request.raw_post)
)
ActiveSupport::SecurityUtils.secure_compare(expected, request.headers['X-Champ-Hmac-SHA256'])
const expected = crypto
.createHmac('sha256', SHARED_SECRET)
.update(rawBody, 'utf8')
.digest('base64');
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
Verify against the raw body, before any JSON parsing — re-serialising changes the bytes and the signature will not match. Reject anything that fails, and do it before you act on the contents.
Modifying and deleting#
On the Webhooks page, click Edit webhook to change any field, then click Save.
To remove it, open it with Edit webhook, click Delete webhook, and confirm. To pause it without deleting it, set Enabled to Disabled.
Troubleshooting#
The dashboard does not show a delivery log, so check on your side, in your endpoint's logs.
Nothing arrives.
- Check that the webhook is set to Enabled.
- Check that the URL is reachable from the public internet.
- Wait at least 30 seconds after creating the topic or post.
Requests arrive, but Champ keeps retrying. Your endpoint is not returning the Expected response code (or 200, if that field is blank).
Nothing arrives for some posts only. The rendered payload is probably not valid JSON for that content. Champ then cannot send it. The usual cause is a comma in the wrong place when a conditional block is skipped, or a quote in the member's text.
Your endpoint is timing out. Return a response fast and do the work afterwards. Acknowledge first, process second.