Node.js Go Python Rust Java RubyCopy const BASE = ' https://sandbox-api.gozem.co/courier ' ;
const token = process . env . GOZEM_ACCESS_TOKEN ;
// Every response is { data, message }. Failures carry a stable `code`; branch on that
// rather than on `message`, which is human-facing text and may be reworded.
async function call ( method , path , body ) {
const res = await fetch ( BASE + path , {
method ,
headers : {
Authorization : ` Bearer ${ token } ` ,
... ( body ? { ' Content-Type ' : ' application/json ' } : {} ) ,
} ,
body : body ? JSON . stringify ( body ) : undefined ,
} );
const payload = await res . json ();
if ( ! res . ok ) throw new Error ( ` ${ res . status } ${ payload . code } : ${ payload . message } ` );
return payload . data ;
}
const pickup = {
label : ' Raku Raku Express HQ - Zone Aéroport ' ,
lat : 6.166245 ,
lon : 1.247951 ,
contact_name : ' Raku Raku Express ' ,
contact_phone : ' +22822200011 ' ,
} ;
const dropoffs = [
{
label : ' Pharmacie Tokoin Forever ' ,
lat : 6.172834 ,
lon : 1.231456 ,
contact_name : ' Kossi Mensah ' ,
contact_phone : ' +22893112233 ' ,
note : ' Hand to the pharmacist directly ' ,
},
];
const vehicles = await call ( ' GET ' , ` /v1/vehicles?lat= ${ pickup . lat } &lon= ${ pickup . lon } ` );
// Take `name`, not `guid`. It is what the quote and booking calls expect.
const vehicle = vehicles . find ( ( v ) => v . name === ' motorcycle ' ) ?? vehicles [ 0 ];
const quote = await call ( ' POST ' , ' /v1/quotes ' , {
title : ' Order #5589 ' ,
vehicle : vehicle . name ,
optimize_route : true ,
pickup ,
dropoffs ,
} );
// This is the number to show the customer. It holds until quote.expires_at.
console . log ( quote . estimated_fare . amount , quote . estimated_fare . currency_code );
const trip = await call ( ' POST ' , ' /v1/trips ' , { quote_id : quote . guid } );
// The trip guid is the key for every later call, so store it against your own order.
await saveOrder ({ tripId: trip . guid , tracking: trip . tracking_url });
// Call this from your webhook handler when courier.trip.completed arrives.
export async function onCompleted ( tripId ) {
const current = await call ( ' GET ' , ` /v1/trips/ ${ tripId } ` );
// Each dropoff carries its own completed_at: proof of delivery per stop.
const delivered = current . dropoffs . filter ( ( d ) => d . completed_at );
console . log ( ` ${ delivered . length } of ${ current . dropoffs . length } stops delivered ` );
const invoice = await call ( ' GET ' , ` /v1/trips/ ${ tripId } /invoice ` );
// Reconcile against payment.status. The trip's own status describes the delivery.
if ( invoice . payment . status === ' paid ' ) {
await settle ( tripId , invoice . invoice_number , invoice . fare_breakdown . total );
}
} package main
import (
" bytes "
" encoding/json "
" fmt "
" net/http "
" os "
)
const base = " https://sandbox-api.gozem.co/courier "
var token = os . Getenv ( " GOZEM_ACCESS_TOKEN " )
// Every response is { data, message }. Failures carry a stable code; branch on that
// rather than on message, which is human-facing text and may be reworded.
type envelope[ T any] struct {
Data T ` json:"data" `
Code string ` json:"code" `
Message string ` json:"message" `
}
func call [ T any]( method , path string , body any) (T, error ) {
var out envelope[T]
var payload [] byte
if body != nil {
payload , _ = json . Marshal ( body )
}
req , err := http . NewRequest ( method , base + path , bytes . NewReader ( payload ))
if err != nil {
return out . Data , err
}
req . Header . Set ( " Authorization " , " Bearer " + token )
if body != nil {
req . Header . Set ( " Content-Type " , " application/json " )
}
res , err := http . DefaultClient . Do ( req )
if err != nil {
return out . Data , err
}
defer res . Body . Close ()
if err := json . NewDecoder ( res . Body ). Decode ( & out ); err != nil {
return out . Data , err
}
if res . StatusCode >= 400 {
return out . Data , fmt . Errorf ( " %d %s : %s " , res . StatusCode , out . Code , out . Message )
}
return out . Data , nil
}
type Vehicle struct {
Name string ` json:"name" `
}
type Fare struct {
Amount int ` json:"amount" `
CurrencyCode string ` json:"currency_code" `
}
type Quote struct {
GUID string ` json:"guid" `
EstimatedFare Fare ` json:"estimated_fare" `
ExpiresAt string ` json:"expires_at" `
}
type Stop struct {
CompletedAt string ` json:"completed_at" `
}
type Trip struct {
GUID string ` json:"guid" `
TrackingURL string ` json:"tracking_url" `
Dropoffs []Stop ` json:"dropoffs" `
}
type Invoice struct {
InvoiceNumber string ` json:"invoice_number" `
Payment struct {
Status string ` json:"status" `
} ` json:"payment" `
FareBreakdown struct {
Total int ` json:"total" `
} ` json:"fare_breakdown" `
}
var pickup = map [ string ]any{
" label " : " Raku Raku Express HQ - Zone Aéroport " ,
" lat " : 6.166245 ,
" lon " : 1.247951 ,
" contact_name " : " Raku Raku Express " ,
" contact_phone " : " +22822200011 " ,
}
var dropoffs = [] map [ string ]any{{
" label " : " Pharmacie Tokoin Forever " ,
" lat " : 6.172834 ,
" lon " : 1.231456 ,
" contact_name " : " Kossi Mensah " ,
" contact_phone " : " +22893112233 " ,
" note " : " Hand to the pharmacist directly " ,
}}
func book () (Trip, error ) {
query := fmt . Sprintf ( " ?lat= %v &lon= %v " , pickup [ " lat " ], pickup [ " lon " ])
vehicles , err := call [[]Vehicle]( " GET " , " /v1/vehicles " + query , nil )
if err != nil {
return Trip{}, err
}
// Take Name, not the guid. It is what the quote and booking calls expect.
vehicle := vehicles [ 0 ]. Name
quote , err := call [Quote]( " POST " , " /v1/quotes " , map [ string ]any{
" title " : " Order #5589 " ,
" vehicle " : vehicle ,
" optimize_route " : true ,
" pickup " : pickup ,
" dropoffs " : dropoffs ,
})
if err != nil {
return Trip{}, err
}
// This is the number to show the customer. It holds until quote.ExpiresAt.
fmt . Println ( quote . EstimatedFare . Amount , quote . EstimatedFare . CurrencyCode )
trip , err := call [Trip]( " POST " , " /v1/trips " , map [ string ]any{ " quote_id " : quote . GUID })
if err != nil {
return Trip{}, err
}
// The trip guid is the key for every later call, so store it against your order.
saveOrder ( trip . GUID , trip . TrackingURL )
return trip , nil
}
// Call this from your webhook handler when courier.trip.completed arrives.
func onCompleted ( tripID string ) error {
current , err := call [Trip]( " GET " , " /v1/trips/ " + tripID , nil )
if err != nil {
return err
}
// Each dropoff carries its own CompletedAt: proof of delivery per stop.
delivered := 0
for _ , stop := range current . Dropoffs {
if stop . CompletedAt != "" {
delivered ++
}
}
fmt . Printf ( " %d of %d stops delivered \n " , delivered , len ( current . Dropoffs ))
invoice , err := call [Invoice]( " GET " , " /v1/trips/ " + tripID + " /invoice " , nil )
if err != nil {
return err
}
// Reconcile against Payment.Status. The trip's own status describes the delivery.
if invoice . Payment . Status == " paid " {
settle ( tripID , invoice . InvoiceNumber , invoice . FareBreakdown . Total )
}
return nil
} import os
import requests
BASE = " https://sandbox-api.gozem.co/courier "
token = os.environ[ " GOZEM_ACCESS_TOKEN " ]
def call ( method , path , body= None ) :
""" Every response is { success, data, message }.
Failures carry a stable `code`; branch on that rather than on `message`, which is
human-facing text and may be reworded.
"""
res = requests. request (
method ,
BASE + path ,
headers = { " Authorization " : f "Bearer {token} " } ,
json = body ,
timeout = 30 ,
)
payload = res. json ()
if not res.ok:
raise RuntimeError ( f " {res.status_code} {payload [ ' code ' ] } : {payload [ ' message ' ] } " )
return payload[ " data " ]
pickup = {
" label " : " Raku Raku Express HQ - Zone Aéroport " ,
" lat " : 6.166245 ,
" lon " : 1.247951 ,
" contact_name " : " Raku Raku Express " ,
" contact_phone " : " +22822200011 " ,
}
dropoffs = [
{
" label " : " Pharmacie Tokoin Forever " ,
" lat " : 6.172834 ,
" lon " : 1.231456 ,
" contact_name " : " Kossi Mensah " ,
" contact_phone " : " +22893112233 " ,
" note " : " Hand to the pharmacist directly " ,
}
]
vehicles = call ( " GET " , f "/v1/vehicles?lat= {pickup [ ' lat ' ] } &lon= {pickup [ ' lon ' ] } " )
# Take `name`, not `guid`. It is what the quote and booking calls expect.
vehicle = next ( (v for v in vehicles if v [ " name " ] == " motorcycle " ) , vehicles [ 0 ])
quote = call (
" POST " ,
" /v1/quotes " ,
{
" title " : " Order #5589 " ,
" vehicle " : vehicle [ " name " ] ,
" optimize_route " : True ,
" pickup " : pickup,
" dropoffs " : dropoffs,
} ,
)
# This is the number to show the customer. It holds until quote["expires_at"].
print ( quote [ " estimated_fare " ] [ " amount " ], quote [ " estimated_fare " ] [ " currency_code " ] )
trip = call ( " POST " , " /v1/trips " , { " quote_id " : quote [ " guid " ] } )
# The trip guid is the key for every later call, so store it against your own order.
save_order ( trip [ " guid " ] , trip [ " tracking_url " ])
def on_completed ( trip_id ) :
""" Call this from your webhook handler when courier.trip.completed arrives. """
current = call ( " GET " , f "/v1/trips/ {trip_id} " )
# Each dropoff carries its own completed_at: proof of delivery per stop.
delivered = [ d for d in current[ " dropoffs " ] if d. get ( " completed_at " ) ]
print ( f " { len ( delivered ) } of { len ( current [ ' dropoffs ' ]) } stops delivered" )
invoice = call ( " GET " , f "/v1/trips/ {trip_id} /invoice" )
# Reconcile against payment.status. The trip's own status describes the delivery.
if invoice[ " payment " ] [ " status " ] == " paid " :
settle ( trip_id , invoice [ " invoice_number " ] , invoice [ " fare_breakdown " ] [ " total " ] ) use serde :: Deserialize;
use serde_json :: json;
const BASE : & str = " https://sandbox-api.gozem.co/courier " ;
#[derive(Deserialize)]
struct Envelope<T> {
data : Option<T>,
code : Option<String>,
message : Option<String>,
}
#[derive(Deserialize)]
struct Vehicle {
name : String,
}
#[derive(Deserialize)]
struct Fare {
amount : i64,
currency_code : String,
}
#[derive(Deserialize)]
struct Quote {
guid : String,
estimated_fare : Fare,
}
#[derive(Deserialize)]
struct Stop {
completed_at : Option<String>,
}
#[derive(Deserialize)]
struct Trip {
guid : String,
tracking_url : String,
#[serde(default)]
dropoffs : Vec<Stop>,
}
#[derive(Deserialize)]
struct Payment {
status : String,
}
#[derive(Deserialize)]
struct Breakdown {
total : i64,
}
#[derive(Deserialize)]
struct Invoice {
invoice_number : String,
payment : Payment,
fare_breakdown : Breakdown,
}
/// Every response is { success, data, message }. Failures carry a stable `code`;
/// branch on that rather than on `message`, which may be reworded.
async fn call <T : for <'de> Deserialize<'de>>(
client : & reqwest :: Client,
method : reqwest :: Method,
path : & str,
body : Option< serde_json :: Value>,
) -> anyhow :: Result<T> {
let token = std :: env :: var ( " GOZEM_ACCESS_TOKEN " ) ? ;
let mut request = client . request ( method , format! ( " {BASE}{path} " )) . bearer_auth ( token );
if let Some( body ) = body {
request = request . json ( & body );
}
let response = request . send () . await ? ;
let status = response . status ();
let envelope : Envelope<T> = response . json () . await ? ;
if ! status . is_success () {
anyhow :: bail! (
" {} {}: {} " ,
status . as_u16 (),
envelope . code . unwrap_or_default (),
envelope . message . unwrap_or_default ()
);
}
envelope . data . ok_or_else ( || anyhow :: anyhow! ( " no data in response " ))
}
fn pickup () -> serde_json :: Value {
json! ({
" label " : " Raku Raku Express HQ - Zone Aéroport " ,
" lat " : 6.166245 ,
" lon " : 1.247951 ,
" contact_name " : " Raku Raku Express " ,
" contact_phone " : " +22822200011 "
})
}
fn dropoffs () -> serde_json :: Value {
json! ([{
" label " : " Pharmacie Tokoin Forever " ,
" lat " : 6.172834 ,
" lon " : 1.231456 ,
" contact_name " : " Kossi Mensah " ,
" contact_phone " : " +22893112233 " ,
" note " : " Hand to the pharmacist directly "
}])
}
async fn book ( client : & reqwest :: Client) -> anyhow :: Result<Trip> {
let vehicles : Vec<Vehicle> = call (
client ,
reqwest :: Method :: GET ,
" /v1/vehicles?lat=6.166245&lon=1.247951 " ,
None,
)
. await ? ;
// Take `name`, not the guid. It is what the quote and booking calls expect.
let vehicle = vehicles
. iter ()
. find ( | v | v . name == " motorcycle " )
. unwrap_or ( & vehicles [ 0 ]);
let quote : Quote = call (
client ,
reqwest :: Method :: POST ,
" /v1/quotes " ,
Some( json! ({
" title " : " Order #5589 " ,
" vehicle " : vehicle . name,
" optimize_route " : true ,
" pickup " : pickup (),
" dropoffs " : dropoffs ()
})),
)
. await ? ;
// This is the number to show the customer. It holds until quote.expires_at.
println! ( " {} {} " , quote . estimated_fare . amount, quote . estimated_fare . currency_code);
let trip : Trip = call (
client ,
reqwest :: Method :: POST ,
" /v1/trips " ,
Some( json! ({ " quote_id " : quote . guid })),
)
. await ? ;
// The trip guid is the key for every later call, so store it against your order.
save_order ( & trip . guid, & trip . tracking_url);
Ok( trip )
}
/// Call this from your webhook handler when courier.trip.completed arrives.
async fn on_completed ( client : & reqwest :: Client, trip_id : & str) -> anyhow :: Result<()> {
let current : Trip = call (
client ,
reqwest :: Method :: GET ,
& format! ( " /v1/trips/{trip_id} " ),
None,
)
. await ? ;
// Each dropoff carries its own completed_at: proof of delivery per stop.
let delivered = current . dropoffs . iter () . filter ( | s | s . completed_at . is_some ()) . count ();
println! ( " {delivered} of {} stops delivered " , current . dropoffs . len ());
let invoice : Invoice = call (
client ,
reqwest :: Method :: GET ,
& format! ( " /v1/trips/{trip_id}/invoice " ),
None,
)
. await ? ;
// Reconcile against payment.status. The trip's own status describes the delivery.
if invoice . payment . status == " paid " {
settle ( trip_id , & invoice . invoice_number, invoice . fare_breakdown . total);
}
Ok(())
} import com . fasterxml . jackson . databind . JsonNode ;
import com . fasterxml . jackson . databind . ObjectMapper ;
import java . net . URI ;
import java . net . http . HttpClient ;
import java . net . http . HttpRequest ;
import java . net . http . HttpResponse ;
import java . time . Duration ;
import java . util . Map ;
public class DeliverAPackage {
private static final String BASE = " https://sandbox-api.gozem.co/courier " ;
private static final String TOKEN = System . getenv ( " GOZEM_ACCESS_TOKEN " ) ;
private static final ObjectMapper MAPPER = new ObjectMapper () ;
private static final HttpClient CLIENT = HttpClient . newHttpClient () ;
/**
* Every response is { success, data, message }. Failures carry a stable code; branch
* on that rather than on message, which is human-facing and may be reworded.
*/
static JsonNode call ( String method , String path , Object body ) throws Exception {
HttpRequest . BodyPublisher payload =
body == null
? HttpRequest . BodyPublishers . noBody ()
: HttpRequest . BodyPublishers . ofString ( MAPPER . writeValueAsString ( body )) ;
HttpRequest . Builder request =
HttpRequest . newBuilder ()
. uri ( URI . create ( BASE + path ))
. header ( " Authorization " , " Bearer " + TOKEN )
. timeout ( Duration . ofSeconds ( 30 ))
. method ( method, payload ) ;
if (body != null ) request . header ( " Content-Type " , " application/json " ) ;
HttpResponse < String > response =
CLIENT . send ( request . build () , HttpResponse . BodyHandlers . ofString ()) ;
JsonNode envelope = MAPPER . readTree ( response . body ()) ;
if ( response . statusCode () >= 400 ) {
throw new RuntimeException (
response . statusCode () + " " + envelope . path ( " code " ) . asText ()
+ " : " + envelope . path ( " message " ) . asText ()) ;
}
return envelope . get ( " data " ) ;
}
static final Map < String , Object > PICKUP =
Map . of (
" label " , " Raku Raku Express HQ - Zone Aéroport " ,
" lat " , 6.166245 ,
" lon " , 1.247951 ,
" contact_name " , " Raku Raku Express " ,
" contact_phone " , " +22822200011 " ) ;
static final Object DROPOFFS =
java . util . List . of (
Map . of (
" label " , " Pharmacie Tokoin Forever " ,
" lat " , 6.172834 ,
" lon " , 1.231456 ,
" contact_name " , " Kossi Mensah " ,
" contact_phone " , " +22893112233 " ,
" note " , " Hand to the pharmacist directly " )) ;
static JsonNode book () throws Exception {
JsonNode vehicles = call ( " GET " , " /v1/vehicles?lat=6.166245&lon=1.247951 " , null ) ;
// Take name, not the guid. It is what the quote and booking calls expect.
String vehicle = vehicles . get ( 0 ) . get ( " name " ) . asText () ;
JsonNode quote =
call (
" POST " ,
" /v1/quotes " ,
Map . of (
" title " , " Order #5589 " ,
" vehicle " , vehicle,
" optimize_route " , true,
" pickup " , PICKUP,
" dropoffs " , DROPOFFS )) ;
// This is the number to show the customer. It holds until quote.expires_at.
JsonNode fare = quote . get ( " estimated_fare " ) ;
System . out . println ( fare . get ( " amount " ) + " " + fare . get ( " currency_code " ) . asText ()) ;
JsonNode trip = call ( " POST " , " /v1/trips " , Map . of ( " quote_id " , quote . get ( " guid " ) . asText ())) ;
// The trip guid is the key for every later call, so store it against your order.
saveOrder ( trip . get ( " guid " ) . asText () , trip . get ( " tracking_url " ) . asText ()) ;
return trip;
}
/** Call this from your webhook handler when courier.trip.completed arrives. */
static void onCompleted ( String tripId ) throws Exception {
JsonNode current = call ( " GET " , " /v1/trips/ " + tripId, null ) ;
// Each dropoff carries its own completed_at: proof of delivery per stop.
long delivered =
java . util . stream . StreamSupport . stream ( current . get ( " dropoffs " ) . spliterator () , false )
. filter ( stop -> stop . hasNonNull ( " completed_at " ))
. count () ;
System . out . println ( delivered + " of " + current . get ( " dropoffs " ) . size () + " stops delivered " ) ;
JsonNode invoice = call ( " GET " , " /v1/trips/ " + tripId + " /invoice " , null ) ;
// Reconcile against payment.status. The trip's own status describes the delivery.
if ( invoice . get ( " payment " ) . get ( " status " ) . asText () . equals ( " paid " ) ) {
settle (
tripId,
invoice . get ( " invoice_number " ) . asText () ,
invoice . get ( " fare_breakdown " ) . get ( " total " ) . asLong ()) ;
}
}
}