Webhook Payload Design: Thin vs Fat Payloads

Thin vs fat webhook payloads and safe migration

Designing webhook payloads well avoids repeated breaking changes and reduces consumer complexity. This guide compares thin vs fat payload models, recommends a hybrid approach, and shows Laravel validation patterns you can ship today.

You will learn when to send identifiers only, when to include actionable snapshots, and how to add schema_version and backward-compatible validation. The primary keyword “webhook payload design thin vs fat” appears in the recommended migration guidance.

Define the “contract” before you ship

Agree a stable contract — event key, payload shape, and version — before you add fields.

Event key as routing primitive (event_key) ([SendPromptly][3])

Use event_key for routing and consumer intent. Consumers should switch on event_key, not on payload internals.

Delivery semantics: at-least-once, retries on failure

Expect retries — make payloads idempotent-friendly and small enough to avoid timeouts.

Micro checklist:

  • Publish event_key and semantics in your contract.
  • Add schema_version when changing shape.
  • Document retries and idempotency expectations for consumers.

Thin payload model

When to use identifiers only and let consumers fetch details.

Include identifiers only (entity_id, event_id)

Thin payloads are small, stable, and keep network usage low. Consumers fetch authoritative details on demand.

Consumer fetches details

Prefer thin when consumers already have reliable fetch endpoints or when payload size matters.

Common gotcha: Thin payloads without a stable fetch endpoint are unusable — ensure a documented detail endpoint exists.

Fat payload model

When to include the full snapshot consumers need to act without additional requests.

Include full snapshot needed to act

Fat payloads improve speed and reduce two-phase fetch logic for consumers, but increase size and coupling.

Large payload risks + validation

Large payloads can cause timeouts, higher latency, and parsing errors. Validate size on both sender and receiver.

Micro checklist:

  • Limit payload size (document a max bytes).
  • Validate required keys and types.
  • Use schema_version for breaking changes.

Use an actionable snapshot for the common fast path, plus stable identifiers for deep-fetches.

“Actionable snapshot + stable identifiers”

  • Include a small, actionable snapshot (status, key fields) plus entity_id for a deeper fetch if needed.
  • Keep optional, non-critical fields clearly documented as nullable/optional.

Add schema_version

Version the payload so consumers can handle migration changes deterministically.

Micro checklist:

  • Add payload.schema_version (integer).
  • Keep additive changes backward compatible.
  • Deprecate fields across releases, not abruptly.

Use the Sample Project to trigger one event and verify your consumer accepts the schema in Message Log.

Laravel validation and backward compatibility

Validate incoming webhook payloads with explicit rules and fail fast for malformed data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

Route::post('/webhooks/sendpromptly', function (Request $r) {
    $data = $r->json()->all();

    $v = Validator::make($data, [
        'event_key' => 'required|string',
        'payload'   => 'required|array',
        'payload.schema_version' => 'nullable|integer',
        'payload.entity_id'      => 'required|string',
        'payload.occurred_at'    => 'required|date',
    ]);

    if ($v->fails()) {
        // Prefer 2xx + DLQ in some systems; if you hard-fail, be sure it’s truly permanent.
        return response()->json(['error' => 'validation_failed', 'details' => $v->errors()], 422);
    }

    return response()->json(['ok' => true], 200);
});

Common gotcha: Returning 5xx for a permanent validation error causes pointless retries — prefer 422 or accept+DLQ.

Testing and rollout

  • Send a payload missing required fields → expect 422.
  • Send a valid payload → expect 200.

For end-to-end testing, trigger a SendPromptly event and confirm the webhook run shows 2xx success (and retries on failure).

Failure modes

  1. No schema version ⇒ breaking changes slip in unnoticed.
  2. Fat payload grows unbounded ⇒ timeouts and retries.
  3. Thin payload without fetch endpoint ⇒ consumers can’t act.
  4. Consumers assume optional fields are always present ⇒ runtime errors.
  5. “Validation failed” returned as 5xx ⇒ pointless retries.
  6. Non-deterministic JSON transforms before signing/verification.

Use the Sample Project to validate schema acceptance in Message Log.

Conclusion

  • Prefer a hybrid: actionable snapshot + stable identifiers.
  • Add schema_version for safe migrations.
  • Validate with explicit Laravel rules and avoid 5xx for permanent validation errors.
  • Keep payloads small where possible; document size limits and expected optional fields.
  • Use Message Log to inspect delivered payloads during rollout.