Could usernames be included in user_badge webhook payload?

Any chance the user_badge webhooks could include username along with user_id? Currently the payload for a user_badge event looks like this:

{
  "user_badge": {
    "id": 123456,
    "granted_at": "2026-09-25T14:54:42.073Z",
    "created_at": "2026-09-26T05:34:12.965Z",
    "post_id": 234567,
    "post_number": 1,
    "badge_id": 123,
    "user_id": 456,
    "granted_by_id": -1,
    "topic_id": 56789
  }
}

Getting the username for the user_id (and granted_by_id if it’s not -1) requires an additional call to /admin/users/{user_id}.json, which not only requires additional requests, but also requires admin privileges as far as I can tell.

Is there a way to get username for a given a user_id using an API key without needing admin privileges?

I’m creating an external service that I was hoping to trigger when specific badge(s) are granted; however, it needs the username and badge webhooks only returns user IDs. :confused:

FWIW, my workaround for non-standard webhooks or when I need additional data is to do it via Workflows and then send that to n8n.

Fwiw, fetching username from user id is just one API call away, unless you’re manually granting thousands of badges in an instant, you should be fine with the default API limits.

Thanks! I’ll look into Workflows.

That’s great to hear! Could you please provide details about that one API call that isn’t /admin/users/{user_id}.json (which requires admin privileges)? I wasn’t able to find an endpoint to do this with an API key that didn’t require admin privileges.

What’s the issue with an admin API key? This is supposed to be a background job ig?

Principal of Least Privilege. This external service will be running on another server and only needs read access to Discourse to do it’s job. The fewer master keys floating around across infrastructure, the better.

This brings up the interesting point of why isn’t there a public-facing endpoint to get a user by id that shows only public fields, if login_required is not selected. It would be useful, no? I wonder why it was not made. Or maybe only logged in users could get such a privilege, but either way, there should be some way to access it without the use of the admin endpoint…

@burke A more onerous workaround would be to look at /groups/trust_level_0/members.json (or any more specific group) and find the user by their id, since that also contains the username.

This is because for all matters and purposes user IDs are essentially an internal convention in discourse and we are expected to rely on usernames that are much more prominent across the board.

Which brings us back to the original request. The webhook should adhere to that concept and send the username, not the user ID.

Agreed! This seems like the best solution. Adding username and granted_by_username and deprecating user_id and granted_by_id could allow for a non-breaking transition period.

Currently, if a badge is changed by the system (i.e., automatically), the webhook returns a granted_by_id of -1. What would be the equivalent for granted_by_username?

  • "" (empty string)
  • "<system>" (a magic value that couldn’t be a username)
  • null
  • Nothing – i.e., omit granted_by_username from the payload if it was a system-driven event

I would assume null or nothing would make the most sense.

-1 is actually a valid user id that does have a username (usually system) associated with it, just like any other user. There is no reason to treat it differently.

Here on meta https://meta.discourse.org/u/system/summary

Oh. Thanks for teaching me that. So, presumably the granted_by_username would then just be "system" for automated changes.

@putty I’ve been looking into Workflows. Until/unless the badge events include usernames (and maybe regardless, since I can use filters in Workflows to only POST the changes my service needs to hear), this looks like a great alternative option! Thanks again!

With webhooks, there’s a X-Discourse-Event-Signature with an HMAC-SHA256 signature of the payload using a secret that allows me to verify the sender. Any chance there’s a way to sign HTTP requests (POSTs) from Workflows? Maybe I just haven’t found it yet. I was hoping there’d be a setting like “Sign request using secret” that I could toggle on & provide a secret. The closest I’ve seen is adding a header_auth credential to send a secret in a header (e.g., X-Discourse-Event-Secret: Discourse Rocks!). That’s better than nothing, but a signature using a secret (not sending the secret itself) would be preferable.

Unfortunately I’ve relied on sending the secret via a header as well. Perhaps @j.jaffeux can suggest a better alternative, which I’ll promptly utilize as well :sweat_smile: