---
title: "API resources — Messages for Laravel | Roundly"
description: "Drop-in JSON resources for threads, messages and participants — safe fields only, read state and reply info included."
url: https://roundly-consulting.com/open-source/docs/messages-for-laravel/api-resources
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Messages for Laravel](https://roundly-consulting.com/open-source/docs/messages-for-laravel.md)

# API resources

JSON resources are provided for building a messaging API or an Inertia/SPA backend. They expose only safe fields, render read state and reply info, and degrade gracefully when relations are not eager-loaded:

```php
use RoundlyConsulting\Messages\Facades\Messages;
use RoundlyConsulting\Messages\Http\Resources\MessageResource;
use RoundlyConsulting\Messages\Http\Resources\ParticipantResource;
use RoundlyConsulting\Messages\Http\Resources\ThreadResource;

return ThreadResource::collection(Messages::inboxFor($request->user()));
return MessageResource::collection(Messages::thread($thread)->messages());
```

## Fields

| Resource | Fields |
| --- | --- |
| `ThreadResource` | id, name, is\_direct, is\_public, is\_archived, last\_activity\_at, unread\_count (when present, e.g. from the inbox), latest\_message and participants (when loaded). |
| `MessageResource` | id, thread\_id, type, body (null once unsent), preview, is\_deleted, sender (when loaded, through resources.participant; null for a system message or a deleted sender), reply\_to (id, excerpt, sender\_id, sender\_type; excerpt is the parent’s preview() snapshot, sender\_type the quoted sender’s class basename), sent\_at, edited\_at (text messages only, from meta.edited\_at — null unless the text was really edited). |
| `ParticipantResource` | id, role, read\_at, joined\_at, participant (when loaded, through resources.participant; null when the model no longer exists). |
| `MessageParticipantResource` | The default for resources.participant: the model’s participateAs() when it implements ParticipatesInMessaging, otherwise id + type, where type is the class basename (User, never App\\Models\\User). |

Pair ThreadResource with Messages::inboxFor() — the inbox already eager-loads everything the resource reads, including unread\_count. Both paginators follow the request’s ?page=, so the envelope’s meta.current\_page and links.next stay in step for an infinite-scroll client.

No resource carries a PHP class name. A MessageResource renders like this:

```json
{
  "id": 7, "thread_id": 1, "type": "text", "body": "On it", "preview": "On it", "is_deleted": false,
  "sender": {"id": 4, "name": "Bob"},
  "reply_to": {"id": 6, "excerpt": "First", "sender_id": 3, "sender_type": "User"},
  "sent_at": "2026-10-18T09:30:00+00:00",
  "edited_at": null
}
```

## Participants and senders

participant and sender render through the class in resources.participant, MessageParticipantResource by default: the model’s participateAs() when it implements ParticipatesInMessaging (the {"id": 4, "name": "Bob"} above), otherwise its key and class basename — {"id": 12, "type": "Restaurant"}, never App\\Models\\Restaurant. To add what your API needs, point the key at your own JsonResource — it receives the participant model:

```php
// config/messages.php
'resources' => [
    'participant' => App\Http\Resources\ChatMemberResource::class,
],

// app/Http/Resources/ChatMemberResource.php
final class ChatMemberResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->resource->getKey(),
            'type' => class_basename($this->resource),
            'name' => $this->resource->name,
            'avatar_url' => $this->resource->avatar_url,
        ];
    }
}
```

MessageParticipantResource::configured() builds the configured resource around a model, or returns null for none. The package resources and the published copies render through it, so one swap reaches both:

```php
use RoundlyConsulting\Messages\Http\Resources\MessageParticipantResource;

MessageParticipantResource::configured($message->sender)?->resolve($request); // null for none
```

## Customising

Publish editable copies — ThreadResource, MessageResource and ParticipantResource in App\\Http\\Resources\\Messages (app/Http/Resources/Messages):

```bash
php artisan vendor:publish --tag="messages-resources"
```

They render exactly what the package’s resources render until you edit them; use them instead of the package classes in your controllers. ThreadResource renders its latest message and participants through your copies too. A MessageResource copy published before 1.1.0 still reads edited\_at off updated\_at, so an unsent or restored message shows as edited there — copy editedAt() over from the package’s MessageResource.

Copies published by 1.1.x or earlier keep the old output: the morph type — the class name on an app without a morph map — in participant.type (for a model without ParticipatesInMessaging) and reply\_to.sender\_type, and null for such a sender. Re-publish them with --force, which overwrites your edits, or copy the participant, sender and senderType() code over from the package’s resources.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/messages-for-laravel.md)
