Laravel Daily's

Build a Speech-to-Text API with Laravel and the AI SDK: Audio to Actionable Data

hero image

Voice input is useful when your application needs information faster than users can type it. Support teams can transcribe calls. Sales teams can capture notes. Mobile users can submit voice messages without opening a keyboard.

Laravel gives you a clean way to expose this capability through a REST endpoint. The Laravel AI SDK handles the provider integration, while Laravel manages routing, validation, storage, queues, and testing.

This guide shows how to build a practical transcription API with Laravel. You will create an endpoint that accepts an audio upload and returns a JSON transcript.

Laravel is a productive PHP web framework for this workflow. Its built-in validation, filesystem, and queue features cover the application layer. The AI SDK supplies a unified transcription API across supported providers.

What We Are Building

The API will expose this endpoint:

POST /api/transcriptions
Content-Type: multipart/form-data

The request will contain an audio file. A successful response will look like this:

{
    "text": "Please schedule a call with the customer tomorrow morning."
}

The initial version is synchronous. The request uploads the audio, sends it to the configured AI provider, and returns the transcript.

That pattern works well for short recordings. Longer files should move to a queued workflow, which we will cover later.

Diagram showing an audio upload moving through a Laravel API, AI SDK, and JSON response

Install the Laravel AI SDK

Start with a Laravel application. Then install the SDK with Composer:

composer require laravel/ai

Publish the SDK configuration and migration files:

php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate

The published configuration lives in config/ai.php. Add the credentials for your chosen provider to .env.

For example:

OPENAI_API_KEY=your-api-key

The current AI SDK supports speech-to-text through OpenAI, ElevenLabs, Mistral, and Gemini. You can review the complete provider support matrix before selecting a provider.

The SDK keeps your application code consistent. Provider credentials and default models remain configuration concerns.

Define the Transcription Route

Create an invokable controller:

php artisan make:controller TranscriptionController --invokable

Register the route in routes/api.php:

<?php

use App\Http\Controllers\TranscriptionController;
use Illuminate\Support\Facades\Route;

Route::post('/transcriptions', TranscriptionController::class);

If the endpoint belongs to authenticated users, add your API authentication middleware. Laravel Sanctum is a practical option for first-party SPAs and mobile clients.

Route::middleware('auth:sanctum')->group(function () {
    Route::post('/transcriptions', TranscriptionController::class);
});

Keep the route focused. Validation and transcription logic belong in the controller or a dedicated application service.

Validate the Audio Upload

Audio is user-controlled input. Validate it before sending anything to an external provider.

Laravel includes file validation rules for MIME types, extensions, and file sizes. Use MIME validation when possible because the framework checks the file contents instead of trusting only the filename.

Open app/Http/Controllers/TranscriptionController.php:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Laravel\Ai\Transcription;

class TranscriptionController extends Controller
{
    public function __invoke(Request $request): JsonResponse
    {
        $request->validate([
            'audio' => [
                'required',
                'file',
                'mimetypes:audio/mpeg,audio/wav,audio/webm,audio/ogg',
                'max:25600',
            ],
        ]);

        $transcript = Transcription::fromUpload(
            $request->file('audio')
        )->generate();

        return response()->json([
            'text' => (string) $transcript,
        ]);
    }
}

The max:25600 rule limits the upload to 25 MB because file sizes are expressed in kilobytes.

Adjust the accepted MIME types and size for your provider. Some providers support additional formats. Your application should also define a duration limit if long recordings create unacceptable processing costs.

For larger applications, move these rules into a form request:

php artisan make:request TranscriptionRequest

A form request keeps authorization and validation outside the controller. Laravel automatically returns a 422 JSON response when validation fails for an API request.

Transcribe Audio with the AI SDK

The key operation is concise:

$transcript = Transcription::fromUpload(
    $request->file('audio')
)->generate();

The SDK also accepts files from a local path or Laravel storage:

use Laravel\Ai\Transcription;

$fromPath = Transcription::fromPath(
    storage_path('app/private/audio.mp3')
)->generate();

$fromStorage = Transcription::fromStorage(
    'audio/audio.mp3'
)->generate();

Cast the response to a string when you need the raw transcript:

$text = (string) $transcript;

The SDK handles provider-specific request details. Your controller does not need to construct multipart HTTP requests or parse provider response formats.

This is where Laravel’s ecosystem helps. The framework handles the HTTP boundary, while the AI SDK provides one expressive API for the provider call.

Store Audio Before Processing

The direct upload example is useful for short recordings. A production API often stores the source file first.

Laravel’s filesystem abstraction supports local storage, Amazon S3, and other S3-compatible services through the same API. Store the upload on a private disk:

$path = $request->file('audio')->store('transcriptions');

Then transcribe the stored file:

$transcript = Transcription::fromStorage($path)->generate();

You can persist the transcript with an Eloquent model:

$recording = Recording::create([
    'audio_path' => $path,
    'status' => 'processing',
]);

$transcript = Transcription::fromStorage($path)->generate();

$recording->update([
    'text' => (string) $transcript,
    'status' => 'completed',
]);

Keep recordings private unless users need public access. For downloads, generate short-lived URLs with Laravel’s temporary URL support.

Bright developer illustration of Laravel code, a microphone, waveform, and transcript card

Add Speaker Diarization

Meetings, interviews, and support calls often include multiple speakers. The AI SDK can request a diarized transcript:

$transcript = Transcription::fromStorage($path)
    ->diarize()
    ->generate();

The response includes the regular text transcript and speaker-segmented data. Store both when your application needs to display who said what.

A simple response can still expose the raw text:

return response()->json([
    'text' => (string) $transcript,
]);

For richer output, inspect the response object and map its diarized segments into your own API schema. Keep your public response format stable even if you change providers later.

Queue Longer Transcriptions

Speech processing can take longer than a normal HTTP request should remain open. Laravel’s queue system lets the API respond quickly while a worker processes the audio in the background.

The AI SDK supports queued transcription generation:

use Laravel\Ai\Responses\TranscriptionResponse;

Transcription::fromStorage($path)
    ->queue()
    ->then(function (TranscriptionResponse $transcript) use ($recording) {
        $recording->update([
            'text' => (string) $transcript,
            'status' => 'completed',
        ]);
    });

Your controller can return a 202 Accepted response:

return response()->json([
    'id' => $recording->id,
    'status' => $recording->status,
], 202);

The client can poll a status endpoint:

Route::get('/transcriptions/{recording}', function (Recording $recording) {
    return response()->json([
        'id' => $recording->id,
        'status' => $recording->status,
        'text' => $recording->text,
    ]);
});

Run a worker locally with:

php artisan queue:work

In production, use a process monitor or a managed platform such as Laravel Cloud. Monitor queue health and provider failures with appropriate application logging and Laravel Nightwatch.

Queueing also gives you a clean place to retry transient provider failures. Configure job attempts and timeouts based on the maximum recording length and provider response time.

Turn Transcripts into Actionable Data

A transcript is useful. Structured data is easier to query.

Once the audio becomes text, you can pass it to an AI SDK agent for tasks such as:

  • Extracting action items.
  • Identifying customer sentiment.
  • Finding names, dates, and order numbers.
  • Classifying support requests.
  • Creating a summary for a CRM record.

For example, store the raw transcript first. Then dispatch a second job that extracts structured fields. This separation makes each stage easier to retry and test.

A useful database shape might include:

recordings
- id
- audio_path
- status
- text
- language
- processed_at
- created_at
- updated_at

You can add a JSON column for extracted data:

- metadata

Keep the original transcript. It provides an audit trail and lets you reprocess the text when your extraction rules change.

Test Without Calling an AI Provider

The AI SDK includes a fake transcription implementation. Use it in feature tests:

use Illuminate\Http\UploadedFile;
use Laravel\Ai\Transcription;

test('audio can be transcribed', function () {
    Transcription::fake([
        'Please schedule a call tomorrow morning.',
    ]);

    $response = $this->postJson('/api/transcriptions', [
        'audio' => UploadedFile::fake()->create(
            'message.mp3',
            100,
            'audio/mpeg'
        ),
    ]);

    $response
        ->assertOk()
        ->assertJson([
            'text' => 'Please schedule a call tomorrow morning.',
        ]);
});

You can also assert that the transcription request used diarization:

Transcription::fake();

$this->postJson('/api/transcriptions', [
    'audio' => UploadedFile::fake()->create(
        'meeting.mp3',
        100,
        'audio/mpeg'
    ),
]);

Transcription::assertGenerated(function ($prompt) {
    return $prompt->isDiarized();
});

Use Storage::fake() when your application stores audio before processing. This keeps tests fast and prevents test files from reaching real storage.

These testing features are among the most useful PHP developer tools for building reliable AI workflows. External providers should be tested separately from your request validation and persistence logic.

Production Checklist

Before exposing the endpoint publicly, review these areas:

  • Require authentication for private recordings.
  • Validate MIME types and file size.
  • Store uploads on a private disk.
  • Limit recording duration where possible.
  • Add rate limiting to the upload route.
  • Queue long-running transcriptions.
  • Record provider errors and processing status.
  • Retry transient failures carefully.
  • Keep raw transcripts separate from extracted data.
  • Delete audio files when retention policies require it.
  • Test validation, storage, queueing, and provider calls independently.

You now have the core pieces needed to build REST API with PHP: a validated multipart endpoint, provider-independent transcription, private file storage, asynchronous processing, and testable application code.

Laravel keeps the application boundary familiar. The AI SDK keeps speech-to-text integration compact. Together, they give you a practical foundation for turning voice recordings into searchable, structured, and actionable data.

Production architecture illustration showing Laravel API, private storage, queue workers, AI providers, database records, and monitoring

Previous
Staying Fresh: Real-Time Polling in Inertia 3.x with usePoll
Next
Building an AI-Powered Laravel Support Assistant with OpenAI and Laravel Boost