Skip to content
DocsProductSign inCreate an account

Platform

Internal API

How Gumza provisions a delivery account on Gumza Dispatch the moment a merchant finishes signup. Documented because it is the pattern to copy if you are putting your own front door in front of Dispatch.

What it solves

A merchant should sign up once. They fill in one form on Gumza, and deliveries work — no second signup on Dispatch, no copying an API key between two dashboards. So at the moment signup completes, the Gumza backend calls Dispatch, creates the matching merchant, and stores the API key and webhook secret it gets back.

The merchant never sees any of it. What they see is a Delivery settings page that already says Connected.

Authentication

One shared secret, INTERNAL_API_KEY, set to the same value on both deployments and sent as X-Internal-Key. It is compared in constant time. If the variable is unset, the surface answers 503 INTERNAL_API_DISABLED — off, never open.

Headers
X-Internal-Key: <32+ random characters, identical on both sides>
Content-Type: application/json

Endpoints

  • POST/api/v1/internal/merchantsCreate a merchant and optionally issue its credentials
  • POST/api/v1/internal/merchants/:merchantId/credentialsRe-mint credentials for a merchant that already exists

Create a merchant

FieldTypeDescription
namestring
required
Business name. The slug is derived from it, with a random suffix if that slug is taken.
contactEmailstring
required
Normalised to lowercase.
contactPhonestring
required
Normalised to Kenyan format; an unparseable number is a 422.
categoryenum
optional
fashion, beauty, electronics, pharmacy, food, retail or other.
salesChannelsenum[]
optional
online_store, instagram, whatsapp, in_store. Defaults to empty.
defaultPickupAddressstring
optional
Where riders collect. Set it here or the merchant cannot dispatch from a connected store.
tenantSlugstring
optional
Which operator tenant to create under. Defaults to the configured self-signup tenant.
issueApiKeyboolean
optional
Defaults to true. Returns the plaintext key once.
webhookUrlstring
optional
Your public endpoint for status events. Setting it returns a webhook secret, once.
gumzaStaffIduuid
optional
Who completed signup. Audit trail only.
POST /api/v1/internal/merchants
curl -X POST https://api.dispatch.gumza.ke/api/v1/internal/merchants \
  -H "X-Internal-Key: $INTERNAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Example Stores",
    "contactEmail": "owner@example.invalid",
    "contactPhone": "+254700000001",
    "category": "retail",
    "salesChannels": ["whatsapp", "in_store"],
    "defaultPickupAddress": "Shop 4, Westlands, Nairobi",
    "issueApiKey": true,
    "webhookUrl": "https://gumza.ke/webhooks/dispatch/easybox"
  }'

# → {
#     "success": true,
#     "data": {
#       "merchantId": "8f1c…",
#       "tenantId": "2a7e…",
#       "slug": "example-stores",
#       "apiKey": "ebx_7f3c91a2.9Xq2…",      // shown once, never again
#       "webhookSecret": "kR3n…"              // shown once, never again
#     }
#   }

Repairing a half-finished link

The interesting case is not the happy path. It is: Dispatch created the merchant, answered 200, and then the caller could not keep the credentials — because its own encryption key was not configured, or the write failed after the merchant row was already recorded. The merchant exists and is unusable.

The credentials endpoint is the repair. It is what sits behind the Connect delivery button on the Gumza settings page.

POST /api/v1/internal/merchants/:merchantId/credentials
curl -X POST https://api.dispatch.gumza.ke/api/v1/internal/merchants/$MERCHANT_ID/credentials \
  -H "X-Internal-Key: $INTERNAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "issueApiKey": true }'
  • issueApiKey is additive. Existing keys keep working, so this is safe to call more than once and safe to call on a live merchant.
  • webhookUrl is not additive — passing it rotates the webhook secret and invalidates the previous one. Send it only when you have no usable secret.
  • A suspended merchant is refused with 409. Handing out a working key for an account whose sessions are already rejected would only produce confusing failures later.

Failure modes worth handling

ResponseMeansDo
503 INTERNAL_API_DISABLEDNo shared key configured on the Dispatch sideCreate the account without a delivery link; let the merchant retry later
401Keys do not match between the two deploymentsFix the configuration; do not retry
409A merchant with these details already exists, or is suspendedRepair with the credentials endpoint instead of creating again
422Unparseable phone, or a name with no letters or digits to slugSurface the field error in your signup form
502 / timeoutDispatch unreachableFinish signup anyway; the merchant retries from Settings

Shared identity

Both products validate sessions against the same JWT_SECRET, so a token issued by one is accepted by the other. That is what lets a merchant move between the Gumza dashboard and Dispatch views without signing in twice. If you are running both, the two secrets must match — and every other rule about them applies twice over.