Bytloop MailDocs
Status

Getting started

  • Introduction
  • Quickstart

Sending email

  • Send a transactional email
  • Sending domains vs mailboxes

Framework guides

  • Send email from Next.js
  • Send email from Ruby on Rails
  • Send email from Django
  • Send email from Laravel

Domains

  • Verify a domain

Receiving email

  • Receive email
  • Webhooks

Reference

  • Rate limits
  • Errors
  • SDKs

Framework guides

Send email from Laravel

Send transactional email from Laravel with Http::, as a service class and as a queued job.

Try it live

Every endpoint in this doc runs against your workspace from the interactive API explorer. Prefill a request, hit Send, and see the response with your own keys.

This is the same POST /api/v1/emails endpoint documented in Send a transactional email, wired into Laravel with the built-in Http facade — no extra package needed.

Service class#

<?php
// app/Services/BytloopMailClient.php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use RuntimeException;

class BytloopMailClient
{
    public static function sendEmail(
        string $from,
        array|string $to,
        string $subject,
        string $html,
        string $idempotencyKey,
        array $tags = [],
    ): array {
        $response = Http::withToken(config('services.bytloop_mail.api_key'))
            ->withHeaders(['Idempotency-Key' => $idempotencyKey])
            ->post('https://mail.bytloop.com/api/v1/emails', [
                'from' => $from,
                'to' => is_array($to) ? $to : [$to],
                'subject' => $subject,
                'html' => $html,
                'tags' => $tags,
            ]);

        if ($response->failed()) {
            throw new RuntimeException(
                "Bytloop Mail send failed ({$response->status()}): {$response->body()}"
            );
        }

        return $response->json();
    }
}
<?php
// app/Services/BytloopMailClient.php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use RuntimeException;

class BytloopMailClient
{
    public static function sendEmail(
        string $from,
        array|string $to,
        string $subject,
        string $html,
        string $idempotencyKey,
        array $tags = [],
    ): array {
        $response = Http::withToken(config('services.bytloop_mail.api_key'))
            ->withHeaders(['Idempotency-Key' => $idempotencyKey])
            ->post('https://mail.bytloop.com/api/v1/emails', [
                'from' => $from,
                'to' => is_array($to) ? $to : [$to],
                'subject' => $subject,
                'html' => $html,
                'tags' => $tags,
            ]);

        if ($response->failed()) {
            throw new RuntimeException(
                "Bytloop Mail send failed ({$response->status()}): {$response->body()}"
            );
        }

        return $response->json();
    }
}
// config/services.php
'bytloop_mail' => [
    'api_key' => env('BYTLOOP_API_KEY'),
],
// config/services.php
'bytloop_mail' => [
    'api_key' => env('BYTLOOP_API_KEY'),
],

Call it directly for a one-off send:

use App\Services\BytloopMailClient;

BytloopMailClient::sendEmail(
    from: 'Support <support@send.acme.com>',
    to: $order->customer_email,
    subject: "Your receipt for order #{$order->id}",
    html: view('emails.receipt', ['order' => $order])->render(),
    idempotencyKey: "receipt-{$order->id}",
    tags: ['receipt_for_order' => (string) $order->id],
);
use App\Services\BytloopMailClient;

BytloopMailClient::sendEmail(
    from: 'Support <support@send.acme.com>',
    to: $order->customer_email,
    subject: "Your receipt for order #{$order->id}",
    html: view('emails.receipt', ['order' => $order])->render(),
    idempotencyKey: "receipt-{$order->id}",
    tags: ['receipt_for_order' => (string) $order->id],
);

Queued job (recommended)#

An Http::post() call inside a controller blocks the response on the API round-trip. Wrap it in a queued job — the same pattern Laravel's own Mail::send() uses with ShouldQueue — so the response comes back immediately and a transient failure retries instead of failing silently:

<?php
// app/Jobs/SendReceiptEmail.php

namespace App\Jobs;

use App\Models\Order;
use App\Services\BytloopMailClient;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class SendReceiptEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 5;
    public array $backoff = [10, 30, 60, 120, 300];

    public function __construct(public Order $order) {}

    public function handle(): void
    {
        BytloopMailClient::sendEmail(
            from: 'Support <support@send.acme.com>',
            to: $this->order->customer_email,
            subject: "Your receipt for order #{$this->order->id}",
            html: view('emails.receipt', ['order' => $this->order])->render(),
            idempotencyKey: "receipt-{$this->order->id}",
            tags: ['receipt_for_order' => (string) $this->order->id],
        );
    }
}
<?php
// app/Jobs/SendReceiptEmail.php

namespace App\Jobs;

use App\Models\Order;
use App\Services\BytloopMailClient;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class SendReceiptEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 5;
    public array $backoff = [10, 30, 60, 120, 300];

    public function __construct(public Order $order) {}

    public function handle(): void
    {
        BytloopMailClient::sendEmail(
            from: 'Support <support@send.acme.com>',
            to: $this->order->customer_email,
            subject: "Your receipt for order #{$this->order->id}",
            html: view('emails.receipt', ['order' => $this->order])->render(),
            idempotencyKey: "receipt-{$this->order->id}",
            tags: ['receipt_for_order' => (string) $this->order->id],
        );
    }
}
// wherever the order is confirmed
SendReceiptEmail::dispatch($order);
// wherever the order is confirmed
SendReceiptEmail::dispatch($order);

Common pitfalls#

  • Calling BytloopMailClient::sendEmail synchronously from a controller. Blocks the request on the API round-trip for no reason the user can see. Use the queued job above for anything triggered by a request.
  • No $backoff / $tries. Without it, a QUEUE_CONNECTION=sync local setup masks the problem, but in production a transient network error or a 5xx during deploy just fails the job once and stops.
  • Passing the API key directly with env() outside config/. Laravel caches config in production (config:cache); reading env() directly in application code silently returns null once that cache is warm. Always go through config('services.bytloop_mail.api_key') as shown above.

Next#

  • Register a webhook endpoint (a Laravel route) to hear about delivery, bounces, and opens.
  • Read the rate limits page so a bulk send (e.g. an Artisan command iterating Order::where(...)->cursor()) doesn't get throttled mid-run.
PreviousSend email from DjangoNextVerify a domain

On this page

  • Service class
  • Queued job (recommended)
  • Common pitfalls
  • Next