{"openapi":"3.1.0","info":{"title":"Osoro merchant API","version":"2026-09-11","description":"Book a delivery in Ghana, ask where it is, be told when it arrives. Authenticate with a key from the merchant portal (Developers) as `Authorization: Bearer osk_…`. There is no arrival-time estimate anywhere in this API, on purpose; the tracking page tells the recipient how many stops stand before theirs."},"servers":[{"url":"https://osoroghana.com/v1"}],"security":[{"apiKey":[]}],"paths":{"/performance":{"get":{"summary":"How your deliveries are going","description":"Your first-attempt rate, on time against the promise, journeys per parcel, what you were charged, your claims, and how many of your parcels went to a door we already knew. Counts are always present; a percentage is null until there are min_for_rate parcels behind it, and short_by says how many more it needs. Every percentage carries low and high — a 95% interval — so a rate over few parcels reads as the wide answer it is.","parameters":[{"name":"months","in":"query","schema":{"type":"integer","minimum":1,"maximum":24,"default":3},"description":"Whole calendar months back, including this one."}],"responses":{"200":{"description":"The scorecard","content":{"application/json":{"schema":{"type":"object","properties":{"min_for_rate":{"type":"integer"},"parcels":{"type":"integer"},"delivered":{"type":"integer"},"first_attempt":{"$ref":"#/components/schemas/Rate"},"on_time":{"$ref":"#/components/schemas/Rate"},"known_door":{"$ref":"#/components/schemas/Rate"},"claims":{"$ref":"#/components/schemas/Rate"},"journeys_per_parcel":{"type":["number","null"]},"fee_minor":{"type":"integer"},"fee_per_drop_minor":{"type":["integer","null"]},"late_credits":{"type":"integer"},"credited_minor":{"type":"integer"}}}}}}}}},"/codes/resolve":{"post":{"summary":"What an Osoro Code points at","description":"A customer earns a code from two deliveries we confirmed at their door, and hands it to anybody sending them something. This says what it resolves to IN WORDS — a neighbourhood — so you can show it on your checkout. It never returns a coordinate, a name or a phone number, and an unknown, paused or revoked code is refused in exactly the same words: a code must not be a way to ask whether somebody still lives somewhere. POST, and not GET, so the code stays out of access logs.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string","description":"As the customer said it: OS-4K2P-9TRW, os 4k2p 9trw, 4K2P9TRW"}}}}}},"responses":{"200":{"description":"In use","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string"},"display":{"type":"string","description":"OS-4K2P-9TRW"},"area":{"type":["string","null"],"description":"A neighbourhood, or null when no landmark is close enough to name"},"confirmed":{"type":"boolean"},"says":{"type":"string","description":"A sentence to show a sender"}}}}}},"404":{"description":"unknown_code — no code like that is in use, whatever the reason","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Slow down; Retry-After says how long","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/service-tiers":{"get":{"summary":"The promises on offer","description":"Each tier: a name, book-by and deliver-by times, how many days, the price relative to your agreed fee, and the share of the fee credited back if we miss it.","responses":{"200":{"description":"The active tiers","content":{"application/json":{"schema":{"type":"object","properties":{"tiers":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"},"promise":{"type":"string"},"promise_days":{"type":"integer"},"cutoff_local":{"type":"string"},"by_local":{"type":"string"},"price_multiplier":{"type":"number"},"late_credit_pct":{"type":"integer"}}}},"default":{"type":["string","null"]}}}}}}}}},"/me":{"get":{"summary":"Who this key belongs to","responses":{"200":{"description":"The merchant and the key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"description":"Not a valid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/bookings":{"post":{"summary":"Book a delivery","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":200},"description":"Send your own id (your order number will do). The same key from you within 24 hours returns the same booking instead of making a second one."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewBooking"}}}},"responses":{"200":{"description":"Replayed — this Idempotency-Key was already booked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"201":{"description":"Booked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"400":{"description":"A field is wrong; `field` names it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"destination_required, unknown_line, idempotency_conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Slow down; Retry-After says how long","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"summary":"List bookings, newest first","parameters":[{"name":"state","in":"query","schema":{"type":"string","enum":["booked","with_rider","at_the_door","delivered","failed","cancelled"]}},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"},"description":"Booked at or after"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"next_cursor from the previous page"}],"responses":{"200":{"description":"A page","content":{"application/json":{"schema":{"type":"object","properties":{"bookings":{"type":"array","items":{"$ref":"#/components/schemas/Booking"}},"next_cursor":{"type":["string","null"]}}}}}}}}},"/bookings/{ref}":{"get":{"summary":"One booking, by our ref or your reference","parameters":[{"name":"ref","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The booking with its events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"404":{"description":"No such booking — or not yours; the answer is the same","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/bookings/{ref}/cancel":{"post":{"summary":"Call a parcel off before the rider reaches the door","parameters":[{"name":"ref","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","minLength":3,"maxLength":300}}}}}},"responses":{"200":{"description":"Cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"409":{"description":"too_late (at the door or done) or already_cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/bookings/{ref}/proof":{"get":{"summary":"The evidence recorded at the door for your parcel","parameters":[{"name":"ref","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"What was recorded, and short-lived links to any photographs","content":{"application/json":{"schema":{"type":"object","properties":{"reference":{"type":"string"},"state":{"type":"string"},"link_expires_in_s":{"type":"integer","description":"How long each media_url stays valid. Fetch the picture, do not store the link."},"note":{"type":"string","nullable":true,"description":"Said in words when there is nothing to show, so an empty list is not read as a fault"},"proofs":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","example":"a photograph at the door"},"at":{"type":"string","format":"date-time"},"rider":{"type":"string","nullable":true,"description":"First name only"},"grade":{"type":"string","description":"How far the evidence carries — including when it does not","example":"checked, and it holds up"},"has_media":{"type":"boolean"},"media_url":{"type":"string","nullable":true,"description":"Relative, signed and short-lived"}}}}}}}}},"404":{"description":"No parcel of yours with that reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/bookings/{ref}/claims":{"post":{"summary":"Say a parcel was lost, damaged or short, and what it cost you","parameters":[{"name":"ref","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","amount","description"],"properties":{"kind":{"type":"string","enum":["lost","damaged","short","other"]},"amount":{"type":"number","description":"GHS, what it cost you"},"description":{"type":"string","minLength":3,"maxLength":600},"photo_sha256":{"type":"string","description":"A photograph already uploaded, by its sha-256 (optional)"}}}}}},"responses":{"201":{"description":"The claim, waiting for a decision","content":{"application/json":{"schema":{"type":"object","properties":{"claim":{"$ref":"#/components/schemas/Claim"}}}}}},"404":{"description":"No such booking — or not yours","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"That parcel already has an open claim","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/claims":{"get":{"summary":"Your claims and what was decided, newest first","responses":{"200":{"description":"Claims","content":{"application/json":{"schema":{"type":"object","properties":{"claims":{"type":"array","items":{"$ref":"#/components/schemas/Claim"}}}}}}}}}},"/events":{"get":{"summary":"What happened to your parcels, oldest first — for polling","parameters":[{"name":"since","in":"query","schema":{"type":"string","format":"date-time"},"description":"Default: the last 24 hours. Pass back next_since."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"name":"types","in":"query","schema":{"type":"string"},"description":"Comma-separated event names. Default: the seven booking events of 2026-09-11. claim.decided, return.requested, return.decided, return.collected, booking.instruction and booking.late are returned only when named here."}],"responses":{"200":{"description":"Events","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"next_since":{"type":"string","format":"date-time"}}}}}}}}},"/funds":{"get":{"summary":"Your wallet, what you owe, and whether an unfunded parcel is held","description":"Read this from your own checkout BEFORE you book, so a shop tops up rather than finding out at the door. All figures are cedis, not pesewas. `available` and `credit_limit` are null when no credit limit is set.","responses":{"200":{"description":"The position","content":{"application/json":{"schema":{"type":"object","properties":{"wallet":{"type":"number"},"owed":{"type":"number"},"credit_limit":{"type":["number","null"]},"available":{"type":["number","null"]},"currency":{"type":"string","const":"GHS"},"holds_when_unfunded":{"type":"boolean","description":"When true, a booking is held rather than sent if there is nothing behind it"}}}}}},"401":{"description":"Not a valid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks":{"get":{"summary":"Your webhook endpoints","responses":{"200":{"description":"Yours","content":{"application/json":{"schema":{"type":"object","properties":{"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}}}}}}}},"post":{"summary":"Add an endpoint","description":"The `secret` is returned ONCE, in this response, and never again — store it before you do anything else. It is what signs every delivery. Five live endpoints is the limit. The address must be https and must not resolve to a private range.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["booking.created","booking.with_rider","booking.collected","booking.at_the_door","booking.delivered","booking.failed","booking.cancelled","claim.decided","return.requested","return.decided","return.collected","booking.instruction","booking.late"]},"description":"Omit for all of them"},"description":{"type":"string","maxLength":120}}}}}},"responses":{"201":{"description":"Created, with the secret","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Webhook"},{"type":"object","properties":{"secret":{"type":"string","description":"whsec_… — shown once"}}}]}}}},"400":{"description":"The address is not usable; `field` names it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"too_many_webhooks","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/{webhook_id}":{"delete":{"summary":"Remove an endpoint, for good","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Gone. Deliveries already waiting are never sent."},"404":{"description":"Not one of yours","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/{webhook_id}/deliveries":{"get":{"summary":"What we tried to send you, and what your server said","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"Attempts, newest first","content":{"application/json":{"schema":{"type":"object","properties":{"deliveries":{"type":"array","items":{"type":"object","properties":{"delivery_id":{"type":"string"},"event_type":{"type":"string"},"ref":{"type":["string","null"]},"attempts":{"type":"integer"},"next_at":{"type":["string","null"],"format":"date-time"},"delivered_at":{"type":["string","null"],"format":"date-time"},"last_status":{"type":["integer","null"]},"last_error":{"type":["string","null"]},"gave_up_at":{"type":["string","null"],"format":"date-time"}}}}}}}}},"404":{"description":"Not one of yours","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/{webhook_id}/test":{"post":{"summary":"Send yourself a specimen delivery","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Queued — watch /deliveries for what your server answered"},"404":{"description":"Not one of yours","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/{webhook_id}/enable":{"post":{"summary":"Turn an endpoint back on after it switched itself off","description":"An endpoint that has failed for three days disables itself. This turns it back on FROM NOW: the deliveries missed while it was off are not replayed. Fetch what you missed with GET /events.","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"On again","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"}}}},"404":{"description":"Not one of yours","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/stores":{"get":{"summary":"Your connected Shopify and WooCommerce shops","responses":{"200":{"description":"Yours","content":{"application/json":{"schema":{"type":"object","properties":{"stores":{"type":"array","items":{"$ref":"#/components/schemas/Store"}}}}}}}}},"post":{"summary":"Connect a shop","description":"Returns the `webhook_url` to paste into that platform. The credential you give is sealed and never read back.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["platform","shop"],"properties":{"platform":{"type":"string","enum":["shopify","woocommerce"]},"shop":{"type":"string","description":"your-shop.myshopify.com, or https://shop.example.com"},"credential":{"type":"string"},"fulfil_on_delivery":{"type":"boolean"},"lines_from_store":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Connected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Store"}}}},"400":{"description":"`field` names what is wrong","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"too_many_stores","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/stores/{store_id}":{"delete":{"summary":"Disconnect a shop","parameters":[{"name":"store_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Disconnected"},"404":{"description":"Not one of yours","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/stores/{store_id}/orders":{"get":{"summary":"Orders that came from that shop, and what we booked for them","parameters":[{"name":"store_id","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"Orders, newest first","content":{"application/json":{"schema":{"type":"object","properties":{"orders":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"404":{"description":"Not one of yours","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sandbox/advance":{"post":{"summary":"TEST KEYS ONLY. Move a parcel one step, the way a rider’s handset moves it","description":"Writes the same events a real handset writes — assigned, arrived, delivered, in that order — so every projection, webhook and figure downstream is the real one. Call it again to take the next step. `fail: true` fails the attempt instead. Refused on a live key: there is no way to tell this system a real parcel was delivered when it was not.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ref"],"properties":{"ref":{"type":"string","description":"Our ref, or your reference"},"fail":{"type":"boolean"},"reason":{"type":"string","maxLength":120}}}}}},"responses":{"200":{"description":"Moved","content":{"application/json":{"schema":{"type":"object","properties":{"ref":{"type":"string"},"event":{"type":"string","enum":["booking.created","booking.with_rider","booking.collected","booking.at_the_door","booking.delivered","booking.failed","booking.cancelled","claim.decided","return.requested","return.decided","return.collected","booking.instruction","booking.late"],"description":"THIS API’s name for what was just written — the same word your webhook endpoint receives, never the board’s internal one"},"state":{"type":"string","enum":["booked","with_rider","at_the_door","delivered","failed","cancelled"],"description":"Where the parcel is now"},"says":{"type":"string","description":"What just happened, in words"}}}}}},"403":{"description":"not_the_sandbox — this is a live key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No parcel of yours with that reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"already_finished — it is delivered, failed or cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sandbox/reset":{"post":{"summary":"TEST KEYS ONLY. Start again","description":"DELETES NOTHING. The event log is append-only here as it is everywhere, so this cancels what is open and books five fresh parcels on top — which is what \"reset\" means in this system, and a sandbox that could erase its history would teach an integrator the one thing about it that is not true.","responses":{"200":{"description":"Cancelled and rebooked","content":{"application/json":{"schema":{"type":"object","properties":{"bookings":{"type":"array","items":{"type":"string"},"description":"The refs of the five new parcels"},"cancelled":{"type":"integer"}}}}}},"403":{"description":"not_the_sandbox — this is a live key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sandbox/messages":{"get":{"summary":"TEST KEYS ONLY. What your customer WOULD have been told","description":"Nothing in a sandbox business is ever handed to a message provider — the numbers in it are invented and belong to real people in Accra — but the messages are still composed and recorded. They are written the moment a step is taken, so they are here straight after /sandbox/advance or /sandbox/reset. This is how you read them. `truncated` says when the list stopped short of `total`.","responses":{"200":{"description":"The messages","content":{"application/json":{"schema":{"type":"object","properties":{"messages":{"type":"array","items":{"type":"object","properties":{"to":{"type":"string"},"body":{"type":"string"},"channel":{"type":"string"},"state":{"type":"string","enum":["not sent — sandbox"],"description":"Always \"not sent — sandbox\". Nothing here reaches anybody"},"sent_to_anybody":{"type":"boolean","enum":[false]},"recorded_as":{"type":"string","description":"The message queue’s own state for the row (pending until our drain has processed it, then sent — to the log, never to a provider). For debugging only"},"at":{"type":"string","format":"date-time"}}}},"total":{"type":"integer"},"truncated":{"type":"boolean"}}}}}},"403":{"description":"not_the_sandbox — this is a live key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"An osk_ key from the portal. A portal login (mt_) is refused here."}},"schemas":{"Rate":{"type":"object","description":"A proportion that knows whether it may be stated. `of` out of `n`. `pct` is null until `n` reaches min_for_rate — `short_by` says how many more are needed — and when it is present, `low` and `high` are a 95% interval around it.","properties":{"of":{"type":"integer"},"n":{"type":"integer"},"pct":{"type":["number","null"]},"low":{"type":["number","null"]},"high":{"type":["number","null"]},"short_by":{"type":"integer"}}},"Claim":{"type":"object","properties":{"claim_id":{"type":"string"},"ref":{"type":"string"},"kind":{"type":"string","enum":["lost","damaged","short","other"]},"amount":{"type":"number"},"currency":{"type":"string"},"description":{"type":"string"},"has_photo":{"type":"boolean"},"raised_at":{"type":"string","format":"date-time"},"state":{"type":"string","enum":["open","accepted","partial","rejected","withdrawn"]},"amount_accepted":{"type":"number","nullable":true},"decision_reason":{"type":"string","nullable":true},"decided_at":{"type":"string","format":"date-time","nullable":true},"credit_note":{"type":"string","nullable":true,"description":"The credit note number when accepted; it is on your next statement"}}},"Me":{"type":"object","required":["merchant","api_version","mode"],"properties":{"merchant":{"type":"string"},"key":{"type":["string","null"],"description":"The key’s label"},"api_version":{"type":"string"},"rate_limit":{"type":"object","properties":{"per_minute":{"type":"integer"},"burst":{"type":"integer"}}},"mode":{"type":"string","enum":["test","live"],"description":"THE FIRST FIELD TO PRINT. `test` is the sandbox, where nothing is real and no message is ever sent. A program that cannot tell which of the two it is talking to is one configuration file away from booking a real rider by accident."}}},"Webhook":{"type":"object","properties":{"webhook_id":{"type":"string"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"}},"description":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"last_success_at":{"type":["string","null"],"format":"date-time"},"last_failure_at":{"type":["string","null"],"format":"date-time"},"failing_since":{"type":["string","null"],"format":"date-time"},"disabled_at":{"type":["string","null"],"format":"date-time","description":"Set when it switched itself off after three days of failing"},"disabled_reason":{"type":["string","null"]},"waiting":{"type":"integer","description":"Deliveries queued for it right now"}}},"Store":{"type":"object","properties":{"store_id":{"type":"string"},"platform":{"type":"string","enum":["shopify","woocommerce"]},"shop":{"type":"string"},"webhook_url":{"type":"string","format":"uri","description":"Paste this into the platform"},"fulfil_on_delivery":{"type":"boolean"},"lines_from_store":{"type":"boolean"},"has_credential":{"type":"boolean","description":"Sealed; never read back"},"created_at":{"type":"string","format":"date-time"},"last_order_at":{"type":["string","null"],"format":"date-time"},"orders_booked":{"type":"integer"},"unfulfilled":{"type":"integer"},"last_error":{"type":["string","null"]},"last_error_at":{"type":["string","null"],"format":"date-time"},"disabled_at":{"type":["string","null"],"format":"date-time"}}},"NewBooking":{"type":"object","required":["recipient"],"properties":{"reference":{"type":"string","maxLength":120,"description":"Your order number. Find the booking by it later."},"osoro_code":{"type":"string","maxLength":24,"description":"An Osoro Code the customer gave you, like OS-4K2P-9TRW. It stands in for the address AND the phone number: both are on the job, neither comes back to you. With one, `recipient.phone` is optional."},"recipient":{"type":"object","properties":{"phone":{"type":"string","description":"Ghanaian mobile, any common spelling: 0244123456, +233244123456"},"name":{"type":"string","maxLength":120}}},"destination":{"type":"object","properties":{"description":{"type":"string","maxLength":500,"description":"As the customer would say it to a taxi driver. Optional ONLY for a number we have delivered to before."},"landmark":{"type":"string","maxLength":120}}},"cod_amount":{"type":"number","minimum":0,"description":"Cedis to collect at the door. 0 or absent: prepaid."},"notes":{"type":"string","maxLength":1000},"delivery_type":{"type":"string","enum":["home","office","shop","pharmacy","kiosk","warehouse"]},"hours":{"type":"string","maxLength":200,"description":"When the door is open: \"Mon-Fri 9-5\""},"window_hours":{"type":"integer","minimum":1,"maximum":336},"service_tier":{"type":"string","description":"A tier code from GET /v1/service-tiers. Fixes the promise (promised_by) and the price. Omit for your default."},"lines":{"type":"array","items":{"type":"object","required":["code","qty"],"properties":{"code":{"type":"string","description":"Your product code, for stock we hold for you"},"qty":{"type":"integer","minimum":1}}}}}},"Booking":{"type":"object","properties":{"ref":{"type":"string","description":"Ours. Unambiguous letters and digits; printed on the label."},"reference":{"type":["string","null"]},"state":{"type":"string","enum":["booked","with_rider","at_the_door","delivered","failed","cancelled"]},"state_since":{"type":["string","null"],"format":"date-time"},"failure_reason":{"type":["string","null"]},"recipient":{"type":"object","properties":{"phone":{"type":["string","null"]},"name":{"type":["string","null"]}}},"destination":{"type":"object","properties":{"description":{"type":["string","null"]},"confidence":{"type":["string","null"],"description":"How well we know the door: bound, confirmed, described, …"}}},"cod_amount":{"type":"number"},"currency":{"type":"string","const":"GHS"},"lines":{"type":"integer"},"has_proof":{"type":"boolean"},"delivery_code":{"type":["string","null"],"description":"Four digits the recipient says back to the rider; null once verified or when not required. db/211."},"service_tier":{"type":["string","null"],"description":"The promise this parcel was booked on, or null. db/212."},"promised_by":{"type":["string","null"],"format":"date-time","description":"When we said it would be delivered by. Missing it credits you the tier's stated share of the fee automatically."},"tracking_url":{"type":"string","format":"uri","description":"Give this to the recipient. It tells them how many stops stand before theirs."},"booked_via":{"type":"string","enum":["api","portal","shopify","woocommerce","desk"]},"created_at":{"type":"string","format":"date-time"},"events":{"type":"array","items":{"$ref":"#/components/schemas/Event"}}}},"Event":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["booking.created","booking.with_rider","booking.collected","booking.at_the_door","booking.delivered","booking.failed","booking.cancelled","claim.decided","return.requested","return.decided","return.collected","booking.instruction","booking.late"]},"ref":{"type":"string"},"reference":{"type":["string","null"]},"state":{"type":"string","enum":["booked","with_rider","at_the_door","delivered","failed","cancelled"]},"at":{"type":"string","format":"date-time"},"note":{"type":["string","null"]}}},"Error":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string","description":"A sentence for a person"},"code":{"type":"string","enum":["unauthorized","disabled","gone","not_found","invalid_request","destination_required","unknown_line","insufficient_stock","rejected","idempotency_conflict","too_late","already_cancelled","too_many_keys","too_many_stores","too_many_webhooks","not_configured","unknown_code","rate_limited","not_the_sandbox","already_finished"],"description":"A word for a program. unauthorized: the key is missing, wrong, or is a portal login rather than an API key; disabled: the key was valid and has been turned off; gone: the thing existed and has been removed; not_found: no such thing, or not yours; invalid_request: the body or a field is not something this endpoint accepts; destination_required: no address, and no delivery to that number before now to infer one from; unknown_line: a line names a product this business does not have; insufficient_stock: the product is known and there is not enough of it free; rejected: refused for a reason with no code of its own — read the sentence; idempotency_conflict: that Idempotency-Key was used with a different body; too_late: the parcel has moved past the point where this can be done; already_cancelled: it was cancelled before you asked; too_many_keys: the limit on live API keys for this business; too_many_stores: the limit on connected storefronts; too_many_webhooks: the limit on webhook endpoints; not_configured: the server is missing a setting this needs; ours to fix, not yours; unknown_code: no Osoro Code like that is in use; rate_limited: too many requests — slow down and retry; not_the_sandbox: a sandbox-only route was called with a live key; already_finished: that parcel is delivered, failed or cancelled, and cannot be moved again"},"field":{"type":"string"},"request_id":{"type":"string"}}}}}}