Search by

flairuk / laravel-uber

ijeffro

A Laravel client for the Uber APIs: rides, guest rides, Uber Direct deliveries, Uber Eats Marketplace and Uber for Business, with OAuth, cached app tokens and verified webhooks.

Package info

github.com/FLAIRUK/laravel-uber

pkg:composer/flairuk/laravel-uber

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-10-05 11:36 UTC

This package is auto-updated.

Last update: 2026-10-05 11:36:33 UTC


README

Laravel Uber

PHP 8.2+  Laravel 12 or 13  Lint  Tests  Downloads on Packagist  MIT licence  12 Uber APIs 
 

Laravel Uber — A Laravel 12 and 13 client for the Uber developer platform: rides, guest and health rides, Uber Direct deliveries, the Uber Eats Marketplace, Uber for Business, Login with Uber, drivers, vehicle suppliers, Ads, AI Solutions and Uber Pay, behind one facade.

  • Every documented API. 12 APIs and about 250 endpoints, mapped from Uber's published OpenAPI specs and reference pages. Each one has a test that checks its method, path and scope.
  • Tokens handled. App tokens are fetched per scope with the client-credentials grant, cached until shortly before they expire, and replaced if Uber rejects them. User tokens from the OAuth flow (with PKCE or pushed authorization requests) are passed with withToken().
  • The right host. Production is api.uber.com. Each API's sandbox is chosen for you: sandbox-api for rides, test-api with sandbox-login tokens for Eats.
  • Verified webhooks. One route verifies Uber's HMAC signatures, whichever key the API signs with. It de-duplicates retries and dispatches Laravel events.
  • Safe retries. Reads are retried on connection errors and 5xx responses. Ride requests, deliveries, orders, charges and cancellations never are.
  • Real errors. Uber's error shapes are turned into typed exceptions that carry the error code, metadata and the surge-confirmation link.

This is an unofficial package. It is not affiliated with or endorsed by Uber. Most Uber APIs need approval from Uber for your app and its scopes.

📦 Installation · 🔑 Authentication · 🚗 Rides · 📦 Direct · 🍔 Eats · 💼 Business · 🧩 More · 🔔 Webhooks · ⚠️ Errors · ⚙️ Configuration



📦 Installation

composer require flairuk/laravel-uber
php artisan uber:install

uber:install publishes config/uber.php and adds these keys to .env and .env.example:

UBER_CLIENT_ID=              # from the Uber developer dashboard
UBER_CLIENT_SECRET=
UBER_SANDBOX=true            # use the sandbox until you go live
UBER_REDIRECT_URI=           # for Login with Uber / acting for a user
UBER_DIRECT_CUSTOMER_ID=     # Uber Direct only
UBER_ORGANIZATION_ID=        # Uber for Business third-party apps only
UBER_WEBHOOK_SIGNING_KEY=    # webhook signing keys, comma-separated

Then check that Uber issues a token for a scope your app is approved for:

php artisan uber:status --scope=eats.deliveries



🔑 Authentication

use FLAIRUK\Uber\Facades\Uber;

You can also type-hint FLAIRUK\Uber\Uber to have it injected.

App endpoints need no setup. These include Direct, Eats, Guest Rides, Health, Business, Vehicle Suppliers and AI Solutions. The package asks auth.uber.com for a client-credentials token with the scope each endpoint needs, and caches it. Uber allows only 100 token requests an hour, and each new token past that invalidates the oldest, so the cache matters.

User endpoints act for a person: a rider, a driver, a store owner activating your Eats integration, or an Ads user. Send them through Uber's OAuth flow, then pass their token:

// 1. Redirect the user to Uber
$pkce = Uber::oauth()::pkce();                         // optional; S256
session(['uber_state' => $state = Str::random(40), 'uber_verifier' => $pkce['verifier']]);

return redirect(Uber::oauth()->authorizeUrl(['profile', 'request', 'offline_access'], $state, codeChallenge: $pkce['challenge']));

// 2. On the callback
abort_unless(hash_equals(session('uber_state'), $request->state), 403);

$token = Uber::oauth()->exchangeCode($request->code, codeVerifier: session('uber_verifier'));
$user->update(['uber_token' => $token->toArray()]);    // store it, encrypted

// 3. Use it
$uber = Uber::withToken($user->uber_token);            // a string, an AccessToken or the stored array

if ($uber->token()->isExpired()) {
    $user->update(['uber_token' => ($fresh = Uber::oauth()->refresh($uber->token()))->toArray()]);
    $uber = Uber::withToken($fresh);
}

More OAuth:

Uber::oauth()->pushAuthorizationRequest($scopes, $state, loginHint: ['email' => $user->email]);   // PAR
Uber::oauth()->authorizeUrlFor($requestUri);
Uber::oauth()->revoke($token);
Uber::oauth()->certs();                     // JWKS for verifying ID tokens (cached)
Uber::identity()->me();                     // OpenID profile (/v3/me), with a user token



🚗 Rides

Riders API (acting for a rider)

$rides = Uber::withToken($token)->rides();

$rides->products(51.5074, -0.1278);
$rides->priceEstimates(51.5074, -0.1278, 51.4700, -0.4543);
$rides->timeEstimates(51.5074, -0.1278);

$fare = $rides->estimate(['product_id' => $productId, 'start_latitude' => 51.5074, /* ... */]);
$ride = $rides->request(['fare_id' => $fare['fare']['fare_id'], 'product_id' => $productId, /* ... */]);

$rides->find($ride['request_id']);
$rides->current();
$rides->cancel($ride['request_id']);
$rides->receipt($ride['request_id']);
$rides->me();
$rides->paymentMethods();
$rides->history()->lazy();                  // every past trip, page by page

Ride requests need the privileged request scope. If the product is surging, request() throws a ConflictException whose surgeConfirmationUrl() you send the rider to. Then request again with the surge_confirmation_id.

Uber's current reference page documents only products, estimates, me, payment methods and creating a request. The other endpoints (current trip, history, places, map and receipt) still work for existing integrations but no longer have reference pages.

In the sandbox, step a trip through its states yourself:

Uber::sandbox()->withToken($token)->rides()->sandbox()->setStatus($requestId, 'accepted');
Uber::sandbox()->withToken($token)->rides()->sandbox()->setProduct($productId, surgeMultiplier: 2.2);

Guest Rides and Uber Health

These order rides for people who don't need an Uber account, billed to your organisation. Guest Rides uses scope guests.trips and Health uses scope health; both share the same API shape.

$estimate = Uber::guestRides()->estimates([
    'pickup' => ['latitude' => 51.5074, 'longitude' => -0.1278],
    'dropoff' => ['latitude' => 51.4700, 'longitude' => -0.4543],
]);

$trip = Uber::guestRides()->create([
    'guest' => ['first_name' => 'Jane', 'last_name' => 'Doe', 'phone_number' => '+447700900000'],
    'pickup' => ['latitude' => 51.5074, 'longitude' => -0.1278],
    'dropoff' => ['latitude' => 51.4700, 'longitude' => -0.4543],
    'product_id' => $estimate[0]['product']['product_id'],
    'fare_id' => $estimate[0]['estimate_info']['fare_id'],
]);

Uber::guestRides()->find($trip['request_id']);
Uber::guestRides()->trips('ACTIVE')->lazy();
Uber::guestRides()->message($trip['request_id'], 'At the main entrance');
Uber::guestRides()->cancel($trip['request_id']);
Uber::guestRides()->zones($lat, $lng);
Uber::guestRides()->autocomplete('Heathrow Terminal 5');

Uber::health()->create($trip);              // identical calls under /v1/health
Uber::health()->widgetToken($userId);       // for Uber's booking and tracking widgets

Third-party apps acting for another organisation call forOrganization($orgId) or set UBER_ORGANIZATION_ID. Either way, the package sends the x-uber-organizationuuid header.

In the sandbox, create a run with test drivers and drive the trip through its states:

$run = Uber::sandbox()->guestRides()->sandbox()->createRun([...]);
$guests = Uber::sandbox()->guestRides()->inSandboxRun($run['run_id']);
$guests->create($trip);
Uber::sandbox()->guestRides()->sandbox()->driverState($run['run_id'], $driverId, 'ACCEPT');



📦 Uber Direct

Courier delivery from your store to your customer. It uses scope eats.deliveries and UBER_DIRECT_CUSTOMER_ID.

$store = ['street_address' => ['20 W 34th St'], 'city' => 'New York', 'state' => 'NY', 'zip_code' => '10001', 'country' => 'US'];

$quote = Uber::direct()->quote(['pickup_address' => $store, 'dropoff_address' => $customer]);   // arrays are JSON-encoded for you

$delivery = Uber::direct()->create([
    'quote_id' => $quote['id'],
    'pickup_name' => 'Store', 'pickup_address' => $store, 'pickup_phone_number' => '+15555555555',
    'dropoff_name' => 'Jane', 'dropoff_address' => $customer, 'dropoff_phone_number' => '+15555555556',
    'manifest_items' => [['name' => 'Bouquet', 'quantity' => 1, 'size' => 'medium']],
    'idempotency_key' => $order->uuid,
]);

Uber::direct()->find($delivery['id']);
Uber::direct()->list(['filter' => 'ongoing'])->lazy();
Uber::direct()->update($delivery['id'], ['tip_by_customer' => 300]);
Uber::direct()->cancel($delivery['id'], 'customer_called_to_cancel');
Uber::direct()->proofOfDelivery($delivery['id']);   // base64 image in ['document']
Uber::direct()->refund([...]);
Uber::direct()->stores($lat, $lng);

Uber::direct()->createTest($delivery);              // the robot courier completes it by itself
Uber::forCustomer($otherCustomerId)->direct()->find($id);

Merchant and store management uses scope direct.organizations:

Uber::direct()->organizations()->create([...]);
Uber::direct()->organizations()->invite($orgId, [...]);
Uber::direct()->businessLocations($orgId)->list()->lazy();
Uber::direct()->businessLocations($orgId)->update($locationId, ['name' => 'Soho']);

Uber Direct Japan uses the same API, so this client covers it too.



🍔 Uber Eats

For point-of-sale and order-management systems.

// Stores
Uber::eats()->stores()->list()->lazy();
Uber::eats()->stores()->find($storeId);
Uber::eats()->stores()->setStatus($storeId, 'OFFLINE', reason: 'Kitchen closed');
Uber::eats()->stores()->setHolidayHours($storeId, ['2026-12-25' => ['open_time_periods' => []]]);

// Menus (sent gzip-compressed)
Uber::eats()->menus()->replace($storeId, $menu);
Uber::eats()->menus()->updateItem($storeId, $itemId, ['suspension_info' => [...]]);

// Orders: accept or deny within 11.5 minutes of orders.notification
Uber::eats()->orders()->find($orderId, expand: ['carts', 'deliveries', 'payment']);
Uber::eats()->orders()->accept($orderId, ['ready_for_pickup_time' => now()->addMinutes(15)]);
Uber::eats()->orders()->deny($orderId, ['type' => 'STORE_CLOSED', 'info' => 'Closed early']);
Uber::eats()->orders()->ready($orderId);
Uber::eats()->orders()->forStore($storeId, ['state' => 'OFFERED'])->lazy();

// Stores still on the previous order API
Uber::eats()->legacyOrders()->accept($orderId);

// Promotions, reports, Paymentless Checkout, your own couriers
Uber::eats()->promotions()->create($storeId, [...]);
Uber::eats()->reports()->create('ORDER_HISTORY_REPORT', now()->subWeek(), now(), [$storeId]);   // link arrives by webhook
Uber::eats()->payments()->charge([...]);                                                     // an idempotency key is added
Uber::eats()->couriers()->location([...]);

Activating a store. The merchant authorizes your app with eats.pos_provisioning. Then, with their token:

$merchant = Uber::withToken($merchantToken)->eats();

$merchant->stores()->list();
$merchant->integrations()->activate($storeId, ['is_order_manager' => true, 'integrator_store_id' => $ourId]);

Uber then sends store.provisioned, and from then on app tokens work for that store.



💼 Uber for Business

Uber::business()->receipts()->find($orderId);                     // business.receipts
Uber::business()->vouchers()->create([...]);                       // organizations.voucher_programs
Uber::business()->vouchers()->generateCodes($programId, 50);
Uber::business()->employees()->create([...]);                      // business.employees
Uber::business()->employeeTrips()->create([...]);                  // business.trips
Uber::business()->organizations()->create([...]);                  // business.organizations
Uber::business()->statements()->find($statementId);                // business.statements

Uber::business()->consentUrl($scopes, $redirectUri, 'Acme Travel'); // third-party consent

Identity administration (org units, users and role assignments):

Uber::identity()->organization($rootOrgId)->createUser([...]);
Uber::identity()->organization($rootOrgId)->users()->lazy();



🧩 More APIs

Uber::withToken($driverToken)->drivers()->trips()->lazy();         // Drivers API
Uber::vehicleSuppliers()->vehicles()->create([...]);               // Uber for Suppliers
Uber::vehicleSuppliers()->shifts()->save($earnerId, $start, $end);
Uber::withToken($adsToken)->ads($accountId)->campaigns()->lazy();  // Uber Ads (login.uber.com OAuth)
Uber::aiSolutions()->translate('en_US', 'fr_FR', 'Your driver is here');
Uber::payments()->confirmDeposit($depositId);                      // Uber Pay (payment providers)
Uber::identity()->linkAccount($yourUserId);                        // account linking

Anything else

For endpoints without a wrapper, call them directly. The package still handles authentication, error mapping and retries:

Uber::get('v1/some/endpoint', ['q' => 1], scopes: ['some.scope']);
Uber::post('v1/some/endpoint', $body, scopes: ['some.scope']);
Uber::withToken($token)->send('PUT', 'v1/thing', ['json' => $body, 'headers' => [...]]);

Not wrapped:

  • Third-party Demand Rides and the /docs/secured APIs: partner-only, with no documented scopes.
  • Uber Freight: it has a separate portal.
  • SCIM: it needs a token issued through your identity provider.
  • Consumer Delivery: no endpoints are published.



🔔 Webhooks

The package registers POST /uber/webhook, which has no CSRF check. Point every Uber dashboard at it, then listen:

use FLAIRUK\Uber\Events\WebhookReceived;

Event::listen('uber.orders.notification', function (WebhookEvent $event) {
    AcceptOrder::dispatch($event->resourceId());       // queue it: answer Uber fast
});

Event::listen('uber.event.delivery_status', fn (WebhookEvent $event) => $event->status());   // Direct
Event::listen(WebhookReceived::class, fn (WebhookReceived $received) => logger($received->event->type));
  • Signatures. Every request must carry a valid X-Uber-Signature; Direct also sends X-Postmates-Signature, which is accepted too. Riders, Eats and Vehicle Suppliers sign with your client secret. Guest Rides, Health, Business and Direct sign with each webhook's signing key, so add those keys to UBER_WEBHOOK_SIGNING_KEY.
  • Retries and order. Uber retries failed deliveries and doesn't guarantee order. Events are de-duplicated by id for 48 hours. If a listener throws, the event is forgotten, so Uber's retry is processed. Fetch $event->resourceHref for the current state rather than trusting arrival order.
  • Your own route. Set UBER_WEBHOOK_PATH= (empty) and use the uber.webhook middleware on a route of your own.



⚠️ Errors

use FLAIRUK\Uber\Exceptions\{AuthenticationException, ConflictException, NotFoundException, RateLimitException, UberException, ValidationException};

try {
    Uber::direct()->create($delivery);
} catch (ValidationException $e) {      // 400 / 422: $e->errorCode, e.g. "address_undeliverable"
} catch (ConflictException $e) {        // 409: duplicate_delivery, surge, current_trip_exists…
    $e->metadata['delivery_id'] ?? $e->surgeConfirmationUrl();
} catch (RateLimitException $e) {       // 429
    $this->release($e->retryAfter());
} catch (AuthenticationException $e) {  // 401 / 403, or a refused token request
} catch (NotFoundException $e) {        // 404
} catch (UberException $e) {            // anything else; $e->response for the raw response
}

A ConfigurationException (also an UberException) means a credential or id is missing. It is thrown before anything is sent.



⚙️ Configuration

Key Env Default
client_id, client_secret UBER_CLIENT_ID, UBER_CLIENT_SECRET
sandbox UBER_SANDBOX false
base_url UBER_BASE_URL chosen per API
locale UBER_LOCALE Uber's default (sent as Accept-Language)
oauth.url UBER_OAUTH_URL https://auth.uber.com
oauth.redirect_uri UBER_REDIRECT_URI
oauth.scopes UBER_SCOPES profile
direct.customer_id UBER_DIRECT_CUSTOMER_ID
business.organization_id UBER_ORGANIZATION_ID
business.vouchers_scope UBER_VOUCHERS_SCOPE organizations.voucher_programs
health.scope UBER_HEALTH_SCOPE health
ads.account_id UBER_ADS_ACCOUNT_ID
webhooks.path UBER_WEBHOOK_PATH uber/webhook
webhooks.signing_keys UBER_WEBHOOK_SIGNING_KEY
timeout UBER_TIMEOUT 30 seconds
retry 2 retries, 500 ms apart (reads only)
cache_store UBER_CACHE_STORE the default store

Scope one call differently without changing the defaults:

Uber::sandbox()->guestRides()->estimates($route);
Uber::locale('fr_FR')->withToken($token)->rides()->products($lat, $lng);
Uber::forCustomer($customerId)->direct()->list();
Uber::guestRides()->forOrganization($orgId)->trips();



🧪 Testing

composer test

The client uses Laravel's HTTP client, so Http::fake() works in your own tests. Fake the token endpoint too:

Http::fake([
    'auth.uber.com/oauth/v2/token' => Http::response(['access_token' => 'test', 'expires_in' => 3600]),
    'api.uber.com/v1/customers/*/deliveries' => Http::response(['id' => 'del_test', 'status' => 'pending']),
]);



📄 License

The MIT License (MIT). See LICENSE for details.

Uber is a trademark of Uber Technologies, Inc. This package is not affiliated with or endorsed by Uber.