Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Rajaongkir Laravel Package

kodepintar/laravel-rajaongkir

A Laravel wrapper for the RajaOngkir API that makes it easy to look up Indonesian provinces, cities, districts, and shipping costs from couriers like JNE, TIKI, and POS, with simple configuration and a clean, expressive interface for your app.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install, pinned, and publish the config

    composer require kodepintar/laravel-rajaongkir:^3.0.4
    php artisan vendor:publish --tag=config
    

    Pin ^3.0.4 explicitly rather than ^3.0 — see the POST bug in the Gotchas section; you do not want a stale CI cache resolving 3.0.0–3.0.3.

    Put credentials in .env, not in the published config:

    RAJAONGKIR_API_KEY=your-key
    RAJAONGKIR_ACCOUNT_TYPE=starter   # starter | basic | pro
    

    Then php artisan config:clear.

  2. Where to look first

    • config/rajaongkir.php — the only file that matters for behaviour. Confirm three things: the base URL / account tier, the API key, and whether the endpoint paths are hardcoded or configurable. The tier determines which endpoints exist at all.
    • src/ — this is a thin wrapper. Read the client class end to end (it is likely one file). You need to know exactly how it builds URLs, how it attaches the key (header key vs query param), and how it decodes the response.
    • The service provider / facade alias — check composer.json extra.laravel or config/app.php aliases for the registered alias (e.g. RajaOngkir). Resolve from the container in your own code rather than relying on the alias, so tests can swap it.
  3. First use case: a cost lookup, end to end

    The core operation — and the one that exercises the POST path — is cost:

    $result = RajaOngkir::cost([
        'origin'      => 501,      // your warehouse city_id
        'destination' => 114,      // customer city_id
        'weight'      => 1200,     // grams, integer
        'courier'     => 'jne',
    ]);
    
    // The payload you actually want is buried three levels down
    $costs = data_get($result, 'rajaongkir.results.0.costs', []);
    

    Do this before you build any UI. If $costs comes back empty against a known-good origin/destination pair, you are almost certainly on a broken POST path or the wrong account tier — not a data problem.

    Prove the body arrives by temporarily logging the outbound request (see Gotchas). Then build the province/city dropdowns, which are the low-risk GET calls.

Implementation Patterns

  • Cache the location endpoints forever. Provinces, cities, and districts are effectively static reference data and the dominant cost of using this package is repeated lookups. Cache them in a Cache::rememberForever() keyed by tier, or better, dump them to a local table via an artisan command and query your own DB. This removes 90% of API calls and makes your checkout resilient to RajaOngkir being down.

    $cities = Cache::rememberForever('rajaongkir.cities', fn () =>
        collect(data_get(RajaOngkir::city(), 'rajaongkir.results', []))
            ->keyBy('city_id')
    );
    
  • Never trust a city_id posted from a form. Validate it against your cached list. A customer submitting destination=999999 should get a validation error, not a 500 from upstream.

  • Wrap the package in your own service behind an interface. The package is 7 stars and last released in 2022; assume you may need to swap it or call the API directly. One class is enough:

    interface ShippingCostProvider
    {
        /** @return ShippingOption[] */
        public function quote(int $originId, int $destinationId, int $grams, array $couriers): array;
    }
    

    Implement it by calling the package, normalising the deeply nested rajaongkir.results[].costs[].cost[] shape into your own DTOs, and dropping everything else. Application code should never touch data_get($x, 'rajaongkir.results.0.costs').

  • Query multiple couriers in one call where the tier supports it. courier accepts a comma-separated list (jne,tiki,pos) on paid tiers. On Starter you are limited to jne, pos, tiki anyway — so don't build a courier picker that lists couriers the tier can't serve.

  • Snapshot the chosen shipping option onto the order. At checkout you quote; at fulfilment you do not re-quote. Store courier, service code (REG, YES, …), cost, and estimated duration as columns on the order. Re-querying later gives you a different price and makes support tickets unresolvable.

  • Denormalise place names alongside IDs. Store destination_city_id and destination_city_name and destination_province_name. This keeps order history and invoices readable without a lookup, and survives a reference-data refresh.

  • Move cost lookups off the request path if the checkout is hot. RajaOngkir latency is variable. Either queue the quote or fetch it via an AJAX endpoint from the checkout page (in which case: cache per origin+destination+courier+weight-bucket for a few minutes, since customers toggle options repeatedly). Wrap real calls in ->retry(2, 200) only for reads — never retry a create-order call.

  • Handle "no service available" as a first-class state. costs is legitimately an empty array for remote destinations or overweight parcels. Return an empty collection from your provider and let the UI say "no shipping option" rather than throwing.

  • Test with Http::fake() and assert the request body. This is the regression guard the package itself lacked:

    Http::fake(['*' => Http::response(['rajaongkir' => ['status' => ['code' => 200], 'results' => [/* … */]]])]);
    
    app(ShippingCostProvider::class)->quote(501, 114, 1200, ['jne']);
    
    Http::assertSent(fn ($request) =>
        $request->method() === 'POST'
        && $request['origin'] === 501
        && $request['weight'] === 1200
    );
    
  • Add a rajaongkir:sync artisan command that refreshes the cached locations and logs the result. Run it on deploy or on a schedule; it is also your canary for "is the API key still valid / has the endpoint moved".

