Back to blog

Send Form Submissions to Discord (For When Slack Is Overkill)

Manuk Minasyan · · 7 min read

If your team's on Slack, the Slack version of this post covers that. This one's for the Discord crowd.

I did the maths once for a three-person side project. Slack Pro is $7.25 per user per month. Three seats, billed annually, came out to around $22 a month, every month, in perpetuity, for the privilege of having form notifications land in a channel. The product wasn't making $22 a month yet. The conversation ended there.

Discord costs nothing. The team was already in the server, the side project had a channel, and a Discord webhook URL is two clicks away in server settings. The bridge between a Laravel form submission and a Discord message is shorter than you'd expect — short enough that any project on FilaForms can ship it in an afternoon. The pattern is the same as Slack, the API is friendlier, and there's no OAuth dance to deal with. This is the Laravel form to Discord wiring, end to end.

Two ways to send form submissions to Discord

You have two paths. One is a webhook URL. The other is a full Discord bot.

A Discord webhook is a URL you create inside a server channel that accepts POST requests and turns them into messages. No OAuth, no bot user, no permissions intent — copy the URL, POST JSON, you get a post. A full bot is a gateway-connected application with slash commands, presence, and the ability to receive events. It's a real build.

Discord webhook Slack incoming webhook
Setup time ~2 minutes ~5 minutes
OAuth required No No
Cost Free, no seat tax Free, but channel sits in a paid workspace
Message format Embeds (rich, native) Block Kit (structured blocks)
Rate limit 30/min per webhook ~1/sec, burstable
Best for Indie teams, communities, side projects Sales/ops teams on a paid Slack plan

For almost every indie team — a contact form, a waitlist, a feedback channel — the webhook is the right answer. The rest of this post covers that path.

How to send form submissions to Discord from Laravel

The shortest version: create a Discord webhook URL in your server, store it as a config value, and have a Laravel listener POST a formatted embed to it on every form submission. FilaForms fires a FormSubmitted event with the form and submission models attached, which is where the listener hangs. The whole thing is about a dozen lines of PHP plus the embed shape.

Setting up the Discord webhook

Open your Discord server, click the server name, choose Server Settings, then Integrations, then Webhooks. Click New Webhook, give it a name your team will recognize ("FilaForms" works), pick the channel you want submissions posted into, and click Copy Webhook URL.

The URL looks like https://discord.com/api/webhooks/<id>/<token>. That's the endpoint. Discord's webhook documentation is worth a skim if you want the full payload schema.

Treat this URL like a password. Anyone holding it can post messages into your channel as the webhook user. Don't commit it. Drop it in your .env:

DISCORD_FORM_WEBHOOK_URL=https://discord.com/api/webhooks/.../...

Then add a config entry in config/services.php so you never call env() outside config files:

'discord' => [
    'form_webhook_url' => env('DISCORD_FORM_WEBHOOK_URL'),
],

That's the Discord side done. The URL is the destination every form submission needs to reach.

Wiring the Discord webhook to FilaForms

There are two ways to make a submission reach that URL.

The first is what FilaForms ships out of the box: configure the outgoing webhook layer to POST to your Discord URL directly. No code. Discord receives a POST, but the raw payload doesn't match the embed shape Discord wants, so the message lands as the bot saying nothing visible. Fine for hackers checking a server log; not fine for a channel humans read.

The second is a small listener that hooks into FormSubmitted and builds the embed Discord expects:

namespace App\Listeners;

use FilaForms\Core\Events\FormSubmitted;
use Illuminate\Support\Facades\Http;

class PostSubmissionToDiscord
{
    public function handle(FormSubmitted $event): void
    {
        $fields = collect($event->submission->data)
            ->take(3)
            ->map(fn ($value, $key) => [
                'name' => (string) $key,
                'value' => (string) $value,
                'inline' => true,
            ])
            ->values()
            ->all();

        Http::post(config('services.discord.form_webhook_url'), [
            'embeds' => [[
                'title' => "New {$event->form->name} submission",
                'color' => 5814783,
                'fields' => $fields,
                'timestamp' => now()->toIso8601String(),
            ]],
        ]);
    }
}

Register it against FormSubmitted in your EventServiceProvider (or rely on auto-discovery), and every submission triggers a post. The take(3) keeps the embed scannable on mobile — Discord renders three inline fields as a single row.

Discord embed format

A Discord embed is a structured card with a fixed schema. The fields you care about for a form submission are five: title, description, fields (an array of key-value pairs with an inline flag), color (a decimal integer for the side stripe), and footer or timestamp.

Map your FilaForms field labels into the fields array — that's the listener's only real job. If your form has a name, email, and message, the keys land in the embed as bold labels and the values land underneath. The inline flag tells Discord to lay three fields side-by-side on desktop and stack them on mobile.

For the footer, set a footer.text value that links back to the admin submission detail page once you have one. Discord embeds don't support clickable footer text by themselves, but you can put a URL into description as a Markdown link — [View in admin](https://yourapp.test/admin/submissions/123) — and Discord will render it as a clickable line under the title.

Edge cases

Discord's webhook rate limit is 30 messages per minute per webhook, per their developer docs. A form that takes more than 30 submissions a minute is a great problem, but it's also one your listener needs to handle without dropping data. Add implements ShouldQueue to the listener so the post happens on a queue worker — 429 responses surface as failed jobs you can retry instead of swallowed exceptions on a web request.

Embed limits matter too. Discord caps embed title at 256 characters, description at 4096, and the whole embed at 6000 characters across all fields combined. The fields array maxes out at 25 entries. None of these come up for a normal contact form, but they will if you point this at a 40-question survey.

What we got wrong

The first version POSTed a content string that read New contact submission: {...} with the form data inlined as a JSON-ish blob. It worked. The Discord server owner muted the channel within a day.

The issue wasn't volume. It was aesthetic. Discord server owners curate channels — emoji, role colours, embed cards — and a bot dumping plain-text JSON into the middle of that breaks the visual rhythm of the space. Slack channels tolerate ugly. Discord channels don't. We rebuilt the post as an embed with a coloured stripe, a header, three inline fields, and a timestamp. The same data, dressed for the room it was going into. The channel got unmuted. Server owner stopped messaging me.

The lesson held over from the Slack post: data is not communication. The platform shapes the format. Pick the one the readers expect.

Try it

Discord delivery is one spoke off FilaForms' webhook delivery. Set up the URL, drop in the listener, ship the embed shape. Your three-person side project gets the ping for free.

If you haven't yet, give FilaForms a spin. Next in the Connect series: Airtable, for the teams that want submissions to land in a tracker grid instead of a chat channel.

Related posts