Skip to content
Finowl

Laravel · Gender prediction

Queue gender enrichment jobs in Laravel

EnrichCustomerGender is a queued job that takes up to 50 customer ids, loads the models, and sends them to /v1/gender/batch as name items with an optional country_code hint. array_filter removes the country key when a customer has none, keeping each item valid. Results come back in order with each item's id, so the job updates the matching Customer through the keyed collection without another query.

The job saves result_status for every prediction, including unknown ones, where gender is null and no further lookups are needed. Item errors are skipped so a later run can try them again. On a 429 rate_limited response the job releases itself for the Retry-After period. Every other failure calls fail, because a batch that fails validation, lacks credits or hits a server error should not be resent automatically.

Steps

  1. 1

    Add gender_status, predicted_gender and gender_confidence columns with a migration and make them fillable.

  2. 2

    Create the job with php artisan make:job EnrichCustomerGender and paste in the example.

  3. 3

    Dispatch it in chunks of 50 from a scheduled Artisan command, as shown at the bottom.

  4. 4

    Run a queue worker and inspect a few rows, including ones that came back unknown.

  5. 5

    Use the stored values only in aggregate reports or as profile suggestions customers can edit.

Install
php artisan make:rule NotBreachedPassword
php artisan make:listener CheckEmailBreaches --event="Illuminate\Auth\Events\Registered"
php artisan make:job EnrichCustomerGender
Queue gender enrichment jobs in Laravel
<?php
// app/Jobs/EnrichCustomerGender.php
namespace App\Jobs;

use App\Exceptions\FinowlException;
use App\Models\Customer;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Http;

class EnrichCustomerGender implements ShouldQueue
{
    use Queueable;

    /** @param  array<int, int>  $customerIds  at most 50, one batch request per job */
    public function __construct(public array $customerIds) {}

    public function handle(): void
    {
        $customers = Customer::whereKey($this->customerIds)->get()->keyBy('id');

        $items = $customers->map(fn (Customer $c) => array_filter([
            'id' => (string) $c->id,
            'type' => 'name',
            'value' => $c->first_name,
            'country' => $c->country_code, // optional ISO 3166-1 alpha-2 hint
        ]))->values()->all();

        $response = Http::finowl()->timeout(30)->post('/v1/gender/batch', ['items' => $items]);

        if ($response->failed()) {
            $error = FinowlException::fromResponse($response);
            if ($error->status === 429 && $error->errorCode === 'rate_limited') {
                $this->release($error->retryAfter ?? 60); // nothing ran, safe to try again
                return;
            }
            $this->fail($error); // 400, 401, quota_exceeded or 5xx: never resend blindly
            return;
        }

        foreach ($response->json('data') as $row) {
            if (isset($row['error'])) {
                continue; // item-level failure; it stays null and a later run picks it up
            }
            $prediction = $row['data'];
            $customers[(int) $row['id']]->update([
                'gender_status' => $prediction['result_status'], // identified or unknown
                'predicted_gender' => $prediction['gender'],     // null when unknown
                'gender_confidence' => $prediction['confidence'],
            ]);
        }
    }
}

// Dispatch in chunks of 50, e.g. from a scheduled command:
// Customer::whereNull('gender_status')->where('first_name', '!=', '')
//     ->chunkById(50, fn ($chunk) => EnrichCustomerGender::dispatch($chunk->modelKeys()));

IN PRODUCTION

Production tips

  • Filter out customers with empty first names before dispatching. Batch validation is all or nothing, so one invalid item makes the request return 400 and none of the names are processed.

  • Add the RateLimited job middleware with a shared limiter slightly under 60 per minute, so several queue workers together stay inside your account's request limit.

  • Keep predicted_gender separate from any gender column customers fill in themselves. Predictions estimate typical name usage and can be wrong for individuals, so the customer's own answer always wins.

Each lookup costs 1 credit (3 when it requests AI), with 100 free credits every month. See pricing and error codes.

A FEW GOOD QUESTIONS

Common questions

Queue gender enrichment jobs in Laravel

Grab an API key, paste the example and run it. 100 free credits every month.