Gotchas and Tips

  • The 3.0.4 fix is directly on your critical path. The changelog reads "fix data tidak terkirim untuk method post" — POST payloads weren't transmitted. The cost endpoint is a POST. So on 3.0.0–3.0.3 your shipping quotes were the broken thing. Upgrade, then re-run a real quote against a known pair. A 200 response does not mean the body arrived; assert on the returned costs array, not the HTTP status.

  • Content-Type matters. If the upstream (or the package's client) sends application/json where the API expects form-encoded, you reproduce the empty-POST symptom exactly. If you are debugging, log the outbound Content-Type and body before blaming the API key.

  • HTTP 200 with an error inside. RajaOngkir wraps its own status in the body: rajaongkir.status.code can be 400/401/500 with an HTTP 200. A ->throw() on the HTTP response will not fire. Always check rajaongkir.status.code in your provider — a bad key and a bad city_id both look like success at the transport layer. This is the single most common source of confusing "empty results".

  • Account tier and base URL must agree. Starter/Basic/Pro are different hosts and different feature sets. Mismatching them produces 401s or missing endpoints. Starter has no international shipping, no subdistrict lookup, and a restricted courier list. If you config a Pro key against the Starter URL you will not get a helpful error.

  • weight is grams and must be a positive integer. A float, a string like "1,2 kg", or 0 will produce empty or nonsensical results. Compute volumetric weight upstream of the call and pass the greater of actual/volumetric, rounded up.

  • IDs arrive as strings. city_id and province_id come back as strings in some responses. Cast consistently at the boundary or your keyBy joins and validation rules will silently miss.

  • Stale config after env edits. php artisan config:clear after any .env change; config:cache on deploy. Half the "my key isn't working" reports are a cached config from a previous environment.

  • Set an explicit timeout. Do not inherit a 30s default in a checkout request. 5–10s, and treat a timeout as "fall back to a flat rate" rather than failing the order.

  • Maintenance risk is real. Last release 2022, low star count, Indonesian-only changelog, no support path. RajaOngkir itself has been migrating accounts to a newer host/API with a different auth header (x-api-key). Verify your key works against the host the package defaults to. If it does not, check whether the base URL and header name are configurable — if the header name is hardcoded, that is your cue to bypass the client and call the endpoint with Laravel's Http facade directly from your own ShippingCostProvider. That is a ~50-line change and, at this package's size, cheaper than fighting it.

  • Keep the API key server-side only. Never proxy it to a browser for dropdown population — ship the cached province/city list from your own endpoint instead.

  • Upgrade path, not fork path. Before forking, try: config overrides, a decorator around your own provider, and Http::macro() for cross-cutting logging. Fork only if the response-shape parsing itself is wrong.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource